mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-20 15:27:18 +00:00
Compare 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 | ||
|
|
819c5e823c | ||
|
|
6bae056e3a | ||
|
|
bb869a51fd | ||
|
|
25b3d08bb9 | ||
|
|
66eb890462 | ||
|
|
cde8ec8262 | ||
|
|
ef1429a639 | ||
|
|
85a011eb84 | ||
|
|
b5e585834d | ||
|
|
ae298fbc51 | ||
|
|
c02e1ed006 | ||
|
|
178c53ee84 | ||
|
|
2c23dbd2be | ||
|
|
3e017625a9 | ||
|
|
124037ce27 | ||
|
|
bc166a713d | ||
|
|
364a9fa7d7 | ||
|
|
f4546ba188 | ||
|
|
5de2a8f6ec | ||
|
|
2365cd2978 | ||
|
|
e8dd3511db | ||
|
|
ae40a9736a | ||
|
|
08cc93d337 | ||
|
|
5fdd9b773c | ||
|
|
e3e7ac1e68 | ||
|
|
10212bd38b | ||
|
|
4bec02f47f | ||
|
|
79f888fe47 | ||
|
|
9393bf7f79 | ||
|
|
1b1d4399a9 | ||
|
|
b2ad259a7c | ||
|
|
ac5c264090 | ||
|
|
fcfdd4e323 | ||
|
|
ea65bb2c0a | ||
|
|
b340055588 | ||
|
|
2918039b6f | ||
|
|
e8fd1f262a | ||
|
|
8887401644 | ||
|
|
f892db6ee8 | ||
|
|
829532c572 | ||
|
|
875eb1e773 |
No files matched your search
@@ -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
|
||||||
Whitespace-only changes.
@@ -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
|
||||||
|
|
||||||
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software, and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
|
|
||||||
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
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
|
##Features
|
||||||
|
|
||||||
@@ -14,9 +14,6 @@ in your own home, or over the internet.
|
|||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* Automatically finds GameStream-compatible PCs on your network
|
||||||
|
|
||||||
##Features in development
|
|
||||||
* Keyboard input
|
|
||||||
|
|
||||||
##Installation
|
##Installation
|
||||||
|
|
||||||
* Download and install Limelight for Android from
|
* 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
|
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
|
||||||
* Android device running 4.1 (Jelly Bean) or higher
|
* Android device running 4.1 (Jelly Bean) or higher
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
* High-end wireless router (802.11n dual-band recommended)
|
||||||
* Exynos/Snapdragon SoC __OR__ Quad-Core 1.4 GHz Cortex-A9 or higher (Tegra 3)
|
|
||||||
|
|
||||||
##Usage
|
##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')
|
||||||
|
}
|
||||||
File renamed without changes.
Binary file not shown.
File renamed without changes.
@@ -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"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.limelight"
|
package="com.limelight">
|
||||||
android:versionCode="28"
|
|
||||||
android:versionName="2.5.2" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="16"
|
|
||||||
android:targetSdkVersion="19" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@@ -16,67 +10,70 @@
|
|||||||
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/AppTheme" >
|
android:theme="@style/AppTheme" >
|
||||||
|
|
||||||
|
<!-- Launcher for traditional devices -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.PcView"
|
android:name=".PcView"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
android:label="@string/app_name" >
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="tv.ouya.intent.category.APP" />
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Launcher for Android TV devices -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.AppView"
|
android:name=".PcViewTv"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
android:logo="@drawable/atv_banner"
|
||||||
android:label="App List" >
|
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
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.StreamSettings"
|
android:name=".preferences.StreamSettings"
|
||||||
android:label="Streaming Settings" >
|
android:label="Streaming Settings" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.AdvancedSettings"
|
android:name=".preferences.AddComputerManually"
|
||||||
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:label="Add Computer Manually" >
|
android:label="Add Computer Manually" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.Game"
|
android:name=".Game"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||||
android:label="@string/title_activity_game"
|
|
||||||
android:parentActivityName="com.limelight.Connection"
|
|
||||||
android:theme="@style/FullscreenTheme" >
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.Connection" />
|
android:value="com.limelight.Connection" />
|
||||||
</activity>
|
</activity>
|
||||||
<service
|
<service
|
||||||
android:name="com.limelight.discovery.DiscoveryService"
|
android:name=".discovery.DiscoveryService"
|
||||||
android:label="mDNS PC Auto-Discovery Service" />
|
android:label="mDNS PC Auto-Discovery Service" />
|
||||||
<service
|
<service
|
||||||
android:name="com.limelight.computers.ComputerManagerService"
|
android:name=".computers.ComputerManagerService"
|
||||||
android:label="Computer Management Service" />
|
android:label="Computer Management Service" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import com.limelight.binding.PlatformBinding;
|
|||||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.R;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ import android.widget.AdapterView;
|
|||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
@@ -58,7 +60,10 @@ public class AppView extends Activity {
|
|||||||
return;
|
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 {
|
try {
|
||||||
ipAddress = InetAddress.getByAddress(address);
|
ipAddress = InetAddress.getByAddress(address);
|
||||||
@@ -76,7 +81,7 @@ public class AppView extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
long id) {
|
long id) {
|
||||||
AppObject app = (AppObject) appListAdapter.getItem(pos);
|
AppObject app = appListAdapter.getItem(pos);
|
||||||
if (app == null || app.app == null) {
|
if (app == null || app.app == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -91,13 +96,13 @@ public class AppView extends Activity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
registerForContextMenu(appList);
|
registerForContextMenu(appList);
|
||||||
updateAppList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +134,7 @@ public class AppView extends Activity {
|
|||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
AppObject selectedApp = (AppObject) appListAdapter.getItem(info.position);
|
AppObject selectedApp = appListAdapter.getItem(info.position);
|
||||||
if (selectedApp == null || selectedApp.app == null) {
|
if (selectedApp == null || selectedApp.app == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -154,7 +159,7 @@ public class AppView extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
AppObject app = (AppObject) appListAdapter.getItem(info.position);
|
AppObject app = appListAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId())
|
||||||
{
|
{
|
||||||
case RESUME_ID:
|
case RESUME_ID:
|
||||||
@@ -216,9 +221,9 @@ public class AppView extends Activity {
|
|||||||
|
|
||||||
// Success case
|
// Success case
|
||||||
return;
|
return;
|
||||||
} catch (GfeHttpResponseException e) {
|
} catch (GfeHttpResponseException ignored) {
|
||||||
} catch (IOException e) {
|
} catch (IOException ignored) {
|
||||||
} catch (XmlPullParserException e) {
|
} catch (XmlPullParserException ignored) {
|
||||||
} finally {
|
} finally {
|
||||||
spinner.dismiss();
|
spinner.dismiss();
|
||||||
}
|
}
|
||||||
@@ -4,19 +4,23 @@ package com.limelight;
|
|||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.input.ControllerHandler;
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.binding.input.KeyboardTranslator;
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
|
import com.limelight.binding.input.TouchContext;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevWatcher;
|
||||||
import com.limelight.binding.video.ConfigurableDecoderRenderer;
|
import com.limelight.binding.video.ConfigurableDecoderRenderer;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.NvConnectionListener;
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
import com.limelight.nvstream.StreamConfiguration;
|
import com.limelight.nvstream.StreamConfiguration;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
import com.limelight.nvstream.input.KeyboardPacket;
|
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.Dialog;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
@@ -31,6 +35,7 @@ import android.view.SurfaceHolder;
|
|||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnGenericMotionListener;
|
import android.view.View.OnGenericMotionListener;
|
||||||
|
import android.view.View.OnSystemUiVisibilityChangeListener;
|
||||||
import android.view.View.OnTouchListener;
|
import android.view.View.OnTouchListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
@@ -38,19 +43,21 @@ import android.view.WindowManager;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
|
||||||
public class Game extends Activity implements SurfaceHolder.Callback, OnGenericMotionListener, OnTouchListener, NvConnectionListener {
|
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||||
|
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||||
|
OnSystemUiVisibilityChangeListener
|
||||||
|
{
|
||||||
private int lastMouseX = Integer.MIN_VALUE;
|
private int lastMouseX = Integer.MIN_VALUE;
|
||||||
private int lastMouseY = Integer.MIN_VALUE;
|
private int lastMouseY = Integer.MIN_VALUE;
|
||||||
private int lastButtonState = 0;
|
private int lastButtonState = 0;
|
||||||
private int lastTouchX = 0;
|
|
||||||
private int lastTouchY = 0;
|
// Only 2 touches are supported
|
||||||
private boolean hasMoved = false;
|
private TouchContext[] touchContextMap = new TouchContext[2];
|
||||||
|
|
||||||
private ControllerHandler controllerHandler;
|
private ControllerHandler controllerHandler;
|
||||||
private KeyboardTranslator keybTranslator;
|
private KeyboardTranslator keybTranslator;
|
||||||
|
|
||||||
private int height;
|
private PreferenceConfiguration prefConfig;
|
||||||
private int width;
|
|
||||||
private Point screenSize = new Point(0, 0);
|
private Point screenSize = new Point(0, 0);
|
||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
@@ -59,7 +66,10 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
|
||||||
private boolean stretchToFit;
|
private EvdevWatcher evdevWatcher;
|
||||||
|
private int modifierFlags = 0;
|
||||||
|
private boolean grabbedInput = true;
|
||||||
|
private boolean grabComboDown = false;
|
||||||
|
|
||||||
private ConfigurableDecoderRenderer decoderRenderer;
|
private ConfigurableDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
@@ -72,33 +82,6 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
|
|
||||||
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 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 int FORCE_HARDWARE_DECODER = -1;
|
|
||||||
public static final int AUTOSELECT_DECODER = 0;
|
|
||||||
public static final int FORCE_SOFTWARE_DECODER = 1;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -122,6 +105,9 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for UI visibility events
|
||||||
|
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
|
||||||
|
|
||||||
// Change volume button behavior
|
// Change volume button behavior
|
||||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||||
|
|
||||||
@@ -132,31 +118,22 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
|
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
|
||||||
|
|
||||||
// Read the stream preferences
|
// Read the stream preferences
|
||||||
SharedPreferences prefs = getSharedPreferences(PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
|
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
|
switch (prefConfig.decoder) {
|
||||||
case Game.FORCE_SOFTWARE_DECODER:
|
case PreferenceConfiguration.FORCE_SOFTWARE_DECODER:
|
||||||
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
|
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
|
||||||
break;
|
break;
|
||||||
case Game.AUTOSELECT_DECODER:
|
case PreferenceConfiguration.AUTOSELECT_DECODER:
|
||||||
break;
|
break;
|
||||||
case Game.FORCE_HARDWARE_DECODER:
|
case PreferenceConfiguration.FORCE_HARDWARE_DECODER:
|
||||||
drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING;
|
drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
stretchToFit = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
if (prefConfig.stretchVideo) {
|
||||||
if (stretchToFit) {
|
|
||||||
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
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);
|
|
||||||
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
display.getSize(screenSize);
|
display.getSize(screenSize);
|
||||||
|
|
||||||
@@ -165,13 +142,6 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
sv.setOnGenericMotionListener(this);
|
sv.setOnGenericMotionListener(this);
|
||||||
sv.setOnTouchListener(this);
|
sv.setOnTouchListener(this);
|
||||||
|
|
||||||
SurfaceHolder sh = sv.getHolder();
|
|
||||||
|
|
||||||
if (stretchToFit) {
|
|
||||||
// Set the surface to the size of the video
|
|
||||||
sh.setFixedSize(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn the user if they're on a metered connection
|
// Warn the user if they're on a metered connection
|
||||||
checkDataConnection();
|
checkDataConnection();
|
||||||
|
|
||||||
@@ -185,13 +155,41 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
||||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
|
|
||||||
// Start the connection
|
decoderRenderer = new ConfigurableDecoderRenderer();
|
||||||
conn = new NvConnection(host, uniqueId, Game.this,
|
decoderRenderer.initializeWithFlags(drFlags);
|
||||||
new StreamConfiguration(app, width, height, refreshRate, bitrate * 1000, sops),
|
|
||||||
PlatformBinding.getCryptoProvider(this));
|
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);
|
keybTranslator = new KeyboardTranslator(conn);
|
||||||
controllerHandler = new ControllerHandler(conn);
|
controllerHandler = new ControllerHandler(conn);
|
||||||
decoderRenderer = new ConfigurableDecoderRenderer();
|
|
||||||
|
SurfaceHolder sh = sv.getHolder();
|
||||||
|
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
|
||||||
|
// Set the surface to the size of the video
|
||||||
|
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize touch contexts
|
||||||
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
|
touchContextMap[i] = new TouchContext(conn, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LimelightBuildProps.ROOT_BUILD) {
|
||||||
|
// Start watching for raw input
|
||||||
|
evdevWatcher = new EvdevWatcher(this);
|
||||||
|
evdevWatcher.start();
|
||||||
|
}
|
||||||
|
|
||||||
// The connection will be started when the surface gets created
|
// The connection will be started when the surface gets created
|
||||||
sh.addCallback(this);
|
sh.addCallback(this);
|
||||||
@@ -242,11 +240,11 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private void hideSystemUi() {
|
private void hideSystemUi(int delay) {
|
||||||
Handler h = getWindow().getDecorView().getHandler();
|
Handler h = getWindow().getDecorView().getHandler();
|
||||||
if (h != null) {
|
if (h != null) {
|
||||||
h.removeCallbacks(hideSystemUi);
|
h.removeCallbacks(hideSystemUi);
|
||||||
h.postDelayed(hideSystemUi, 1000);
|
h.postDelayed(hideSystemUi, delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,11 +252,11 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
|
|
||||||
SpinnerDialog.closeDialogs();
|
SpinnerDialog.closeDialogs(this);
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
conn.stop();
|
stopConnection();
|
||||||
|
|
||||||
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||||
@@ -287,6 +285,84 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
wifiLock.release();
|
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) {
|
private static byte getModifierState(KeyEvent event) {
|
||||||
byte modifier = 0;
|
byte modifier = 0;
|
||||||
if (event.isShiftPressed()) {
|
if (event.isShiftPressed()) {
|
||||||
@@ -301,6 +377,10 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
return modifier;
|
return modifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte getModifierState() {
|
||||||
|
return (byte) modifierFlags;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
InputDevice dev = event.getDevice();
|
InputDevice dev = event.getDevice();
|
||||||
@@ -322,6 +402,21 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
return super.onKeyDown(keyCode, event);
|
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,
|
keybTranslator.sendKeyDown(translated,
|
||||||
getModifierState(event));
|
getModifierState(event));
|
||||||
}
|
}
|
||||||
@@ -331,16 +426,6 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
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();
|
InputDevice dev = event.getDevice();
|
||||||
if (dev == null) {
|
if (dev == null) {
|
||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
@@ -360,6 +445,15 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
return super.onKeyUp(keyCode, event);
|
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,
|
keybTranslator.sendKeyUp(translated,
|
||||||
getModifierState(event));
|
getModifierState(event));
|
||||||
}
|
}
|
||||||
@@ -367,70 +461,67 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void touchDownEvent(int eventX, int eventY)
|
private TouchContext getTouchContext(int actionIndex)
|
||||||
{
|
{
|
||||||
lastTouchX = eventX;
|
if (actionIndex < touchContextMap.length) {
|
||||||
lastTouchY = eventY;
|
return touchContextMap[actionIndex];
|
||||||
hasMoved = false;
|
}
|
||||||
}
|
else {
|
||||||
|
return null;
|
||||||
public void touchUpEvent(int eventX, int eventY)
|
|
||||||
{
|
|
||||||
if (!hasMoved)
|
|
||||||
{
|
|
||||||
// We haven't moved so send a click
|
|
||||||
|
|
||||||
// Lower the mouse button
|
|
||||||
conn.sendMouseButtonDown((byte) 0x01);
|
|
||||||
|
|
||||||
// We need to sleep a bit here because some games
|
|
||||||
// do input detection by polling
|
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException e) {}
|
|
||||||
|
|
||||||
// Raise the mouse button
|
|
||||||
conn.sendMouseButtonUp((byte) 0x01);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void touchMoveEvent(int eventX, int eventY)
|
// Returns true if the event was consumed
|
||||||
{
|
private boolean handleMotionEvent(MotionEvent event) {
|
||||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
// Pass through keyboard input if we're not grabbing
|
||||||
{
|
if (!grabbedInput) {
|
||||||
hasMoved = true;
|
return false;
|
||||||
conn.sendMouseMove((short)(eventX - lastTouchX),
|
|
||||||
(short)(eventY - lastTouchY));
|
|
||||||
|
|
||||||
lastTouchX = eventX;
|
|
||||||
lastTouchY = eventY;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
if (controllerHandler.handleMotionEvent(event)) {
|
||||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||||
{
|
{
|
||||||
// This case is for touch-based input devices
|
// This case is for touch-based input devices
|
||||||
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN ||
|
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN ||
|
||||||
event.getSource() == InputDevice.SOURCE_STYLUS)
|
event.getSource() == InputDevice.SOURCE_STYLUS)
|
||||||
{
|
{
|
||||||
int eventX = (int)event.getX();
|
int actionIndex = event.getActionIndex();
|
||||||
int eventY = (int)event.getY();
|
|
||||||
|
int eventX = (int)event.getX(actionIndex);
|
||||||
|
int eventY = (int)event.getY(actionIndex);
|
||||||
|
|
||||||
|
TouchContext context = getTouchContext(actionIndex);
|
||||||
|
if (context == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.getActionMasked())
|
switch (event.getActionMasked())
|
||||||
{
|
{
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
touchDownEvent(eventX, eventY);
|
context.touchDownEvent(eventX, eventY);
|
||||||
break;
|
break;
|
||||||
|
case MotionEvent.ACTION_POINTER_UP:
|
||||||
case MotionEvent.ACTION_UP:
|
case MotionEvent.ACTION_UP:
|
||||||
touchUpEvent(eventX, eventY);
|
context.touchUpEvent(eventX, eventY);
|
||||||
|
if (actionIndex == 0 && event.getPointerCount() > 1) {
|
||||||
|
// The original secondary touch now becomes primary
|
||||||
|
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
touchMoveEvent(eventX, eventY);
|
// ACTION_MOVE is special because it always has actionIndex == 0
|
||||||
|
// We'll call the move handlers for all indexes manually
|
||||||
|
for (TouchContext aTouchContextMap : touchContextMap) {
|
||||||
|
aTouchContextMap.touchMoveEvent(eventX, eventY);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return super.onTouchEvent(event);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This case is for mice
|
// This case is for mice
|
||||||
@@ -438,30 +529,36 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
{
|
{
|
||||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
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 ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
conn.sendMouseButtonDown((byte) 0x01);
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
conn.sendMouseButtonUp((byte) 0x01);
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
|
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||||
conn.sendMouseButtonDown((byte) 0x03);
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
conn.sendMouseButtonUp((byte) 0x03);
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
|
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||||
conn.sendMouseButtonDown((byte) 0x02);
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
conn.sendMouseButtonUp((byte) 0x02);
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,31 +568,29 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return super.onTouchEvent(event);
|
// Unknown source
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handled a known source
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onTouchEvent(event);
|
// Unknown class
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
return handleMotionEvent(event) || super.onTouchEvent(event);
|
||||||
if (controllerHandler.handleMotionEvent(event)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
|
||||||
{
|
|
||||||
// Send a mouse move update (if neccessary)
|
|
||||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onGenericMotionEvent(event);
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||||
|
return handleMotionEvent(event) || super.onGenericMotionEvent(event);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void updateMousePosition(int eventX, int eventY) {
|
private void updateMousePosition(int eventX, int eventY) {
|
||||||
// Send a mouse move if we already have a mouse location
|
// Send a mouse move if we already have a mouse location
|
||||||
@@ -509,8 +604,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
|
|
||||||
// Scale the deltas if the device resolution is different
|
// Scale the deltas if the device resolution is different
|
||||||
// than the stream resolution
|
// than the stream resolution
|
||||||
deltaX = (int)Math.round((double)deltaX * ((double)width / (double)screenSize.x));
|
deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x));
|
||||||
deltaY = (int)Math.round((double)deltaY * ((double)height / (double)screenSize.y));
|
deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y));
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
}
|
}
|
||||||
@@ -522,15 +617,13 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||||
// Send it to the activity's motion event handler
|
return handleMotionEvent(event);
|
||||||
return onGenericMotionEvent(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
// Send it to the activity's touch event handler
|
return handleMotionEvent(event);
|
||||||
return onTouchEvent(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -544,16 +637,30 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
public void stageComplete(Stage stage) {
|
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
|
@Override
|
||||||
public void stageFailed(Stage stage) {
|
public void stageFailed(Stage stage) {
|
||||||
spinner.dismiss();
|
if (spinner != null) {
|
||||||
spinner = null;
|
spinner.dismiss();
|
||||||
|
spinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
|
stopConnection();
|
||||||
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
|
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
|
||||||
conn.stop();
|
|
||||||
connecting = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,21 +669,23 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
||||||
|
stopConnection();
|
||||||
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
|
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
|
||||||
conn.stop();
|
|
||||||
connected = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionStarted() {
|
public void connectionStarted() {
|
||||||
spinner.dismiss();
|
if (spinner != null) {
|
||||||
spinner = null;
|
spinner.dismiss();
|
||||||
|
spinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
connecting = false;
|
connecting = false;
|
||||||
connected = true;
|
connected = true;
|
||||||
|
|
||||||
hideSystemUi();
|
hideSystemUi(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -591,12 +700,14 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void displayTransientMessage(final String message) {
|
public void displayTransientMessage(final String message) {
|
||||||
runOnUiThread(new Runnable() {
|
if (!prefConfig.disableWarnings) {
|
||||||
@Override
|
runOnUiThread(new Runnable() {
|
||||||
public void run() {
|
@Override
|
||||||
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
|
public void run() {
|
||||||
}
|
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -610,8 +721,9 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
|
|
||||||
// Resize the surface to match the aspect ratio of the video
|
// Resize the surface to match the aspect ratio of the video
|
||||||
// This must be done after the surface is created.
|
// This must be done after the surface is created.
|
||||||
if (!stretchToFit) {
|
if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) {
|
||||||
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView), width, height);
|
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
||||||
|
prefConfig.width, prefConfig.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||||
@@ -622,8 +734,85 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
|||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
conn.stop();
|
stopConnection();
|
||||||
connected = false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseMove(int deltaX, int deltaY) {
|
||||||
|
conn.sendMouseMove((short) deltaX, (short) deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseButtonEvent(int buttonId, boolean down) {
|
||||||
|
byte buttonIndex;
|
||||||
|
|
||||||
|
switch (buttonId)
|
||||||
|
{
|
||||||
|
case EvdevListener.BUTTON_LEFT:
|
||||||
|
buttonIndex = MouseButtonPacket.BUTTON_LEFT;
|
||||||
|
break;
|
||||||
|
case EvdevListener.BUTTON_MIDDLE:
|
||||||
|
buttonIndex = MouseButtonPacket.BUTTON_MIDDLE;
|
||||||
|
break;
|
||||||
|
case EvdevListener.BUTTON_RIGHT:
|
||||||
|
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LimeLog.warning("Unhandled button: "+buttonId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (down) {
|
||||||
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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 java.net.UnknownHostException;
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
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;
|
||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
import com.limelight.nvstream.wol.WakeOnLanSender;
|
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||||
|
import com.limelight.preferences.AddComputerManually;
|
||||||
|
import com.limelight.preferences.StreamSettings;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -20,8 +23,10 @@ import android.app.Service;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@@ -59,6 +64,9 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
// Start updates
|
// Start updates
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
|
|
||||||
|
// Force a keypair to be generated early to avoid discovery delays
|
||||||
|
new AndroidCryptoProvider(PcView.this).getClientCertificate();
|
||||||
}
|
}
|
||||||
}.start();
|
}.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 APP_LIST_ID = 1;
|
||||||
private final static int PAIR_ID = 2;
|
private final static int PAIR_ID = 2;
|
||||||
private final static int UNPAIR_ID = 3;
|
private final static int UNPAIR_ID = 3;
|
||||||
private final static int WOL_ID = 4;
|
private final static int WOL_ID = 4;
|
||||||
private final static int DELETE_ID = 5;
|
private final static int DELETE_ID = 5;
|
||||||
|
|
||||||
@Override
|
private void initializeViews() {
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
|
|
||||||
// Bind to the computer manager service
|
// Set default preferences if we've never been run
|
||||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
Service.BIND_AUTO_CREATE);
|
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
settingsButton = (Button)findViewById(R.id.settingsButton);
|
settingsButton = (Button)findViewById(R.id.settingsButton);
|
||||||
addComputerButton = (Button)findViewById(R.id.manuallyAddPc);
|
addComputerButton = (Button)findViewById(R.id.manuallyAddPc);
|
||||||
|
|
||||||
pcList = (ListView)findViewById(R.id.pcListView);
|
pcList = (ListView)findViewById(R.id.pcListView);
|
||||||
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
|
pcList.setAdapter(pcListAdapter);
|
||||||
pcListAdapter.setNotifyOnChange(false);
|
pcList.setItemsCanFocus(true);
|
||||||
pcList.setAdapter(pcListAdapter);
|
pcList.setOnItemClickListener(new OnItemClickListener() {
|
||||||
pcList.setItemsCanFocus(true);
|
|
||||||
pcList.setOnItemClickListener(new OnItemClickListener() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
long id) {
|
long id) {
|
||||||
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(pos);
|
ComputerObject computer = pcListAdapter.getItem(pos);
|
||||||
if (computer.details == null) {
|
if (computer.details == null) {
|
||||||
// Placeholder item; no context menu for it
|
// Placeholder item; no context menu for it
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
// Open the context menu if a PC is offline
|
// Open the context menu if a PC is offline
|
||||||
openContextMenu(arg1);
|
openContextMenu(arg1);
|
||||||
}
|
}
|
||||||
else if (computer.details.pairState != PairState.PAIRED) {
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
// Pair an unpaired machine by default
|
// Pair an unpaired machine by default
|
||||||
doPair(computer.details);
|
doPair(computer.details);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
doAppList(computer.details);
|
doAppList(computer.details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
registerForContextMenu(pcList);
|
registerForContextMenu(pcList);
|
||||||
settingsButton.setOnClickListener(new OnClickListener() {
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
startActivity(new Intent(PcView.this, StreamSettings.class));
|
startActivity(new Intent(PcView.this, StreamSettings.class));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
addComputerButton.setOnClickListener(new OnClickListener() {
|
addComputerButton.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
@@ -129,7 +139,26 @@ public class PcView extends Activity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
private void startComputerUpdates() {
|
||||||
@@ -156,7 +185,7 @@ public class PcView extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopComputerUpdates() {
|
private void stopComputerUpdates(boolean wait) {
|
||||||
if (managerBinder != null) {
|
if (managerBinder != null) {
|
||||||
if (!runningPolling) {
|
if (!runningPolling) {
|
||||||
return;
|
return;
|
||||||
@@ -165,6 +194,11 @@ public class PcView extends Activity {
|
|||||||
freezeUpdates = true;
|
freezeUpdates = true;
|
||||||
|
|
||||||
managerBinder.stopPolling();
|
managerBinder.stopPolling();
|
||||||
|
|
||||||
|
if (wait) {
|
||||||
|
managerBinder.waitForPollingStopped();
|
||||||
|
}
|
||||||
|
|
||||||
runningPolling = false;
|
runningPolling = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +223,7 @@ public class PcView extends Activity {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
stopComputerUpdates();
|
stopComputerUpdates(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -201,13 +235,13 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
stopComputerUpdates();
|
stopComputerUpdates(false);
|
||||||
|
|
||||||
// Call superclass
|
// Call superclass
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
|
ComputerObject computer = pcListAdapter.getItem(info.position);
|
||||||
if (computer == null || computer.details == null) {
|
if (computer == null || computer.details == null) {
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
return;
|
return;
|
||||||
@@ -258,6 +292,9 @@ public class PcView extends Activity {
|
|||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
|
// Stop updates and wait while pairing
|
||||||
|
stopComputerUpdates(true);
|
||||||
|
|
||||||
InetAddress addr = null;
|
InetAddress addr = null;
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
addr = computer.localIp;
|
addr = computer.localIp;
|
||||||
@@ -312,6 +349,9 @@ public class PcView extends Activity {
|
|||||||
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start polling again
|
||||||
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -442,7 +482,7 @@ public class PcView extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
|
ComputerObject computer = pcListAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId())
|
||||||
{
|
{
|
||||||
case PAIR_ID:
|
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 {}
|
||||||
File renamed without changes.
File renamed without changes.
+15
-8
@@ -23,15 +23,16 @@ import java.security.spec.InvalidKeySpecException;
|
|||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name;
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
||||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
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.ContentSigner;
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
|
||||||
@@ -51,6 +52,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
private RSAPrivateKey key;
|
private RSAPrivateKey key;
|
||||||
private byte[] pemCertBytes;
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
|
private static final Object globalCryptoLock = new Object();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// Install the Bouncy Castle provider
|
// Install the Bouncy Castle provider
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
@@ -71,7 +74,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
try {
|
try {
|
||||||
FileInputStream fin = new FileInputStream(f);
|
FileInputStream fin = new FileInputStream(f);
|
||||||
byte[] fileData = new byte[(int) f.length()];
|
byte[] fileData = new byte[(int) f.length()];
|
||||||
fin.read(fileData);
|
if (fin.read(fileData) != f.length()) {
|
||||||
|
// Failed to read
|
||||||
|
fileData = null;
|
||||||
|
}
|
||||||
fin.close();
|
fin.close();
|
||||||
return fileData;
|
return fileData;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -150,7 +156,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||||
X500Name name = nameBuilder.build();
|
X500Name name = nameBuilder.build();
|
||||||
|
|
||||||
X509v3CertificateBuilder certBuilder = new 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 {
|
try {
|
||||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
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)
|
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||||
StringWriter strWriter = new StringWriter();
|
StringWriter strWriter = new StringWriter();
|
||||||
PEMWriter pemWriter = new PEMWriter(strWriter);
|
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||||
pemWriter.writeObject(cert);
|
pemWriter.writeObject(cert);
|
||||||
pemWriter.close();
|
pemWriter.close();
|
||||||
|
|
||||||
@@ -208,7 +215,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
public X509Certificate getClientCertificate() {
|
public X509Certificate getClientCertificate() {
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
// the certificate and key at a time
|
// the certificate and key at a time
|
||||||
synchronized (this) {
|
synchronized (globalCryptoLock) {
|
||||||
// Return a loaded cert if we have one
|
// Return a loaded cert if we have one
|
||||||
if (cert != null) {
|
if (cert != null) {
|
||||||
return cert;
|
return cert;
|
||||||
@@ -235,7 +242,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
public RSAPrivateKey getClientPrivateKey() {
|
public RSAPrivateKey getClientPrivateKey() {
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
// the certificate and key at a time
|
// the certificate and key at a time
|
||||||
synchronized (this) {
|
synchronized (globalCryptoLock) {
|
||||||
// Return a loaded key if we have one
|
// Return a loaded key if we have one
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
return key;
|
return key;
|
||||||
@@ -260,7 +267,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getPemEncodedClientCertificate() {
|
public byte[] getPemEncodedClientCertificate() {
|
||||||
synchronized (this) {
|
synchronized (globalCryptoLock) {
|
||||||
// Call our helper function to do the cert loading/generation for us
|
// Call our helper function to do the cert loading/generation for us
|
||||||
getClientCertificate();
|
getClientCertificate();
|
||||||
|
|
||||||
+152
-63
@@ -9,6 +9,7 @@ import android.view.MotionEvent;
|
|||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
import com.limelight.utils.Vector2d;
|
||||||
|
|
||||||
public class ControllerHandler {
|
public class ControllerHandler {
|
||||||
private short inputMap = 0x0000;
|
private short inputMap = 0x0000;
|
||||||
@@ -37,12 +38,31 @@ public class ControllerHandler {
|
|||||||
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
|
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
|
||||||
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
|
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
|
||||||
|
|
||||||
|
private Vector2d inputVector = new Vector2d();
|
||||||
|
private Vector2d normalizedInputVector = new Vector2d();
|
||||||
|
|
||||||
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
|
|
||||||
public ControllerHandler(NvConnection conn) {
|
public ControllerHandler(NvConnection conn) {
|
||||||
this.conn = 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) {
|
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
||||||
@@ -51,10 +71,10 @@ public class ControllerHandler {
|
|||||||
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
|
|
||||||
InputDevice.MotionRange leftTriggerRange = dev.getMotionRange(MotionEvent.AXIS_LTRIGGER);
|
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
||||||
InputDevice.MotionRange rightTriggerRange = dev.getMotionRange(MotionEvent.AXIS_RTRIGGER);
|
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
||||||
InputDevice.MotionRange brakeRange = dev.getMotionRange(MotionEvent.AXIS_BRAKE);
|
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
||||||
InputDevice.MotionRange gasRange = dev.getMotionRange(MotionEvent.AXIS_GAS);
|
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
||||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||||
{
|
{
|
||||||
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||||
@@ -69,8 +89,8 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
|
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||||
if (rxRange != null && ryRange != null) {
|
if (rxRange != null && ryRange != null) {
|
||||||
String devName = dev.getName();
|
String devName = dev.getName();
|
||||||
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
||||||
@@ -82,6 +102,7 @@ public class ControllerHandler {
|
|||||||
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||||
mapping.triggersIdleNegative = true;
|
mapping.triggersIdleNegative = true;
|
||||||
|
mapping.isXboxController = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// DS4 controller uses RX and RY for triggers
|
// DS4 controller uses RX and RY for triggers
|
||||||
@@ -95,8 +116,8 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
||||||
InputDevice.MotionRange zRange = dev.getMotionRange(MotionEvent.AXIS_Z);
|
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
|
||||||
InputDevice.MotionRange rzRange = dev.getMotionRange(MotionEvent.AXIS_RZ);
|
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
|
||||||
|
|
||||||
// Most other controllers use Z and RZ for the right stick
|
// Most other controllers use Z and RZ for the right stick
|
||||||
if (zRange != null && rzRange != null) {
|
if (zRange != null && rzRange != null) {
|
||||||
@@ -104,8 +125,8 @@ public class ControllerHandler {
|
|||||||
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
|
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||||
|
|
||||||
// Try RX and RY now
|
// Try RX and RY now
|
||||||
if (rxRange != null && ryRange != null) {
|
if (rxRange != null && ryRange != null) {
|
||||||
@@ -116,8 +137,8 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Some devices have "hats" for d-pads
|
// Some devices have "hats" for d-pads
|
||||||
InputDevice.MotionRange hatXRange = dev.getMotionRange(MotionEvent.AXIS_HAT_X);
|
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
|
||||||
InputDevice.MotionRange hatYRange = dev.getMotionRange(MotionEvent.AXIS_HAT_Y);
|
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
|
||||||
if (hatXRange != null && hatYRange != null) {
|
if (hatXRange != null && hatYRange != null) {
|
||||||
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||||
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||||
@@ -127,24 +148,54 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
InputDevice.MotionRange lsXRange = dev.getMotionRange(mapping.leftStickXAxis);
|
InputDevice.MotionRange lsXRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis);
|
||||||
InputDevice.MotionRange lsYRange = dev.getMotionRange(mapping.leftStickYAxis);
|
InputDevice.MotionRange lsYRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis);
|
||||||
if (lsXRange != null) {
|
if (lsXRange != null && lsYRange != null) {
|
||||||
mapping.leftStickXAxisDeadzone = lsXRange.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()),
|
||||||
if (lsYRange != null) {
|
Math.abs(lsYRange.getFlat()));
|
||||||
mapping.leftStickYAxisDeadzone = 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) {
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
InputDevice.MotionRange rsXRange = dev.getMotionRange(mapping.rightStickXAxis);
|
InputDevice.MotionRange rsXRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickXAxis);
|
||||||
InputDevice.MotionRange rsYRange = dev.getMotionRange(mapping.rightStickYAxis);
|
InputDevice.MotionRange rsYRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickYAxis);
|
||||||
if (rsXRange != null) {
|
if (rsXRange != null && rsYRange != null) {
|
||||||
mapping.rightStickXAxisDeadzone = rsXRange.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()),
|
||||||
if (rsYRange != null) {
|
Math.abs(rsYRange.getFlat()));
|
||||||
mapping.rightStickYAxisDeadzone = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,9 +228,9 @@ public class ControllerHandler {
|
|||||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int handleRemapping(ControllerMapping mapping, int keyCode) {
|
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
||||||
if (mapping.isDualShock4) {
|
if (mapping.isDualShock4) {
|
||||||
switch (keyCode) {
|
switch (event.getKeyCode()) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||||
|
|
||||||
@@ -218,7 +269,7 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
switch (keyCode) {
|
switch (event.getKeyCode()) {
|
||||||
// These are duplicate dpad events for hat input
|
// These are duplicate dpad events for hat input
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
@@ -228,8 +279,59 @@ public class ControllerHandler {
|
|||||||
return 0;
|
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) {
|
||||||
|
// Reinitialize our cached Vector2d object
|
||||||
|
inputVector.initialize(x, y);
|
||||||
|
|
||||||
|
if (inputVector.getMagnitude() <= deadzoneRadius) {
|
||||||
|
// Deadzone -- return the zero vector
|
||||||
|
return Vector2d.ZERO;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Scale the input based on the distance from the deadzone
|
||||||
|
inputVector.getNormalized(normalizedInputVector);
|
||||||
|
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
|
||||||
|
|
||||||
|
// Bound the X value to -1.0 to 1.0
|
||||||
|
if (normalizedInputVector.getX() > 1.0f) {
|
||||||
|
normalizedInputVector.setX(1.0f);
|
||||||
|
}
|
||||||
|
else if (normalizedInputVector.getX() < -1.0f) {
|
||||||
|
normalizedInputVector.setX(-1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bound the Y value to -1.0 to 1.0
|
||||||
|
if (normalizedInputVector.getY() > 1.0f) {
|
||||||
|
normalizedInputVector.setY(1.0f);
|
||||||
|
}
|
||||||
|
else if (normalizedInputVector.getY() < -1.0f) {
|
||||||
|
normalizedInputVector.setY(-1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedInputVector;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleMotionEvent(MotionEvent event) {
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
@@ -240,30 +342,20 @@ public class ControllerHandler {
|
|||||||
|
|
||||||
// Handle left stick events outside of the deadzone
|
// Handle left stick events outside of the deadzone
|
||||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
float LS_X = event.getAxisValue(mapping.leftStickXAxis);
|
Vector2d leftStickVector = handleDeadZone(event.getAxisValue(mapping.leftStickXAxis),
|
||||||
float LS_Y = event.getAxisValue(mapping.leftStickYAxis);
|
event.getAxisValue(mapping.leftStickYAxis), mapping.leftStickDeadzoneRadius);
|
||||||
if (LS_X >= -mapping.leftStickXAxisDeadzone && LS_X <= mapping.leftStickXAxisDeadzone) {
|
|
||||||
LS_X = 0;
|
leftStickX = (short)(leftStickVector.getX() * 0x7FFE);
|
||||||
}
|
leftStickY = (short)(-leftStickVector.getY() * 0x7FFE);
|
||||||
if (LS_Y >= -mapping.leftStickYAxisDeadzone && LS_Y <= mapping.leftStickYAxisDeadzone) {
|
|
||||||
LS_Y = 0;
|
|
||||||
}
|
|
||||||
leftStickX = (short)Math.round(LS_X * 0x7FFF);
|
|
||||||
leftStickY = (short)Math.round(-LS_Y * 0x7FFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle right stick events outside of the deadzone
|
// Handle right stick events outside of the deadzone
|
||||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
float RS_X = event.getAxisValue(mapping.rightStickXAxis);
|
Vector2d rightStickVector = handleDeadZone(event.getAxisValue(mapping.rightStickXAxis),
|
||||||
float RS_Y = event.getAxisValue(mapping.rightStickYAxis);
|
event.getAxisValue(mapping.rightStickYAxis), mapping.rightStickDeadzoneRadius);
|
||||||
if (RS_X >= -mapping.rightStickXAxisDeadzone && RS_X <= mapping.rightStickXAxisDeadzone) {
|
|
||||||
RS_X = 0;
|
rightStickX = (short)(rightStickVector.getX() * 0x7FFE);
|
||||||
}
|
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
|
||||||
if (RS_Y >= -mapping.rightStickYAxisDeadzone && RS_Y <= mapping.rightStickYAxisDeadzone) {
|
|
||||||
RS_Y = 0;
|
|
||||||
}
|
|
||||||
rightStickX = (short)Math.round(RS_X * 0x7FFF);
|
|
||||||
rightStickY = (short)Math.round(-RS_Y * 0x7FFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle controllers with analog triggers
|
// Handle controllers with analog triggers
|
||||||
@@ -276,8 +368,8 @@ public class ControllerHandler {
|
|||||||
R2 = (R2 + 1) / 2;
|
R2 = (R2 + 1) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
leftTrigger = (byte)Math.round(L2 * 0xFF);
|
leftTrigger = (byte)(L2 * 0xFF);
|
||||||
rightTrigger = (byte)Math.round(R2 * 0xFF);
|
rightTrigger = (byte)(R2 * 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hats emulate d-pad events
|
// Hats emulate d-pad events
|
||||||
@@ -312,7 +404,7 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
keyCode = handleRemapping(mapping, keyCode);
|
keyCode = handleRemapping(mapping, event);
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -326,7 +418,7 @@ public class ControllerHandler {
|
|||||||
// UI thread.
|
// UI thread.
|
||||||
try {
|
try {
|
||||||
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
@@ -403,7 +495,7 @@ public class ControllerHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +513,7 @@ public class ControllerHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +527,7 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
keyCode = handleRemapping(mapping, keyCode);
|
keyCode = handleRemapping(mapping, event);
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -528,16 +620,12 @@ public class ControllerHandler {
|
|||||||
|
|
||||||
class ControllerMapping {
|
class ControllerMapping {
|
||||||
public int leftStickXAxis = -1;
|
public int leftStickXAxis = -1;
|
||||||
public float leftStickXAxisDeadzone;
|
|
||||||
|
|
||||||
public int leftStickYAxis = -1;
|
public int leftStickYAxis = -1;
|
||||||
public float leftStickYAxisDeadzone;
|
public float leftStickDeadzoneRadius;
|
||||||
|
|
||||||
public int rightStickXAxis = -1;
|
public int rightStickXAxis = -1;
|
||||||
public float rightStickXAxisDeadzone;
|
|
||||||
|
|
||||||
public int rightStickYAxis = -1;
|
public int rightStickYAxis = -1;
|
||||||
public float rightStickYAxisDeadzone;
|
public float rightStickDeadzoneRadius;
|
||||||
|
|
||||||
public int leftTriggerAxis = -1;
|
public int leftTriggerAxis = -1;
|
||||||
public int rightTriggerAxis = -1;
|
public int rightTriggerAxis = -1;
|
||||||
@@ -549,5 +637,6 @@ public class ControllerHandler {
|
|||||||
public float hatYDeadzone;
|
public float hatYDeadzone;
|
||||||
|
|
||||||
public boolean isDualShock4;
|
public boolean isDualShock4;
|
||||||
|
public boolean isXboxController;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,93 @@
|
|||||||
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
|
public class TouchContext {
|
||||||
|
private int lastTouchX = 0;
|
||||||
|
private int lastTouchY = 0;
|
||||||
|
private int originalTouchX = 0;
|
||||||
|
private int originalTouchY = 0;
|
||||||
|
private long originalTouchTime = 0;
|
||||||
|
|
||||||
|
private NvConnection conn;
|
||||||
|
private int actionIndex;
|
||||||
|
|
||||||
|
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||||
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
|
|
||||||
|
public TouchContext(NvConnection conn, int actionIndex)
|
||||||
|
{
|
||||||
|
this.conn = conn;
|
||||||
|
this.actionIndex = actionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTap()
|
||||||
|
{
|
||||||
|
int xDelta = Math.abs(lastTouchX - originalTouchX);
|
||||||
|
int yDelta = Math.abs(lastTouchY - originalTouchY);
|
||||||
|
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||||
|
|
||||||
|
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
|
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
|
timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte getMouseButtonIndex()
|
||||||
|
{
|
||||||
|
if (actionIndex == 1) {
|
||||||
|
return MouseButtonPacket.BUTTON_RIGHT;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return MouseButtonPacket.BUTTON_LEFT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean touchDownEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
originalTouchX = lastTouchX = eventX;
|
||||||
|
originalTouchY = lastTouchY = eventY;
|
||||||
|
originalTouchTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (isTap())
|
||||||
|
{
|
||||||
|
byte buttonIndex = getMouseButtonIndex();
|
||||||
|
|
||||||
|
// Lower the mouse button
|
||||||
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
|
|
||||||
|
// We need to sleep a bit here because some games
|
||||||
|
// do input detection by polling
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
|
// Raise the mouse button
|
||||||
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||||
|
{
|
||||||
|
// We only send moves for the primary touch point
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
conn.sendMouseMove((short)(eventX - lastTouchX),
|
||||||
|
(short)(eventY - lastTouchY));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchX = eventX;
|
||||||
|
lastTouchY = eventY;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
public class EvdevEvent {
|
||||||
|
public static final int EVDEV_MIN_EVENT_SIZE = 16;
|
||||||
|
public static final int EVDEV_MAX_EVENT_SIZE = 24;
|
||||||
|
|
||||||
|
/* Event types */
|
||||||
|
public static final short EV_SYN = 0x00;
|
||||||
|
public static final short EV_KEY = 0x01;
|
||||||
|
public static final short EV_REL = 0x02;
|
||||||
|
public static final short EV_MSC = 0x04;
|
||||||
|
|
||||||
|
/* Relative axes */
|
||||||
|
public static final short REL_X = 0x00;
|
||||||
|
public static final short REL_Y = 0x01;
|
||||||
|
public static final short REL_WHEEL = 0x08;
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
public static final short BTN_LEFT = 0x110;
|
||||||
|
public static final short BTN_RIGHT = 0x111;
|
||||||
|
public static final short BTN_MIDDLE = 0x112;
|
||||||
|
public static final short BTN_SIDE = 0x113;
|
||||||
|
public static final short BTN_EXTRA = 0x114;
|
||||||
|
public static final short BTN_FORWARD = 0x115;
|
||||||
|
public static final short BTN_BACK = 0x116;
|
||||||
|
public static final short BTN_TASK = 0x117;
|
||||||
|
public static final short BTN_GAMEPAD = 0x130;
|
||||||
|
|
||||||
|
/* Keys */
|
||||||
|
public static final short KEY_Q = 16;
|
||||||
|
|
||||||
|
public short type;
|
||||||
|
public short code;
|
||||||
|
public int value;
|
||||||
|
|
||||||
|
public EvdevEvent(short type, short code, int value) {
|
||||||
|
this.type = type;
|
||||||
|
this.code = code;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class EvdevHandler {
|
||||||
|
|
||||||
|
private String absolutePath;
|
||||||
|
private EvdevListener listener;
|
||||||
|
private boolean shutdown = false;
|
||||||
|
private int fd = -1;
|
||||||
|
|
||||||
|
private Thread handlerThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// All the finally blocks here make this code look like a mess
|
||||||
|
// but it's important that we get this right to avoid causing
|
||||||
|
// system-wide input problems.
|
||||||
|
|
||||||
|
// Open the /dev/input/eventX file
|
||||||
|
fd = EvdevReader.open(absolutePath);
|
||||||
|
if (fd == -1) {
|
||||||
|
LimeLog.warning("Unable to open "+absolutePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if it's a mouse or keyboard, but not a gamepad
|
||||||
|
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
|
||||||
|
EvdevReader.isGamepad(fd)) {
|
||||||
|
// We only handle keyboards and mice
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab it for ourselves
|
||||||
|
if (!EvdevReader.grab(fd)) {
|
||||||
|
LimeLog.warning("Unable to grab "+absolutePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
||||||
|
|
||||||
|
try {
|
||||||
|
int deltaX = 0;
|
||||||
|
int deltaY = 0;
|
||||||
|
byte deltaScroll = 0;
|
||||||
|
|
||||||
|
while (!isInterrupted() && !shutdown) {
|
||||||
|
EvdevEvent event = EvdevReader.read(fd, buffer);
|
||||||
|
if (event == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type)
|
||||||
|
{
|
||||||
|
case EvdevEvent.EV_SYN:
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
listener.mouseMove(deltaX, deltaY);
|
||||||
|
deltaX = deltaY = 0;
|
||||||
|
}
|
||||||
|
if (deltaScroll != 0) {
|
||||||
|
listener.mouseScroll(deltaScroll);
|
||||||
|
deltaScroll = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_REL:
|
||||||
|
switch (event.code)
|
||||||
|
{
|
||||||
|
case EvdevEvent.REL_X:
|
||||||
|
deltaX = event.value;
|
||||||
|
break;
|
||||||
|
case EvdevEvent.REL_Y:
|
||||||
|
deltaY = event.value;
|
||||||
|
break;
|
||||||
|
case EvdevEvent.REL_WHEEL:
|
||||||
|
deltaScroll = (byte) event.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_KEY:
|
||||||
|
switch (event.code)
|
||||||
|
{
|
||||||
|
case EvdevEvent.BTN_LEFT:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
case EvdevEvent.BTN_MIDDLE:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
case EvdevEvent.BTN_RIGHT:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.BTN_SIDE:
|
||||||
|
case EvdevEvent.BTN_EXTRA:
|
||||||
|
case EvdevEvent.BTN_FORWARD:
|
||||||
|
case EvdevEvent.BTN_BACK:
|
||||||
|
case EvdevEvent.BTN_TASK:
|
||||||
|
// Other unhandled mouse buttons
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// We got some unrecognized button. This means
|
||||||
|
// someone is trying to use the other device in this
|
||||||
|
// "combination" input device. We'll try to handle
|
||||||
|
// it via keyboard, but we're not going to disconnect
|
||||||
|
// if we can't
|
||||||
|
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
||||||
|
if (keyCode != 0) {
|
||||||
|
listener.keyboardEvent(event.value != 0, keyCode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_MSC:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Release our grab
|
||||||
|
EvdevReader.ungrab(fd);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Close the file
|
||||||
|
EvdevReader.close(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EvdevHandler(String absolutePath, EvdevListener listener) {
|
||||||
|
this.absolutePath = absolutePath;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
handlerThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
// Close the fd. It doesn't matter if this races
|
||||||
|
// with the handler thread. We'll close this out from
|
||||||
|
// under the thread to wake it up
|
||||||
|
if (fd != -1) {
|
||||||
|
EvdevReader.close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown = true;
|
||||||
|
handlerThread.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
handlerThread.join();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyDeleted() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
public interface EvdevListener {
|
||||||
|
public static final int BUTTON_LEFT = 1;
|
||||||
|
public static final int BUTTON_MIDDLE = 2;
|
||||||
|
public static final int BUTTON_RIGHT = 3;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public class EvdevReader {
|
||||||
|
static {
|
||||||
|
System.loadLibrary("evdev_reader");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requires root to chmod /dev/input/eventX
|
||||||
|
public static boolean setPermissions(String[] files, int octalPermissions) {
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("su");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Process p = builder.start();
|
||||||
|
|
||||||
|
OutputStream stdin = p.getOutputStream();
|
||||||
|
for (String file : files) {
|
||||||
|
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) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the fd to be passed to other function or -1 on error
|
||||||
|
public static native int open(String fileName);
|
||||||
|
|
||||||
|
// Prevent other apps (including Android itself) from using the device while "grabbed"
|
||||||
|
public static native boolean grab(int fd);
|
||||||
|
public static native boolean ungrab(int fd);
|
||||||
|
|
||||||
|
// Used for checking device capabilities
|
||||||
|
public static native boolean hasRelAxis(int fd, short axis);
|
||||||
|
public static native boolean hasAbsAxis(int fd, short axis);
|
||||||
|
public static native boolean hasKey(int fd, short key);
|
||||||
|
|
||||||
|
public static boolean isMouse(int fd) {
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
return hasRelAxis(fd, EvdevEvent.REL_X) &&
|
||||||
|
hasRelAxis(fd, EvdevEvent.REL_Y) &&
|
||||||
|
hasKey(fd, EvdevEvent.BTN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAlphaKeyboard(int fd) {
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
return hasKey(fd, EvdevEvent.KEY_Q);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGamepad(int fd) {
|
||||||
|
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the bytes read or -1 on error
|
||||||
|
private static native int read(int fd, byte[] buffer);
|
||||||
|
|
||||||
|
// Takes a byte buffer to use to read the output into.
|
||||||
|
// This buffer MUST be in native byte order and at least
|
||||||
|
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||||
|
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
||||||
|
int bytesRead = read(fd, buffer.array());
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
LimeLog.warning("Failed to read: "+bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||||
|
LimeLog.warning("Short read: "+bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.limit(bytesRead);
|
||||||
|
buffer.rewind();
|
||||||
|
|
||||||
|
// Throw away the time stamp
|
||||||
|
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||||
|
buffer.getLong();
|
||||||
|
buffer.getLong();
|
||||||
|
} else {
|
||||||
|
buffer.getInt();
|
||||||
|
buffer.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the fd from open()
|
||||||
|
public static native int close(int fd);
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
public class EvdevTranslator {
|
||||||
|
|
||||||
|
public static final short EVDEV_KEY_CODES[] = {
|
||||||
|
0, //KeyEvent.VK_RESERVED
|
||||||
|
KeyEvent.KEYCODE_ESCAPE,
|
||||||
|
KeyEvent.KEYCODE_1,
|
||||||
|
KeyEvent.KEYCODE_2,
|
||||||
|
KeyEvent.KEYCODE_3,
|
||||||
|
KeyEvent.KEYCODE_4,
|
||||||
|
KeyEvent.KEYCODE_5,
|
||||||
|
KeyEvent.KEYCODE_6,
|
||||||
|
KeyEvent.KEYCODE_7,
|
||||||
|
KeyEvent.KEYCODE_8,
|
||||||
|
KeyEvent.KEYCODE_9,
|
||||||
|
KeyEvent.KEYCODE_0,
|
||||||
|
KeyEvent.KEYCODE_MINUS,
|
||||||
|
KeyEvent.KEYCODE_EQUALS,
|
||||||
|
KeyEvent.KEYCODE_DEL,
|
||||||
|
KeyEvent.KEYCODE_TAB,
|
||||||
|
KeyEvent.KEYCODE_Q,
|
||||||
|
KeyEvent.KEYCODE_W,
|
||||||
|
KeyEvent.KEYCODE_E,
|
||||||
|
KeyEvent.KEYCODE_R,
|
||||||
|
KeyEvent.KEYCODE_T,
|
||||||
|
KeyEvent.KEYCODE_Y,
|
||||||
|
KeyEvent.KEYCODE_U,
|
||||||
|
KeyEvent.KEYCODE_I,
|
||||||
|
KeyEvent.KEYCODE_O,
|
||||||
|
KeyEvent.KEYCODE_P,
|
||||||
|
KeyEvent.KEYCODE_LEFT_BRACKET,
|
||||||
|
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
||||||
|
KeyEvent.KEYCODE_ENTER,
|
||||||
|
KeyEvent.KEYCODE_CTRL_LEFT,
|
||||||
|
KeyEvent.KEYCODE_A,
|
||||||
|
KeyEvent.KEYCODE_S,
|
||||||
|
KeyEvent.KEYCODE_D,
|
||||||
|
KeyEvent.KEYCODE_F,
|
||||||
|
KeyEvent.KEYCODE_G,
|
||||||
|
KeyEvent.KEYCODE_H,
|
||||||
|
KeyEvent.KEYCODE_J,
|
||||||
|
KeyEvent.KEYCODE_K,
|
||||||
|
KeyEvent.KEYCODE_L,
|
||||||
|
KeyEvent.KEYCODE_SEMICOLON,
|
||||||
|
KeyEvent.KEYCODE_APOSTROPHE,
|
||||||
|
KeyEvent.KEYCODE_GRAVE,
|
||||||
|
KeyEvent.KEYCODE_SHIFT_LEFT,
|
||||||
|
KeyEvent.KEYCODE_BACKSLASH,
|
||||||
|
KeyEvent.KEYCODE_Z,
|
||||||
|
KeyEvent.KEYCODE_X,
|
||||||
|
KeyEvent.KEYCODE_C,
|
||||||
|
KeyEvent.KEYCODE_V,
|
||||||
|
KeyEvent.KEYCODE_B,
|
||||||
|
KeyEvent.KEYCODE_N,
|
||||||
|
KeyEvent.KEYCODE_M,
|
||||||
|
KeyEvent.KEYCODE_COMMA,
|
||||||
|
KeyEvent.KEYCODE_PERIOD,
|
||||||
|
KeyEvent.KEYCODE_SLASH,
|
||||||
|
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
||||||
|
KeyEvent.KEYCODE_ALT_LEFT,
|
||||||
|
KeyEvent.KEYCODE_SPACE,
|
||||||
|
KeyEvent.KEYCODE_CAPS_LOCK,
|
||||||
|
KeyEvent.KEYCODE_F1,
|
||||||
|
KeyEvent.KEYCODE_F2,
|
||||||
|
KeyEvent.KEYCODE_F3,
|
||||||
|
KeyEvent.KEYCODE_F4,
|
||||||
|
KeyEvent.KEYCODE_F5,
|
||||||
|
KeyEvent.KEYCODE_F6,
|
||||||
|
KeyEvent.KEYCODE_F7,
|
||||||
|
KeyEvent.KEYCODE_F8,
|
||||||
|
KeyEvent.KEYCODE_F9,
|
||||||
|
KeyEvent.KEYCODE_F10,
|
||||||
|
KeyEvent.KEYCODE_NUM_LOCK,
|
||||||
|
KeyEvent.KEYCODE_SCROLL_LOCK,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_7,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_8,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_9,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_4,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_5,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_6,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ADD,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_1,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_2,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_3,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_0,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_DOT,
|
||||||
|
0,
|
||||||
|
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
||||||
|
0, //KeyEvent.VK_102ND,
|
||||||
|
KeyEvent.KEYCODE_F11,
|
||||||
|
KeyEvent.KEYCODE_F12,
|
||||||
|
0, //KeyEvent.VK_RO,
|
||||||
|
0, //KeyEvent.VK_KATAKANA,
|
||||||
|
0, //KeyEvent.VK_HIRAGANA,
|
||||||
|
0, //KeyEvent.VK_HENKAN,
|
||||||
|
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
||||||
|
0, //KeyEvent.VK_MUHENKAN,
|
||||||
|
0, //KeyEvent.VK_KPJPCOMMA,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
||||||
|
KeyEvent.KEYCODE_CTRL_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
||||||
|
KeyEvent.KEYCODE_SYSRQ,
|
||||||
|
KeyEvent.KEYCODE_ALT_RIGHT,
|
||||||
|
0, //KeyEvent.VK_LINEFEED,
|
||||||
|
KeyEvent.KEYCODE_HOME,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
|
KeyEvent.KEYCODE_PAGE_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_MOVE_END,
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||||
|
KeyEvent.KEYCODE_INSERT,
|
||||||
|
KeyEvent.KEYCODE_FORWARD_DEL,
|
||||||
|
0, //KeyEvent.VK_MACRO,
|
||||||
|
0, //KeyEvent.VK_MUTE,
|
||||||
|
0, //KeyEvent.VK_VOLUMEDOWN,
|
||||||
|
0, //KeyEvent.VK_VOLUMEUP,
|
||||||
|
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
||||||
|
0, //KeyEvent.VK_KPPLUSMINUS,
|
||||||
|
KeyEvent.KEYCODE_BREAK,
|
||||||
|
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
||||||
|
};
|
||||||
|
|
||||||
|
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
||||||
|
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
||||||
|
return EVDEV_KEY_CODES[evdevKeyCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
import android.os.FileObserver;
|
||||||
|
|
||||||
|
@SuppressWarnings("ALL")
|
||||||
|
public class EvdevWatcher {
|
||||||
|
private static final String PATH = "/dev/input";
|
||||||
|
private static final String REQUIRED_FILE_PREFIX = "event";
|
||||||
|
|
||||||
|
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
||||||
|
private boolean shutdown = false;
|
||||||
|
private boolean init = false;
|
||||||
|
private boolean ungrabbed = false;
|
||||||
|
private EvdevListener listener;
|
||||||
|
private Thread startThread;
|
||||||
|
|
||||||
|
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
|
||||||
|
@Override
|
||||||
|
public void onEvent(int event, String fileName) {
|
||||||
|
if (fileName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (handlers) {
|
||||||
|
if (shutdown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event & FileObserver.CREATE) != 0) {
|
||||||
|
LimeLog.info("Starting evdev handler for "+fileName);
|
||||||
|
|
||||||
|
if (!init) {
|
||||||
|
// If this a real new device, update permissions again so we can read it
|
||||||
|
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
|
||||||
|
}
|
||||||
|
|
||||||
|
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
|
||||||
|
|
||||||
|
// If we're ungrabbed now, don't start the handler
|
||||||
|
if (!ungrabbed) {
|
||||||
|
handler.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers.put(fileName, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event & FileObserver.DELETE) != 0) {
|
||||||
|
LimeLog.info("Halting evdev handler for "+fileName);
|
||||||
|
|
||||||
|
EvdevHandler handler = handlers.remove(fileName);
|
||||||
|
if (handler != null) {
|
||||||
|
handler.notifyDeleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EvdevWatcher(EvdevListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File[] rundownWithPermissionsChange(int newPermissions) {
|
||||||
|
// Rundown existing files
|
||||||
|
File devInputDir = new File(PATH);
|
||||||
|
File[] files = devInputDir.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
return new File[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set desired permissions
|
||||||
|
String[] filePaths = new String[files.length];
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
filePaths[i] = files[i].getAbsolutePath();
|
||||||
|
}
|
||||||
|
EvdevReader.setPermissions(filePaths, newPermissions);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ungrabAll() {
|
||||||
|
synchronized (handlers) {
|
||||||
|
// Note that we're ungrabbed for now
|
||||||
|
ungrabbed = true;
|
||||||
|
|
||||||
|
// Stop all handlers
|
||||||
|
for (EvdevHandler handler : handlers.values()) {
|
||||||
|
handler.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void regrabAll() {
|
||||||
|
synchronized (handlers) {
|
||||||
|
// We're regrabbing everything now
|
||||||
|
ungrabbed = false;
|
||||||
|
|
||||||
|
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
|
||||||
|
// We need to recreate each entry since we can't reuse a stopped one
|
||||||
|
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
|
||||||
|
entry.getValue().start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
startThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// List all files and allow us access
|
||||||
|
File[] files = rundownWithPermissionsChange(0666);
|
||||||
|
|
||||||
|
init = true;
|
||||||
|
for (File f : files) {
|
||||||
|
observer.onEvent(FileObserver.CREATE, f.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done with initial onEvent calls
|
||||||
|
init = false;
|
||||||
|
|
||||||
|
// Start watching for new files
|
||||||
|
observer.startWatching();
|
||||||
|
|
||||||
|
synchronized (startThread) {
|
||||||
|
// Wait to be awoken again by shutdown()
|
||||||
|
try {
|
||||||
|
startThread.wait();
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Giveup eventX permissions
|
||||||
|
rundownWithPermissionsChange(066);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
startThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
// Let start thread cleanup on it's own sweet time
|
||||||
|
synchronized (startThread) {
|
||||||
|
startThread.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the observer
|
||||||
|
observer.stopWatching();
|
||||||
|
|
||||||
|
synchronized (handlers) {
|
||||||
|
// Stop creating new handlers
|
||||||
|
shutdown = true;
|
||||||
|
|
||||||
|
// If we've already ungrabbed, there's nothing else to do
|
||||||
|
if (ungrabbed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop all handlers
|
||||||
|
for (EvdevHandler handler : handlers.values()) {
|
||||||
|
handler.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.VideoDepacketizer;
|
||||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||||
|
|
||||||
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||||
|
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
@@ -38,6 +39,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
private int findOptimalPerformanceLevel() {
|
private int findOptimalPerformanceLevel() {
|
||||||
StringBuilder cpuInfo = new StringBuilder();
|
StringBuilder cpuInfo = new StringBuilder();
|
||||||
BufferedReader br = null;
|
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) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.targetFps = redrawRate;
|
this.targetFps = redrawRate;
|
||||||
|
|
||||||
int perfLevel = findOptimalPerformanceLevel();
|
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
|
||||||
int threadCount;
|
int threadCount;
|
||||||
|
|
||||||
int avcFlags = 0;
|
int avcFlags = 0;
|
||||||
+16
-3
@@ -16,15 +16,28 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
|
if (decoderRenderer == null) {
|
||||||
|
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||||
|
}
|
||||||
|
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeWithFlags(int drFlags) {
|
||||||
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||||
MediaCodecDecoderRenderer.findSafeDecoder() != null)) {
|
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
||||||
decoderRenderer = new MediaCodecDecoderRenderer();
|
decoderRenderer = new MediaCodecDecoderRenderer();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
decoderRenderer = new AndroidCpuDecoderRenderer();
|
decoderRenderer = new AndroidCpuDecoderRenderer();
|
||||||
}
|
}
|
||||||
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
}
|
||||||
|
|
||||||
|
public boolean isHardwareAccelerated() {
|
||||||
|
if (decoderRenderer == null) {
|
||||||
|
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||||
|
}
|
||||||
|
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-1
@@ -4,6 +4,7 @@ import java.net.InetAddress;
|
|||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
@@ -44,7 +45,7 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
private void initializeDb() {
|
private void initializeDb() {
|
||||||
// Create tables if they aren't already there
|
// 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)",
|
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||||
COMPUTER_TABLE_NAME,
|
COMPUTER_TABLE_NAME,
|
||||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||||
@@ -109,6 +110,8 @@ public class ComputerDatabaseManager {
|
|||||||
computerList.add(details);
|
computerList.add(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.close();
|
||||||
|
|
||||||
return computerList;
|
return computerList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +120,7 @@ public class ComputerDatabaseManager {
|
|||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
if (!c.moveToFirst()) {
|
if (!c.moveToFirst()) {
|
||||||
// No matching computer
|
// No matching computer
|
||||||
|
c.close();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +150,8 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
details.macAddress = c.getString(4);
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
|
c.close();
|
||||||
|
|
||||||
// If a field is corrupt or missing, delete the database entry
|
// If a field is corrupt or missing, delete the database entry
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
details.macAddress == null) {
|
details.macAddress == null) {
|
||||||
File renamed without changes.
+146
-184
@@ -1,17 +1,14 @@
|
|||||||
package com.limelight.computers;
|
package com.limelight.computers;
|
||||||
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InterfaceAddress;
|
import java.util.HashMap;
|
||||||
import java.net.NetworkInterface;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
@@ -23,29 +20,27 @@ import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
|||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.net.NetworkInfo;
|
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
public class ComputerManagerService extends Service {
|
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 POLLING_PERIOD_MS = 5000;
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
|
|
||||||
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
private ComputerDatabaseManager dbManager;
|
private ComputerDatabaseManager dbManager;
|
||||||
|
private AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||||
|
|
||||||
private IdentityManager idManager;
|
private IdentityManager idManager;
|
||||||
private ThreadPoolExecutor pollingPool;
|
private HashMap<ComputerDetails, Thread> pollingThreads;
|
||||||
private Timer pollingTimer;
|
|
||||||
private ComputerManagerListener listener = null;
|
private ComputerManagerListener listener = null;
|
||||||
|
private AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
|
|
||||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
private ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
synchronized (discoveryServiceConnection) {
|
synchronized (discoveryServiceConnection) {
|
||||||
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
|
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
|
||||||
@@ -64,6 +59,79 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 class ComputerManagerBinder extends Binder {
|
||||||
public void startPolling(ComputerManagerListener listener) {
|
public void startPolling(ComputerManagerListener listener) {
|
||||||
// Set the listener
|
// Set the listener
|
||||||
@@ -73,8 +141,22 @@ public class ComputerManagerService extends Service {
|
|||||||
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||||
|
|
||||||
// Start polling known machines
|
// Start polling known machines
|
||||||
pollingTimer = new Timer();
|
if (!getLocalDatabaseReference()) {
|
||||||
pollingTimer.schedule(getTimerTask(), 0, POLLING_PERIOD_MS);
|
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() {
|
public void waitForReady() {
|
||||||
@@ -84,11 +166,19 @@ public class ComputerManagerService extends Service {
|
|||||||
// Wait for the bind notification
|
// Wait for the bind notification
|
||||||
discoveryServiceConnection.wait(1000);
|
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) {
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||||
}
|
}
|
||||||
@@ -117,10 +207,12 @@ public class ComputerManagerService extends Service {
|
|||||||
discoveryBinder.stopDiscovery();
|
discoveryBinder.stopDiscovery();
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
if (pollingTimer != null) {
|
synchronized (pollingThreads) {
|
||||||
pollingTimer.cancel();
|
for (Thread t : pollingThreads.values()) {
|
||||||
pollingTimer = null;
|
t.interrupt();
|
||||||
}
|
}
|
||||||
|
pollingThreads.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the listener
|
// Remove the listener
|
||||||
listener = null;
|
listener = null;
|
||||||
@@ -155,8 +247,15 @@ public class ComputerManagerService extends Service {
|
|||||||
fakeDetails.localIp = addr;
|
fakeDetails.localIp = addr;
|
||||||
fakeDetails.remoteIp = addr;
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
// Put it in the thread pool to process later
|
// Spawn a thread for this computer
|
||||||
pollingPool.execute(getPollingRunnable(fakeDetails));
|
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) {
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
@@ -166,116 +265,38 @@ public class ComputerManagerService extends Service {
|
|||||||
fakeDetails.remoteIp = addr;
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
getPollingRunnable(fakeDetails).run();
|
runPoll(fakeDetails);
|
||||||
|
|
||||||
// If the machine is reachable, it was successful
|
// If the machine is reachable, it was successful
|
||||||
return fakeDetails.state == ComputerDetails.State.ONLINE;
|
return fakeDetails.state == ComputerDetails.State.ONLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(String name) {
|
||||||
|
if (!getLocalDatabaseReference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove it from the database
|
// Remove it from the database
|
||||||
dbManager.deleteComputer(name);
|
dbManager.deleteComputer(name);
|
||||||
|
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TimerTask getTimerTask() {
|
private boolean getLocalDatabaseReference() {
|
||||||
return new TimerTask() {
|
if (dbRefCount.get() == 0) {
|
||||||
@Override
|
return false;
|
||||||
public void run() {
|
|
||||||
List<ComputerDetails> computerList = dbManager.getAllComputers();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbRefCount.incrementAndGet();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void releaseLocalDatabaseReference() {
|
||||||
|
if (dbRefCount.decrementAndGet() == 0) {
|
||||||
|
dbManager.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ComputerDetails tryPollIp(InetAddress ipAddr) {
|
private ComputerDetails tryPollIp(InetAddress ipAddr) {
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||||
@@ -297,7 +318,7 @@ public class ComputerManagerService extends Service {
|
|||||||
polledDetails = tryPollIp(details.remoteIp);
|
polledDetails = tryPollIp(details.remoteIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (polledDetails == null) {
|
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||||
// Failed, so let's try the fallback
|
// Failed, so let's try the fallback
|
||||||
if (!localFirst) {
|
if (!localFirst) {
|
||||||
polledDetails = tryPollIp(details.localIp);
|
polledDetails = tryPollIp(details.localIp);
|
||||||
@@ -311,7 +332,8 @@ public class ComputerManagerService extends Service {
|
|||||||
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||||
ComputerDetails.Reachability.REMOTE;
|
ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else if (polledDetails != null) {
|
||||||
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||||
ComputerDetails.Reachability.REMOTE;
|
ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
@@ -327,61 +349,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean doPollMachine(ComputerDetails details) {
|
private boolean doPollMachine(ComputerDetails details) {
|
||||||
// Get the network type
|
return pollComputer(details, true);
|
||||||
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);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dbManager.updateComputer(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update anyone listening
|
|
||||||
if (listener != null) {
|
|
||||||
listener.notifyComputerUpdated(details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -390,16 +358,14 @@ public class ComputerManagerService extends Service {
|
|||||||
bindService(new Intent(this, DiscoveryService.class),
|
bindService(new Intent(this, DiscoveryService.class),
|
||||||
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
// Create the thread pool for updating computer state
|
pollingThreads = new HashMap<ComputerDetails, Thread>();
|
||||||
pollingPool = new ThreadPoolExecutor(MAX_CONCURRENT_REQUESTS, MAX_CONCURRENT_REQUESTS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>(),
|
|
||||||
new ThreadPoolExecutor.DiscardPolicy());
|
|
||||||
|
|
||||||
// Lookup or generate this device's UID
|
// Lookup or generate this device's UID
|
||||||
idManager = new IdentityManager(this);
|
idManager = new IdentityManager(this);
|
||||||
|
|
||||||
// Initialize the DB
|
// Initialize the DB
|
||||||
dbManager = new ComputerDatabaseManager(this);
|
dbManager = new ComputerDatabaseManager(this);
|
||||||
|
dbRefCount.set(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -409,14 +375,10 @@ public class ComputerManagerService extends Service {
|
|||||||
unbindService(discoveryServiceConnection);
|
unbindService(discoveryServiceConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the thread pool
|
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
|
||||||
pollingPool.shutdownNow();
|
|
||||||
try {
|
|
||||||
pollingPool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
|
|
||||||
} catch (InterruptedException e) {}
|
|
||||||
|
|
||||||
// Close the DB
|
// Remove the initial DB reference
|
||||||
dbManager.close();
|
releaseLocalDatabaseReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
+4
-3
@@ -4,6 +4,7 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
@@ -53,7 +54,7 @@ public class IdentityManager {
|
|||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
try {
|
try {
|
||||||
reader.close();
|
reader.close();
|
||||||
} catch (IOException e) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +62,7 @@ public class IdentityManager {
|
|||||||
private static String generateNewUniqueId(Context c) {
|
private static String generateNewUniqueId(Context c) {
|
||||||
// Generate a new UID hex string
|
// Generate a new UID hex string
|
||||||
LimeLog.info("Generating new UID");
|
LimeLog.info("Generating new UID");
|
||||||
String uidStr = String.format("%016x", new Random().nextLong());
|
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
||||||
|
|
||||||
OutputStreamWriter writer = null;
|
OutputStreamWriter writer = null;
|
||||||
try {
|
try {
|
||||||
@@ -75,7 +76,7 @@ public class IdentityManager {
|
|||||||
if (writer != null) {
|
if (writer != null) {
|
||||||
try {
|
try {
|
||||||
writer.close();
|
writer.close();
|
||||||
} catch (IOException e) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
+3
-2
@@ -1,10 +1,11 @@
|
|||||||
package com.limelight;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.R;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -97,7 +98,7 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
addThread.join();
|
addThread.join();
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
addThread = null;
|
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 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)
|
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() {
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.remove(this);
|
rundownDialogs.remove(Dialog.this);
|
||||||
alert.dismiss();
|
alert.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
+16
-11
@@ -1,6 +1,7 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
@@ -13,7 +14,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
private ProgressDialog progress;
|
private ProgressDialog progress;
|
||||||
private boolean finish;
|
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)
|
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||||
{
|
{
|
||||||
@@ -31,16 +32,19 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
return spinner;
|
return spinner;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs(Activity activity)
|
||||||
{
|
{
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
for (SpinnerDialog d : rundownDialogs) {
|
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
|
||||||
if (d.progress.isShowing()) {
|
while (i.hasNext()) {
|
||||||
d.progress.dismiss();
|
SpinnerDialog dialog = i.next();
|
||||||
|
if (dialog.activity == activity) {
|
||||||
|
i.remove();
|
||||||
|
if (dialog.progress.isShowing()) {
|
||||||
|
dialog.progress.dismiss();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rundownDialogs.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,12 +67,13 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
|
// If we're dying, don't bother doing anything
|
||||||
|
if (activity.isFinishing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (progress == null)
|
if (progress == null)
|
||||||
{
|
{
|
||||||
// If we're dying, don't bother creating a dialog
|
|
||||||
if (activity.isFinishing())
|
|
||||||
return;
|
|
||||||
|
|
||||||
progress = new ProgressDialog(activity);
|
progress = new ProgressDialog(activity);
|
||||||
|
|
||||||
progress.setTitle(title);
|
progress.setTitle(title);
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,13 @@
|
|||||||
|
# Android.mk for Limelight's Evdev Reader
|
||||||
|
MY_LOCAL_PATH := $(call my-dir)
|
||||||
|
|
||||||
|
include $(call all-subdir-makefiles)
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 575 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user