Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69c7b5a0d5 | |||
| d1ad3115fa | |||
| 770af402a4 | |||
| 3236c0b93a | |||
| 51aacc3f38 | |||
| 397c6f46f9 | |||
| d00f78f859 | |||
| 29fec2e0de | |||
| 88d28665ef | |||
| de1f4da258 | |||
| 7985be57ab | |||
| a835e7aaa2 | |||
| c4dc5eb9e1 | |||
| db758f386e | |||
| 3fb3eefa94 | |||
| 2d6c756e70 | |||
| 03e965d449 | |||
| 34f72544d8 | |||
| d839ea9781 | |||
| 2b7f13fdbb | |||
| 7557a3a4ae | |||
| fcecba484f | |||
| fa85a0a0bd | |||
| dc64bfeba2 | |||
| 871b73c48d | |||
| 5dcff91d27 | |||
| 0041fc1dab | |||
| 314242ab08 | |||
| 09e8ddfd74 | |||
| 4cea483a87 | |||
| 444c4602c1 | |||
| 5b6eac7140 | |||
| 7cdd184197 | |||
| be153b84cb | |||
| 06c53e2251 | |||
| 695519bdf5 | |||
| bf7d033ab2 | |||
| df67795c4a | |||
| 72c1696f43 | |||
| 8eca3683c9 | |||
| 80c17b4913 | |||
| e5050f10bb | |||
| e912e4de57 | |||
| 8dee1f0d80 | |||
| 53594ada66 | |||
| 848ed1ad72 | |||
| 307e807c8f | |||
| 6a27780d56 | |||
| 57f98dbb4a | |||
| 5af7d83ec1 | |||
| 4a6f77f43a | |||
| c96f9fb635 | |||
| e3a477a243 | |||
| 9fcd641143 | |||
| 6d1cbc5a64 | |||
| ec71060d98 | |||
| 03f706fb85 | |||
| 7ad87bd3ee | |||
| 4e088f6183 | |||
| 1b16ea6f53 | |||
| f262503bc8 | |||
| b2ba216cd1 | |||
| 94ba7f8e45 | |||
| a267cf59c7 | |||
| 79e8bef289 | |||
| 99e3b5f33b | |||
| afbe64f3ff | |||
| 43b1a73ae0 | |||
| d08eeb8a2d | |||
| 7c39e5c974 | |||
| cd49334199 | |||
| dd59f0bc6d | |||
| cf2d83a1ea | |||
| d5b6130936 | |||
| 4ae29b0075 | |||
| 34e35cd493 | |||
| a17af070c5 | |||
| fbe0a26800 | |||
| 25ad99df94 | |||
| 6338e7b8eb | |||
| 1b9846d519 | |||
| a4ece13a1d | |||
| 066b8430a0 | |||
| 2b54a91f3d | |||
| 2d01633372 | |||
| 5dc01069fc | |||
| d450008833 | |||
| a37fff6eb5 | |||
| 6604675bf9 | |||
| 1965cc2347 | |||
| 312ca27906 | |||
| 0bceadbd9a | |||
| dfc3daabcd | |||
| b9ba9adc1f | |||
| f112d45e1a | |||
| 88f139873c | |||
| d317c5bf03 | |||
| 9d72314b9c | |||
| 2cc7243573 | |||
| 269d9a6bc6 | |||
| 244130fc1b | |||
| a67791b8aa | |||
| 21e46a5c3b | |||
| 2df2f850d5 | |||
| 406d26ec1c | |||
| 68c1aaf433 | |||
| 9ef577dbdd | |||
| 982ecbc015 | |||
| 7e44b5abd5 | |||
| 6dbb1a0c1f | |||
| 94b1c04fa6 | |||
| 9758276f1c | |||
| 971263c52d | |||
| 9b58e7bb4d | |||
| 69ecf0251d | |||
| 350a4d8825 | |||
| 44f447df7b | |||
| 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 | |||
| 562569dc6b | |||
| a18aa51f5a | |||
| 8efe194682 | |||
| f07e927103 | |||
| a50211ab95 | |||
| 247a19766c | |||
| 731e4dc31e | |||
| cd4bf9a28b | |||
| 196c0e6cbc | |||
| e2cb7c953c | |||
| 426b3c8522 | |||
| 9648cf257f | |||
| 31d8687f67 | |||
| 991407a2cf | |||
| 1b026f1354 | |||
| 9d4ca6293f | |||
| 2296b80edb | |||
| 5577d48dcf | |||
| e92a281fd8 | |||
| b4c3f9678a | |||
| 82f79c466a | |||
| d428f316b4 | |||
| 828f4877b6 | |||
| 09e8e8e6b3 | |||
| 77c8051ec6 | |||
| 6bae056e3a | |||
| bb869a51fd | |||
| 25b3d08bb9 |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<classpath>
|
|
||||||
<classpathentry kind="src" path="src"/>
|
|
||||||
<classpathentry kind="src" path="gen"/>
|
|
||||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
|
||||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
|
||||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
|
||||||
<classpathentry kind="output" path="bin/classes"/>
|
|
||||||
</classpath>
|
|
||||||
+35
-1
@@ -1 +1,35 @@
|
|||||||
/bin/*
|
#built application files
|
||||||
|
*.apk
|
||||||
|
*.ap_
|
||||||
|
|
||||||
|
# files for the dex VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Windows thumbnail db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# OSX files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Eclipse project files
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
|
||||||
|
# Android Studio
|
||||||
|
.idea
|
||||||
|
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Compiled JNI libraries folder
|
||||||
|
**/jniLibs
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
*** SESSION Sep 21, 2013 18:55:11.17 -------------------------------------------
|
|
||||||
*** SESSION Sep 21, 2013 18:55:55.08 -------------------------------------------
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
-3
@@ -1,3 +0,0 @@
|
|||||||
com.android.ide.eclipse.adt.fixLegacyEditors=1
|
|
||||||
com.android.ide.eclipse.adt.sdk=C\:\\Users\\Andrew\\Desktop\\ADT\\adt-bundle-windows-x86_64-20130917\\sdk
|
|
||||||
eclipse.preferences.version=1
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
eclipse.preferences.version=1
|
|
||||||
spelling_locale_initialized=true
|
|
||||||
useAnnotationsPrefPage=true
|
|
||||||
useQuickDiffPrefPage=true
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
eclipse.preferences.version=1
|
|
||||||
version=1
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
content_assist_proposals_background=255,255,255
|
|
||||||
content_assist_proposals_foreground=0,0,0
|
|
||||||
eclipse.preferences.version=1
|
|
||||||
fontPropagated=true
|
|
||||||
org.eclipse.jdt.ui.editor.tab.width=
|
|
||||||
org.eclipse.jdt.ui.formatterprofiles.version=12
|
|
||||||
org.eclipse.jdt.ui.javadoclocations.migrated=true
|
|
||||||
org.eclipse.jface.textfont=1|Courier New|10.0|0|WINDOWS|1|0|0|0|0|0|0|0|0|1|0|0|0|0|Courier New;
|
|
||||||
proposalOrderMigrated=true
|
|
||||||
spelling_locale_initialized=true
|
|
||||||
tabWidthPropagated=true
|
|
||||||
useAnnotationsPrefPage=true
|
|
||||||
useQuickDiffPrefPage=true
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
PROBLEMS_FILTERS_MIGRATE=true
|
|
||||||
eclipse.preferences.version=1
|
|
||||||
platformState=1379804095671
|
|
||||||
quickStart=false
|
|
||||||
tipsAndTricks=true
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
eclipse.preferences.version=1
|
|
||||||
showIntro=false
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<typeInfoHistroy/>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<qualifiedTypeNameHistroy/>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<section name="Workbench">
|
|
||||||
<section name="org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart">
|
|
||||||
<item value="true" key="group_libraries"/>
|
|
||||||
<item value="false" key="linkWithEditor"/>
|
|
||||||
<item value="2" key="layout"/>
|
|
||||||
<item value="1" key="rootMode"/>
|
|
||||||
<item value="<?xml version="1.0" encoding="UTF-8"?>
<packageExplorer group_libraries="1" layout="2" linkWithEditor="0" rootMode="1" workingSetName="">
<customFilters userDefinedPatternsEnabled="false">
<xmlDefinedFilters>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.StaticsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.FieldsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter" isEnabled="true"/>
</xmlDefinedFilters>
</customFilters>
</packageExplorer>" key="memento"/>
|
|
||||||
</section>
|
|
||||||
<section name="JavaElementSearchActions">
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<section name="Workbench">
|
|
||||||
<section name="ChooseWorkspaceDialogSettings">
|
|
||||||
<item value="185" key="DIALOG_Y_ORIGIN"/>
|
|
||||||
<item value="381" key="DIALOG_X_ORIGIN"/>
|
|
||||||
</section>
|
|
||||||
<section name="WORKBENCH_SETTINGS">
|
|
||||||
<list key="ENABLED_TRANSFERS">
|
|
||||||
</list>
|
|
||||||
</section>
|
|
||||||
<section name="ExternalProjectImportWizard">
|
|
||||||
<item value="false" key="WizardProjectsImportPage.STORE_ARCHIVE_SELECTED"/>
|
|
||||||
<item value="false" key="WizardProjectsImportPage.STORE_COPY_PROJECT_ID"/>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<section name="Workbench">
|
|
||||||
<section name="org.eclipse.ui.internal.QuickAccess">
|
|
||||||
<item value="1025" key="dialogWidth"/>
|
|
||||||
<item value="525" key="dialogHeight"/>
|
|
||||||
<list key="orderedProviders">
|
|
||||||
</list>
|
|
||||||
<list key="textArray">
|
|
||||||
</list>
|
|
||||||
<list key="orderedElements">
|
|
||||||
</list>
|
|
||||||
<list key="textEntries">
|
|
||||||
</list>
|
|
||||||
</section>
|
|
||||||
<section name="ImportExportAction">
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<workingSetManager>
|
|
||||||
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory" id="1379804109849_0" label="Window Working Set" name="Aggregate for window 1379804109848"/>
|
|
||||||
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory" id="1379804153983_1" label="Window Working Set" name="Aggregate for window 1379804153983"/>
|
|
||||||
</workingSetManager>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
org.eclipse.core.runtime=1
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<projectDescription>
|
|
||||||
<name>Limelight</name>
|
|
||||||
<comment></comment>
|
|
||||||
<projects>
|
|
||||||
</projects>
|
|
||||||
<buildSpec>
|
|
||||||
<buildCommand>
|
|
||||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
<buildCommand>
|
|
||||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
<buildCommand>
|
|
||||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
</buildSpec>
|
|
||||||
<natures>
|
|
||||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
|
||||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
|
||||||
</natures>
|
|
||||||
</projectDescription>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
eclipse.preferences.version=1
|
|
||||||
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
|
||||||
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
|
|
||||||
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
|
||||||
org.eclipse.jdt.core.compiler.compliance=1.6
|
|
||||||
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
|
||||||
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
|
||||||
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
|
||||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
|
||||||
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
|
||||||
org.eclipse.jdt.core.compiler.source=1.6
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
#Limelight
|
#Limelight
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
<?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="gson-2.3.1" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="okhttp-2.1.0" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="okio-1.0.1" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="androidasync-e1dfb4" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="ion-2f46fa" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import com.android.builder.model.ProductFlavor
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 21
|
||||||
|
buildToolsVersion "21.1.1"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 16
|
||||||
|
targetSdkVersion 21
|
||||||
|
|
||||||
|
versionName "3.0.2"
|
||||||
|
versionCode = 48
|
||||||
|
}
|
||||||
|
|
||||||
|
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: '+'
|
||||||
|
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+'
|
||||||
|
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+'
|
||||||
|
compile group: 'com.google.android', name: 'support-v4', version:'+'
|
||||||
|
|
||||||
|
// FIXME: Pending resolution of issue #346 using custom build
|
||||||
|
//compile group: 'com.koushikdutta.ion', name: 'ion', version:'+'
|
||||||
|
compile group: 'com.google.code.gson', name: 'gson', version:'+'
|
||||||
|
compile files('libs/androidasync-e1dfb4.jar')
|
||||||
|
compile files('libs/ion-2f46fa.jar')
|
||||||
|
|
||||||
|
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+'
|
||||||
|
compile group: 'com.squareup.okio', name:'okio', version:'+'
|
||||||
|
compile files('libs/jmdns-fixed.jar')
|
||||||
|
compile files('libs/limelight-common.jar')
|
||||||
|
compile files('libs/tinyrtsp.jar')
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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="36"
|
|
||||||
android:versionName="2.5.7" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="16"
|
|
||||||
android:targetSdkVersion="21" />
|
|
||||||
|
|
||||||
<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" />
|
||||||
@@ -21,14 +15,12 @@
|
|||||||
<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 -->
|
<!-- 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" />
|
||||||
@@ -38,11 +30,10 @@
|
|||||||
|
|
||||||
<!-- Launcher for Android TV devices -->
|
<!-- Launcher for Android TV devices -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.PcViewTv"
|
android:name=".PcViewTv"
|
||||||
android:logo="@drawable/atv_banner"
|
android:logo="@drawable/atv_banner"
|
||||||
android:icon="@drawable/atv_banner"
|
android:icon="@drawable/atv_banner"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
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.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
@@ -50,48 +41,40 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.limelight.AppView"
|
android:name=".AppView"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||||
android:label="App List" >
|
|
||||||
<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:theme="@style/StreamTheme"
|
||||||
android:theme="@style/FullscreenTheme" >
|
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.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>
|
||||||
|
|
||||||
@@ -9,11 +9,13 @@ import java.util.List;
|
|||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.grid.AppGridAdapter;
|
||||||
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.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -25,18 +27,17 @@ import android.view.View;
|
|||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.GridView;
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
public class AppView extends Activity {
|
public class AppView extends Activity {
|
||||||
private ListView appList;
|
private AppGridAdapter appGridAdapter;
|
||||||
private ArrayAdapter<AppObject> appListAdapter;
|
|
||||||
private InetAddress ipAddress;
|
private InetAddress ipAddress;
|
||||||
private String uniqueId;
|
private String uniqueId;
|
||||||
private boolean remote;
|
private boolean remote;
|
||||||
|
private boolean firstLoad = true;
|
||||||
|
|
||||||
private final static int RESUME_ID = 1;
|
private final static int RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
@@ -51,15 +52,18 @@ public class AppView extends Activity {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_app_view);
|
setContentView(R.layout.activity_app_view);
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
|
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
|
||||||
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
|
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
|
||||||
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
|
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
|
||||||
if (address == null || uniqueId == null) {
|
if (address == null || uniqueId == null) {
|
||||||
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
|
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
|
||||||
TextView label = (TextView) findViewById(R.id.appListText);
|
TextView label = (TextView) findViewById(R.id.appListText);
|
||||||
setTitle(labelText);
|
setTitle(labelText);
|
||||||
label.setText(labelText);
|
label.setText(labelText);
|
||||||
@@ -67,34 +71,39 @@ public class AppView extends Activity {
|
|||||||
try {
|
try {
|
||||||
ipAddress = InetAddress.getByAddress(address);
|
ipAddress = InetAddress.getByAddress(address);
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
return;
|
e.printStackTrace();
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
appList = (ListView)findViewById(R.id.pcListView);
|
GridView appGrid = (GridView) findViewById(R.id.appGridView);
|
||||||
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
|
try {
|
||||||
appListAdapter.setNotifyOnChange(false);
|
appGridAdapter = new AppGridAdapter(this, ipAddress, uniqueId);
|
||||||
appList.setAdapter(appListAdapter);
|
} catch (Exception e) {
|
||||||
appList.setItemsCanFocus(true);
|
e.printStackTrace();
|
||||||
appList.setOnItemClickListener(new OnItemClickListener() {
|
finish();
|
||||||
@Override
|
return;
|
||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
}
|
||||||
long id) {
|
appGrid.setAdapter(appGridAdapter);
|
||||||
AppObject app = (AppObject) appListAdapter.getItem(pos);
|
appGrid.setOnItemClickListener(new OnItemClickListener() {
|
||||||
if (app == null || app.app == null) {
|
@Override
|
||||||
return;
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
}
|
long id) {
|
||||||
|
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||||
// Only open the context menu if something is running, otherwise start it
|
if (app == null || app.app == null) {
|
||||||
if (getRunningAppId() != -1) {
|
return;
|
||||||
openContextMenu(arg1);
|
}
|
||||||
}
|
|
||||||
else {
|
// Only open the context menu if something is running, otherwise start it
|
||||||
doStart(app.app);
|
if (getRunningAppId() != -1) {
|
||||||
}
|
openContextMenu(arg1);
|
||||||
}
|
} else {
|
||||||
});
|
doStart(app.app);
|
||||||
registerForContextMenu(appList);
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(appGrid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -108,14 +117,18 @@ public class AppView extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
updateAppList();
|
// Display the error message if it was the
|
||||||
|
// first load, but just kill the activity
|
||||||
|
// on subsequent errors
|
||||||
|
updateAppList(firstLoad);
|
||||||
|
firstLoad = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getRunningAppId() {
|
private int getRunningAppId() {
|
||||||
int runningAppId = -1;
|
int runningAppId = -1;
|
||||||
for (int i = 0; i < appListAdapter.getCount(); i++) {
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
AppObject app = appListAdapter.getItem(i);
|
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||||
if (app.app == null) {
|
if (app.app == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -129,11 +142,11 @@ public class AppView extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
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 = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
if (selectedApp == null || selectedApp.app == null) {
|
if (selectedApp == null || selectedApp.app == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -141,12 +154,12 @@ public class AppView extends Activity {
|
|||||||
int runningAppId = getRunningAppId();
|
int runningAppId = getRunningAppId();
|
||||||
if (runningAppId != -1) {
|
if (runningAppId != -1) {
|
||||||
if (runningAppId == selectedApp.app.getAppId()) {
|
if (runningAppId == selectedApp.app.getAppId()) {
|
||||||
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
|
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
|
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||||
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
|
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,41 +171,28 @@ 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 = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId()) {
|
||||||
{
|
case RESUME_ID:
|
||||||
case RESUME_ID:
|
// Resume is the same as start for us
|
||||||
// Resume is the same as start for us
|
doStart(app.app);
|
||||||
doStart(app.app);
|
return true;
|
||||||
return true;
|
|
||||||
|
case QUIT_ID:
|
||||||
case QUIT_ID:
|
doQuit(app.app);
|
||||||
doQuit(app.app);
|
return true;
|
||||||
return true;
|
|
||||||
|
case CANCEL_ID:
|
||||||
case CANCEL_ID:
|
return true;
|
||||||
return true;
|
|
||||||
|
default:
|
||||||
default:
|
return super.onContextItemSelected(item);
|
||||||
return super.onContextItemSelected(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String generateString(NvApp app) {
|
|
||||||
StringBuilder str = new StringBuilder();
|
|
||||||
str.append(app.getAppName());
|
|
||||||
if (app.getIsRunning()) {
|
|
||||||
str.append(" - Running");
|
|
||||||
}
|
|
||||||
return str.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addListPlaceholder() {
|
private void updateAppList(final boolean displayError) {
|
||||||
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
|
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
|
||||||
}
|
getResources().getString(R.string.applist_refresh_msg), true);
|
||||||
|
|
||||||
private void updateAppList() {
|
|
||||||
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
|
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -204,30 +204,37 @@ public class AppView extends Activity {
|
|||||||
AppView.this.runOnUiThread(new Runnable() {
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
appListAdapter.clear();
|
appGridAdapter.clear();
|
||||||
if (appList.isEmpty()) {
|
for (NvApp app : appList) {
|
||||||
addListPlaceholder();
|
appGridAdapter.addApp(new AppObject(app));
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
for (NvApp app : appList) {
|
appGridAdapter.notifyDataSetChanged();
|
||||||
appListAdapter.add(new AppObject(generateString(app), app));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appListAdapter.notifyDataSetChanged();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
|
if (displayError) {
|
||||||
|
Dialog.displayDialog(AppView.this, getResources().getString(R.string.applist_refresh_error_title),
|
||||||
|
getResources().getString(R.string.applist_refresh_error_msg), true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Just finish the activity immediately
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
@@ -242,7 +249,7 @@ public class AppView extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doQuit(final NvApp app) {
|
private void doQuit(final NvApp app) {
|
||||||
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -251,17 +258,16 @@ public class AppView extends Activity {
|
|||||||
try {
|
try {
|
||||||
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
if (httpConn.quitApp()) {
|
if (httpConn.quitApp()) {
|
||||||
message = "Successfully quit "+app.getAppName();
|
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message = "Failed to quit "+app.getAppName();
|
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
|
||||||
}
|
}
|
||||||
updateAppList();
|
updateAppList(true);
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
message = "Failed to resolve host";
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
message = getResources().getString(R.string.error_404);
|
||||||
+ "Try rebooting your machine or reinstalling GFE.";
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
@@ -278,17 +284,15 @@ public class AppView extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class AppObject {
|
public class AppObject {
|
||||||
public String text;
|
|
||||||
public NvApp app;
|
public NvApp app;
|
||||||
|
|
||||||
public AppObject(String text, NvApp app) {
|
public AppObject(NvApp app) {
|
||||||
this.text = text;
|
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return text;
|
return app.getAppName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
|
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
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;
|
||||||
@@ -14,13 +15,13 @@ 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.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;
|
||||||
@@ -57,8 +58,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
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;
|
||||||
@@ -67,9 +67,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
|
||||||
private boolean stretchToFit;
|
|
||||||
private boolean toastsDisabled;
|
|
||||||
|
|
||||||
private EvdevWatcher evdevWatcher;
|
private EvdevWatcher evdevWatcher;
|
||||||
private int modifierFlags = 0;
|
private int modifierFlags = 0;
|
||||||
private boolean grabbedInput = true;
|
private boolean grabbedInput = true;
|
||||||
@@ -86,35 +83,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
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 String DISABLE_TOASTS_PREF_STRING = "NoToasts";
|
|
||||||
|
|
||||||
public static final int BITRATE_DEFAULT_720_30 = 5;
|
|
||||||
public static final int BITRATE_DEFAULT_720_60 = 10;
|
|
||||||
public static final int BITRATE_DEFAULT_1080_30 = 10;
|
|
||||||
public static final int BITRATE_DEFAULT_1080_60 = 30;
|
|
||||||
|
|
||||||
public static final int DEFAULT_WIDTH = 1280;
|
|
||||||
public static final int DEFAULT_HEIGHT = 720;
|
|
||||||
public static final int DEFAULT_REFRESH_RATE = 60;
|
|
||||||
public static final int DEFAULT_DECODER = 0;
|
|
||||||
public static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
|
||||||
public static final boolean DEFAULT_STRETCH = false;
|
|
||||||
public static final boolean DEFAULT_SOPS = true;
|
|
||||||
public static final boolean DEFAULT_DISABLE_TOASTS = false;
|
|
||||||
|
|
||||||
public static final int FORCE_HARDWARE_DECODER = -1;
|
|
||||||
public static final int AUTOSELECT_DECODER = 0;
|
|
||||||
public static final int FORCE_SOFTWARE_DECODER = 1;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -146,36 +114,27 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Inflate the content
|
// Inflate the content
|
||||||
setContentView(R.layout.activity_game);
|
setContentView(R.layout.activity_game);
|
||||||
|
|
||||||
// Start the spinner
|
// Start the spinner
|
||||||
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
|
spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
|
||||||
|
getResources().getString(R.string.conn_establishing_msg), 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);
|
|
||||||
toastsDisabled = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
|
|
||||||
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
display.getSize(screenSize);
|
display.getSize(screenSize);
|
||||||
@@ -186,41 +145,51 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
sv.setOnTouchListener(this);
|
sv.setOnTouchListener(this);
|
||||||
|
|
||||||
// Warn the user if they're on a metered connection
|
// Warn the user if they're on a metered connection
|
||||||
checkDataConnection();
|
checkDataConnection();
|
||||||
|
|
||||||
// Make sure Wi-Fi is fully powered up
|
// Make sure Wi-Fi is fully powered up
|
||||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||||
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
||||||
wifiLock.setReferenceCounted(false);
|
wifiLock.setReferenceCounted(false);
|
||||||
wifiLock.acquire();
|
wifiLock.acquire();
|
||||||
|
|
||||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||||
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);
|
||||||
|
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||||
|
|
||||||
decoderRenderer = new ConfigurableDecoderRenderer();
|
decoderRenderer = new ConfigurableDecoderRenderer();
|
||||||
decoderRenderer.initializeWithFlags(drFlags);
|
decoderRenderer.initializeWithFlags(drFlags);
|
||||||
|
|
||||||
StreamConfiguration config =
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
new StreamConfiguration(app, width, height,
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
refreshRate, bitrate * 1000, sops,
|
.setRefreshRate(prefConfig.fps)
|
||||||
(decoderRenderer.getCapabilities() &
|
.setApp(app)
|
||||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0);
|
.setBitrate(prefConfig.bitrate * 1000)
|
||||||
|
.setEnableSops(prefConfig.enableSops)
|
||||||
|
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
||||||
|
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
|
||||||
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
|
.setMaxPacketSize(remote ? 1024 : 1292)
|
||||||
|
.setRemote(remote)
|
||||||
|
.build();
|
||||||
|
|
||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
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, prefConfig.deadzonePercentage);
|
||||||
|
|
||||||
SurfaceHolder sh = sv.getHolder();
|
SurfaceHolder sh = sv.getHolder();
|
||||||
if (stretchToFit || !decoderRenderer.isHardwareAccelerated()) {
|
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
|
||||||
// Set the surface to the size of the video
|
// Set the surface to the size of the video
|
||||||
sh.setFixedSize(width, height);
|
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize touch contexts
|
// Initialize touch contexts
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
touchContextMap[i] = new TouchContext(conn, i);
|
touchContextMap[i] = new TouchContext(conn, i,
|
||||||
|
((double)prefConfig.width / (double)screenSize.x),
|
||||||
|
((double)prefConfig.height / (double)screenSize.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LimelightBuildProps.ROOT_BUILD) {
|
if (LimelightBuildProps.ROOT_BUILD) {
|
||||||
@@ -252,7 +221,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
{
|
{
|
||||||
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
if (mgr.isActiveNetworkMetered()) {
|
if (mgr.isActiveNetworkMetered()) {
|
||||||
displayTransientMessage("Warning: Your active network connection is metered!");
|
displayTransientMessage(getResources().getString(R.string.conn_metered));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,13 +269,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||||
String message = null;
|
String message = null;
|
||||||
if (averageEndToEndLat > 0) {
|
if (averageEndToEndLat > 0) {
|
||||||
message = "Average client-side frame latency: "+averageEndToEndLat+" ms";
|
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
||||||
if (averageDecoderLat > 0) {
|
if (averageDecoderLat > 0) {
|
||||||
message += " (hardware decoder latency: "+averageDecoderLat+" ms)";
|
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (averageDecoderLat > 0) {
|
else if (averageDecoderLat > 0) {
|
||||||
message = "Average hardware decoder latency: "+averageDecoderLat+" ms";
|
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
@@ -421,18 +390,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
InputDevice dev = event.getDevice();
|
|
||||||
if (dev == null) {
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass-through virtual navigation keys
|
// Pass-through virtual navigation keys
|
||||||
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try the controller handler first
|
// Try the controller handler first
|
||||||
boolean handled = controllerHandler.handleButtonDown(keyCode, event);
|
boolean handled = controllerHandler.handleButtonDown(event);
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
// Try the keyboard handler
|
// Try the keyboard handler
|
||||||
short translated = keybTranslator.translate(event.getKeyCode());
|
short translated = keybTranslator.translate(event.getKeyCode());
|
||||||
@@ -464,18 +428,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
InputDevice dev = event.getDevice();
|
|
||||||
if (dev == null) {
|
|
||||||
return super.onKeyUp(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass-through virtual navigation keys
|
// Pass-through virtual navigation keys
|
||||||
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try the controller handler first
|
// Try the controller handler first
|
||||||
boolean handled = controllerHandler.handleButtonUp(keyCode, event);
|
boolean handled = controllerHandler.handleButtonUp(event);
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
// Try the keyboard handler
|
// Try the keyboard handler
|
||||||
short translated = keybTranslator.translate(event.getKeyCode());
|
short translated = keybTranslator.translate(event.getKeyCode());
|
||||||
@@ -554,9 +513,28 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
// ACTION_MOVE is special because it always has actionIndex == 0
|
// ACTION_MOVE is special because it always has actionIndex == 0
|
||||||
// We'll call the move handlers for all indexes manually
|
// We'll call the move handlers for all indexes manually
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
|
||||||
touchContextMap[i].touchMoveEvent(eventX, eventY);
|
// First process the historical events
|
||||||
}
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
for (TouchContext aTouchContextMap : touchContextMap) {
|
||||||
|
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
|
||||||
|
{
|
||||||
|
aTouchContextMap.touchMoveEvent(
|
||||||
|
(int)event.getHistoricalX(aTouchContextMap.getActionIndex(), i),
|
||||||
|
(int)event.getHistoricalY(aTouchContextMap.getActionIndex(), i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process the current values
|
||||||
|
for (TouchContext aTouchContextMap : touchContextMap) {
|
||||||
|
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
|
||||||
|
{
|
||||||
|
aTouchContextMap.touchMoveEvent(
|
||||||
|
(int)event.getX(aTouchContextMap.getActionIndex()),
|
||||||
|
(int)event.getY(aTouchContextMap.getActionIndex()));
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -600,9 +578,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
// First process the history
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
||||||
|
}
|
||||||
|
|
||||||
lastButtonState = event.getButtonState();
|
// Now process the current values
|
||||||
|
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||||
|
|
||||||
|
lastButtonState = event.getButtonState();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -620,21 +604,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
if (!handleMotionEvent(event)) {
|
return handleMotionEvent(event) || super.onTouchEvent(event);
|
||||||
return super.onTouchEvent(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||||
if (!handleMotionEvent(event)) {
|
return handleMotionEvent(event) || super.onGenericMotionEvent(event);
|
||||||
return super.onGenericMotionEvent(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -648,8 +626,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
@@ -673,7 +651,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
@Override
|
@Override
|
||||||
public void stageStarting(Stage stage) {
|
public void stageStarting(Stage stage) {
|
||||||
if (spinner != null) {
|
if (spinner != null) {
|
||||||
spinner.setMessage("Starting "+stage.getName());
|
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,7 +682,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
stopConnection();
|
stopConnection();
|
||||||
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||||
|
getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,7 +694,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
||||||
stopConnection();
|
stopConnection();
|
||||||
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
||||||
|
getResources().getString(R.string.conn_terminated_msg), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,7 +724,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void displayTransientMessage(final String message) {
|
public void displayTransientMessage(final String message) {
|
||||||
if (!toastsDisabled) {
|
if (!prefConfig.disableWarnings) {
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -765,8 +745,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// 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 && decoderRenderer.isHardwareAccelerated()) {
|
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,
|
||||||
@@ -819,6 +800,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
conn.sendMouseScroll(amount);
|
conn.sendMouseScroll(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void keyboardEvent(boolean buttonDown, short keyCode) {
|
public void keyboardEvent(boolean buttonDown, short keyCode) {
|
||||||
short keyMap = keybTranslator.translate(keyCode);
|
short keyMap = keybTranslator.translate(keyCode);
|
||||||
if (keyMap != 0) {
|
if (keyMap != 0) {
|
||||||
@@ -9,12 +9,16 @@ import com.limelight.binding.PlatformBinding;
|
|||||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.grid.PcGridAdapter;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
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 com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
@@ -24,6 +28,7 @@ import android.content.ServiceConnection;
|
|||||||
import android.content.res.Configuration;
|
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;
|
||||||
@@ -32,16 +37,15 @@ import android.view.ContextMenu.ContextMenuInfo;
|
|||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.GridView;
|
||||||
import android.widget.Button;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ListView;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
public class PcView extends Activity {
|
public class PcView extends Activity {
|
||||||
private Button settingsButton, addComputerButton;
|
private RelativeLayout noPcFoundLayout;
|
||||||
private ListView pcList;
|
private PcGridAdapter pcGridAdapter;
|
||||||
private ArrayAdapter<ComputerObject> pcListAdapter;
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling;
|
private boolean freezeUpdates, runningPolling;
|
||||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
@@ -88,58 +92,60 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
// Set default preferences if we've never been run
|
||||||
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
settingsButton = (Button)findViewById(R.id.settingsButton);
|
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
||||||
addComputerButton = (Button)findViewById(R.id.manuallyAddPc);
|
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
||||||
|
|
||||||
pcList = (ListView)findViewById(R.id.pcListView);
|
GridView pcGrid = (GridView) findViewById(R.id.pcGridView);
|
||||||
pcList.setAdapter(pcListAdapter);
|
pcGrid.setAdapter(pcGridAdapter);
|
||||||
pcList.setItemsCanFocus(true);
|
pcGrid.setOnItemClickListener(new OnItemClickListener() {
|
||||||
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) pcGridAdapter.getItem(pos);
|
||||||
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(pos);
|
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
if (computer.details == null) {
|
// Do nothing
|
||||||
// Placeholder item; no context menu for it
|
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
return;
|
// Open the context menu if a PC is offline
|
||||||
}
|
openContextMenu(arg1);
|
||||||
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
} else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
// Open the context menu if a PC is offline
|
// Pair an unpaired machine by default
|
||||||
openContextMenu(arg1);
|
doPair(computer.details);
|
||||||
}
|
} else {
|
||||||
else if (computer.details.pairState != PairState.PAIRED) {
|
doAppList(computer.details);
|
||||||
// Pair an unpaired machine by default
|
}
|
||||||
doPair(computer.details);
|
}
|
||||||
}
|
});
|
||||||
else {
|
registerForContextMenu(pcGrid);
|
||||||
doAppList(computer.details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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) {
|
||||||
Intent i = new Intent(PcView.this, AddComputerManually.class);
|
Intent i = new Intent(PcView.this, AddComputerManually.class);
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pcListAdapter.isEmpty()) {
|
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
|
||||||
addListPlaceholder();
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
}
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
else {
|
}
|
||||||
pcListAdapter.notifyDataSetChanged();
|
else {
|
||||||
}
|
noPcFoundLayout.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -148,9 +154,8 @@ public class PcView extends Activity {
|
|||||||
// Bind to the computer manager service
|
// Bind to the computer manager service
|
||||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||||
Service.BIND_AUTO_CREATE);
|
Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
|
pcGridAdapter = new PcGridAdapter(this);
|
||||||
pcListAdapter.setNotifyOnChange(false);
|
|
||||||
|
|
||||||
initializeViews();
|
initializeViews();
|
||||||
}
|
}
|
||||||
@@ -169,7 +174,7 @@ public class PcView extends Activity {
|
|||||||
PcView.this.runOnUiThread(new Runnable() {
|
PcView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
updateListView(details);
|
updateComputer(details);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -228,33 +233,32 @@ 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(false);
|
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 = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
if (computer == null || computer.details == null) {
|
if (computer == null || computer.details == null ||
|
||||||
|
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inflate the context menu
|
// Inflate the context menu
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
|
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
else if (computer.details.pairState != PairState.PAIRED) {
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
|
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
|
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
|
||||||
menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
|
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,26 +269,25 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
private void doPair(final ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (computer.runningGameId != 0) {
|
if (computer.runningGameId != 0) {
|
||||||
Toast.makeText(PcView.this, "Computer is currently in a game. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show();
|
||||||
"You must close the game before pairing.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show();
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
// Stop updates and wait while pairing
|
// Stop updates and wait while pairing
|
||||||
stopComputerUpdates(true);
|
stopComputerUpdates(true);
|
||||||
@@ -302,23 +305,28 @@ public class PcView extends Activity {
|
|||||||
PlatformBinding.getDeviceName(),
|
PlatformBinding.getDeviceName(),
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
message = "Already paired";
|
// Don't display any toast, but open the app list
|
||||||
|
message = null;
|
||||||
|
success = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
final String pinStr = PairingManager.generatePinString();
|
final String pinStr = PairingManager.generatePinString();
|
||||||
|
|
||||||
// Spin the dialog off in a thread because it blocks
|
// Spin the dialog off in a thread because it blocks
|
||||||
Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||||
|
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||||
|
|
||||||
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||||
message = "Incorrect PIN";
|
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||||
}
|
}
|
||||||
else if (pairState == PairingManager.PairState.FAILED) {
|
else if (pairState == PairingManager.PairState.FAILED) {
|
||||||
message = "Pairing failed";
|
message = getResources().getString(R.string.pair_fail);
|
||||||
}
|
}
|
||||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||||
message = "Paired successfully";
|
// Just navigate to the app view without displaying a toast
|
||||||
|
message = null;
|
||||||
|
success = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should be no other values
|
// Should be no other values
|
||||||
@@ -326,21 +334,29 @@ public class PcView extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
message = "Failed to resolve host";
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
message = getResources().getString(R.string.error_404);
|
||||||
+ "Try rebooting your machine or reinstalling GFE.";
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
final String toastMessage = message;
|
final String toastMessage = message;
|
||||||
|
final boolean toastSuccess = success;
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
if (toastMessage != null) {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastSuccess) {
|
||||||
|
// Open the app list after a successful pairing attemp
|
||||||
|
doAppList(computer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -352,26 +368,25 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
private void doWakeOnLan(final ComputerDetails computer) {
|
private void doWakeOnLan(final ComputerDetails computer) {
|
||||||
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
|
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computer.macAddress == null) {
|
if (computer.macAddress == null) {
|
||||||
Toast.makeText(PcView.this, "Unable to wake PC because GFE didn't send a MAC address", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
WakeOnLanSender.sendWolPacket(computer);
|
WakeOnLanSender.sendWolPacket(computer);
|
||||||
message = "It may take a few seconds for your PC to wake up. " +
|
message = getResources().getString(R.string.wol_waking_msg);
|
||||||
"If it doesn't, make sure it's configured properly for Wake-On-LAN.";
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
message = "Failed to send Wake-On-LAN packets";
|
message = getResources().getString(R.string.wol_fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String toastMessage = message;
|
final String toastMessage = message;
|
||||||
@@ -387,16 +402,15 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
private void doUnpair(final ComputerDetails computer) {
|
private void doUnpair(final ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show();
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -418,20 +432,19 @@ public class PcView extends Activity {
|
|||||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
httpConn.unpair();
|
httpConn.unpair();
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
|
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
|
||||||
message = "Unpaired successfully";
|
message = getResources().getString(R.string.unpair_success);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message = "Failed to unpair";
|
message = getResources().getString(R.string.unpair_fail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message = "Device was not paired";
|
message = getResources().getString(R.string.unpair_error);
|
||||||
}
|
}
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
message = "Failed to resolve host";
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
message = getResources().getString(R.string.error_404);
|
||||||
+ "Try rebooting your machine or reinstalling GFE.";
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
@@ -449,12 +462,11 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
private void doAppList(ComputerDetails computer) {
|
private void doAppList(ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +488,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 = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId())
|
||||||
{
|
{
|
||||||
case PAIR_ID:
|
case PAIR_ID:
|
||||||
@@ -493,12 +505,11 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
case DELETE_ID:
|
case DELETE_ID:
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
managerBinder.removeComputer(computer.details.name);
|
managerBinder.removeComputer(computer.details.name);
|
||||||
removeListView(computer.details);
|
removeComputer(computer.details);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case APP_LIST_ID:
|
case APP_LIST_ID:
|
||||||
@@ -509,71 +520,24 @@ public class PcView extends Activity {
|
|||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String generateString(ComputerDetails details) {
|
|
||||||
StringBuilder str = new StringBuilder();
|
|
||||||
str.append(details.name).append(" - ");
|
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
|
||||||
str.append("Online ");
|
|
||||||
if (details.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
str.append("(Local) - ");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
str.append("(Remote) - ");
|
|
||||||
}
|
|
||||||
if (details.pairState == PairState.PAIRED) {
|
|
||||||
if (details.runningGameId == 0) {
|
|
||||||
str.append("Available");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
str.append("In Game");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
str.append("Not Paired");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
str.append("Offline");
|
|
||||||
}
|
|
||||||
return str.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addListPlaceholder() {
|
private void removeComputer(ComputerDetails details) {
|
||||||
pcListAdapter.add(new ComputerObject("Discovery is running. No computers found yet. " +
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
"If your PC doesn't show up in about 15 seconds, " +
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
"make sure your computer is running GFE or add your PC manually using the button above.", null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeListView(ComputerDetails details) {
|
|
||||||
for (int i = 0; i < pcListAdapter.getCount(); i++) {
|
|
||||||
ComputerObject computer = pcListAdapter.getItem(i);
|
|
||||||
|
|
||||||
if (details.equals(computer.details)) {
|
if (details.equals(computer.details)) {
|
||||||
pcListAdapter.remove(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pcListAdapter.getCount() == 0) {
|
|
||||||
// Add the placeholder if we're down to 0 computers
|
|
||||||
addListPlaceholder();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateListView(ComputerDetails details) {
|
private void updateComputer(ComputerDetails details) {
|
||||||
String computerString = generateString(details);
|
|
||||||
ComputerObject existingEntry = null;
|
ComputerObject existingEntry = null;
|
||||||
boolean placeholderPresent = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < pcListAdapter.getCount(); i++) {
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
ComputerObject computer = pcListAdapter.getItem(i);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
|
|
||||||
// If there's a placeholder, there's nothing else
|
|
||||||
if (computer.details == null) {
|
|
||||||
placeholderPresent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is the same computer
|
// Check if this is the same computer
|
||||||
if (details.equals(computer.details)) {
|
if (details.equals(computer.details)) {
|
||||||
@@ -584,35 +548,30 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
// Replace the information in the existing entry
|
// Replace the information in the existing entry
|
||||||
existingEntry.text = computerString;
|
|
||||||
existingEntry.details = details;
|
existingEntry.details = details;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If the placeholder is the only object, remove it
|
|
||||||
if (placeholderPresent) {
|
|
||||||
pcListAdapter.remove(pcListAdapter.getItem(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new entry
|
// Add a new entry
|
||||||
pcListAdapter.add(new ComputerObject(computerString, details));
|
pcGridAdapter.addComputer(new ComputerObject(details));
|
||||||
|
|
||||||
|
// Remove the "Discovery in progress" view
|
||||||
|
noPcFoundLayout.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the view that the data has changed
|
// Notify the view that the data has changed
|
||||||
pcListAdapter.notifyDataSetChanged();
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ComputerObject {
|
public class ComputerObject {
|
||||||
public String text;
|
|
||||||
public ComputerDetails details;
|
public ComputerDetails details;
|
||||||
|
|
||||||
public ComputerObject(String text, ComputerDetails details) {
|
public ComputerObject(ComputerDetails details) {
|
||||||
this.text = text;
|
|
||||||
this.details = details;
|
this.details = details;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return text;
|
return details.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.media.AudioTrack;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
|
||||||
|
public class AndroidAudioRenderer implements AudioRenderer {
|
||||||
|
|
||||||
|
public static final int FRAME_SIZE = 960;
|
||||||
|
|
||||||
|
private AudioTrack track;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean streamInitialized(int channelCount, int sampleRate) {
|
||||||
|
int channelConfig;
|
||||||
|
int bufferSize;
|
||||||
|
|
||||||
|
switch (channelCount)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're not supposed to request less than the minimum
|
||||||
|
// buffer size for our buffer, but it appears that we can
|
||||||
|
// do this on many devices and it lowers audio latency.
|
||||||
|
// We'll try the small buffer size first and if it fails,
|
||||||
|
// use the recommended larger buffer size.
|
||||||
|
try {
|
||||||
|
// Buffer two frames of audio if possible
|
||||||
|
bufferSize = FRAME_SIZE * 2;
|
||||||
|
|
||||||
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
|
sampleRate,
|
||||||
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM);
|
||||||
|
track.play();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Try to release the AudioTrack if we got far enough
|
||||||
|
try {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// Now try the larger buffer size
|
||||||
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||||
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
|
FRAME_SIZE * 2);
|
||||||
|
|
||||||
|
// Round to next frame
|
||||||
|
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
|
||||||
|
|
||||||
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
|
sampleRate,
|
||||||
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM);
|
||||||
|
track.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Audio track buffer size: "+bufferSize);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
||||||
|
track.write(audioData, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void streamClosing() {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCapabilities() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -52,7 +52,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
private RSAPrivateKey key;
|
private RSAPrivateKey key;
|
||||||
private byte[] pemCertBytes;
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
private static Object globalCryptoLock = new Object();
|
private static final Object globalCryptoLock = new Object();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// Install the Bouncy Castle provider
|
// Install the Bouncy Castle provider
|
||||||
@@ -74,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) {
|
||||||
+260
-171
@@ -7,6 +7,7 @@ import android.view.InputDevice;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
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;
|
import com.limelight.utils.Vector2d;
|
||||||
@@ -39,17 +40,51 @@ public class ControllerHandler {
|
|||||||
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 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;
|
||||||
|
private double stickDeadzone;
|
||||||
|
private final ControllerMapping defaultMapping = new ControllerMapping();
|
||||||
|
private boolean hasGameController;
|
||||||
|
|
||||||
public ControllerHandler(NvConnection conn) {
|
public ControllerHandler(NvConnection conn, int deadzonePercentage) {
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
|
|
||||||
// We want limelight-common to scale the axis values to match Xinput values
|
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
||||||
ControllerPacket.enableAxisScaling = true;
|
// is required for controller batching support to work.
|
||||||
|
deadzonePercentage = 10;
|
||||||
|
|
||||||
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
|
for (int i = 0; i < ids.length; i++) {
|
||||||
|
InputDevice dev = InputDevice.getDevice(ids[i]);
|
||||||
|
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
||||||
|
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
||||||
|
// This looks like a gamepad, but we'll check X and Y to be sure
|
||||||
|
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) != null &&
|
||||||
|
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) != null) {
|
||||||
|
// This is a gamepad
|
||||||
|
hasGameController = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1% is the lowest possible deadzone we support
|
||||||
|
if (deadzonePercentage <= 0) {
|
||||||
|
deadzonePercentage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stickDeadzone = (double)deadzonePercentage / 100.0;
|
||||||
|
|
||||||
|
// Initialize the default mapping for events with no device
|
||||||
|
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
|
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
|
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
|
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||||
|
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||||
|
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
|
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
|
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||||
@@ -67,9 +102,18 @@ public class ControllerHandler {
|
|||||||
|
|
||||||
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
||||||
ControllerMapping mapping = new ControllerMapping();
|
ControllerMapping mapping = new ControllerMapping();
|
||||||
|
String devName = dev.getName();
|
||||||
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
|
||||||
|
LimeLog.info("Creating controller mapping for device: "+devName);
|
||||||
|
|
||||||
|
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
|
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null &&
|
||||||
|
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) {
|
||||||
|
// This is a gamepad
|
||||||
|
hasGameController = true;
|
||||||
|
mapping.hasJoystickAxes = true;
|
||||||
|
}
|
||||||
|
|
||||||
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
||||||
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
||||||
@@ -91,8 +135,7 @@ public class ControllerHandler {
|
|||||||
{
|
{
|
||||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||||
if (rxRange != null && ryRange != null) {
|
if (rxRange != null && ryRange != null && devName != null) {
|
||||||
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")) {
|
||||||
// Xbox controllers use RX and RY for right stick
|
// Xbox controllers use RX and RY for right stick
|
||||||
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||||
@@ -142,70 +185,73 @@ public class ControllerHandler {
|
|||||||
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;
|
||||||
|
|
||||||
mapping.hatXDeadzone = hatXRange.getFlat();
|
|
||||||
mapping.hatYDeadzone = hatYRange.getFlat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
InputDevice.MotionRange lsXRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis);
|
mapping.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
InputDevice.MotionRange lsYRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis);
|
|
||||||
if (lsXRange != null && lsYRange != null) {
|
|
||||||
// The flat values should never be negative but we'll deal with it if they are
|
|
||||||
mapping.leftStickDeadzoneRadius = Math.max(Math.abs(lsXRange.getFlat()),
|
|
||||||
Math.abs(lsYRange.getFlat()));
|
|
||||||
|
|
||||||
// Some devices (certain OUYAs at least) report a deadzone that's larger
|
|
||||||
// than the entire range of their axis likely due to some system software bug.
|
|
||||||
// If we see a very large deadzone, simply ignore the value and use our default.
|
|
||||||
if (mapping.leftStickDeadzoneRadius > 0.5f) {
|
|
||||||
mapping.leftStickDeadzoneRadius = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there isn't a (reasonable) deadzone at all, use 20%
|
|
||||||
if (mapping.leftStickDeadzoneRadius < 0.02f) {
|
|
||||||
mapping.leftStickDeadzoneRadius = 0.20f;
|
|
||||||
}
|
|
||||||
// Check that the deadzone is 15% at minimum
|
|
||||||
else if (mapping.leftStickDeadzoneRadius < 0.15f) {
|
|
||||||
mapping.leftStickDeadzoneRadius = 0.15f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
InputDevice.MotionRange rsXRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickXAxis);
|
mapping.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
InputDevice.MotionRange rsYRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickYAxis);
|
}
|
||||||
if (rsXRange != null && rsYRange != null) {
|
|
||||||
// The flat values should never be negative but we'll deal with it if they are
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
mapping.rightStickDeadzoneRadius = Math.max(Math.abs(rsXRange.getFlat()),
|
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis);
|
||||||
Math.abs(rsYRange.getFlat()));
|
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis);
|
||||||
|
|
||||||
// Some devices (certain OUYAs at least) report a deadzone that's larger
|
// It's important to have a valid deadzone so controller packet batching works properly
|
||||||
// than the entire range of their axis likely due to some system software bug.
|
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
|
||||||
// If we see a very large deadzone, simply ignore the value and use our default.
|
|
||||||
if (mapping.rightStickDeadzoneRadius > 0.5f) {
|
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
|
||||||
mapping.rightStickDeadzoneRadius = 0;
|
if (mapping.triggerDeadzone < 0.13f ||
|
||||||
}
|
mapping.triggerDeadzone > 0.30f)
|
||||||
|
{
|
||||||
// If there isn't a (reasonable) deadzone at all, use 20%
|
mapping.triggerDeadzone = 0.13f;
|
||||||
if (mapping.rightStickDeadzoneRadius < 0.02f) {
|
}
|
||||||
mapping.rightStickDeadzoneRadius = 0.20f;
|
}
|
||||||
}
|
|
||||||
// Check that the deadzone is 15% at minimum
|
if (devName != null) {
|
||||||
else if (mapping.rightStickDeadzoneRadius < 0.15f) {
|
// For the Nexus Player (and probably other ATV devices), we should
|
||||||
mapping.rightStickDeadzoneRadius = 0.15f;
|
// use the back button as start since it doesn't have a start/menu button
|
||||||
}
|
// on the controller
|
||||||
}
|
if (devName.contains("ASUS Gamepad")) {
|
||||||
}
|
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
|
||||||
|
// is Android 5.0 anyway
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
|
||||||
|
if (!hasStartKey[0] && !hasStartKey[1]) {
|
||||||
|
mapping.backIsStart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses
|
||||||
|
// so we increase the deadzone on them to minimize this
|
||||||
|
mapping.triggerDeadzone = 0.30f;
|
||||||
|
}
|
||||||
|
// Classify this device as a remote by name
|
||||||
|
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
|
||||||
|
// It's only a remote if it doesn't any sticks
|
||||||
|
if (!mapping.hasJoystickAxes) {
|
||||||
|
mapping.isRemote = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// NYKO Playpad has a fake hat that mimics the left stick for some reason
|
||||||
|
else if (devName.contains("NYKO PLAYPAD")) {
|
||||||
|
mapping.hatXAxis = -1;
|
||||||
|
mapping.hatYAxis = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius);
|
||||||
|
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone);
|
||||||
|
|
||||||
return mapping;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControllerMapping getMappingForDevice(InputDevice dev) {
|
private ControllerMapping getMappingForDevice(InputDevice dev) {
|
||||||
// Unknown devices can't be handled
|
// Unknown devices use the default mapping
|
||||||
if (dev == null) {
|
if (dev == null) {
|
||||||
return null;
|
return defaultMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
String descriptor = dev.getDescriptor();
|
String descriptor = dev.getDescriptor();
|
||||||
@@ -227,9 +273,18 @@ public class ControllerHandler {
|
|||||||
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
|
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
|
||||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
||||||
if (mapping.isDualShock4) {
|
// Device MAY BE NULL
|
||||||
|
private int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
||||||
|
// For remotes, don't capture the back button
|
||||||
|
if (mapping.isRemote) {
|
||||||
|
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.isDualShock4) {
|
||||||
switch (event.getKeyCode()) {
|
switch (event.getKeyCode()) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||||
@@ -297,114 +352,145 @@ public class ControllerHandler {
|
|||||||
return KeyEvent.KEYCODE_DPAD_DOWN;
|
return KeyEvent.KEYCODE_DPAD_DOWN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Past here we can fixup the keycode and potentially trigger
|
||||||
|
// another special case so we need to remember what keycode we're using
|
||||||
|
int keyCode = event.getKeyCode();
|
||||||
|
|
||||||
|
// This is a hack for (at least) the "Tablet Remote" app
|
||||||
|
// which sends BACK with META_ALT_ON instead of KEYCODE_BUTTON_B
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK &&
|
||||||
|
!event.hasNoModifiers() &&
|
||||||
|
(event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0)
|
||||||
|
{
|
||||||
|
keyCode = KeyEvent.KEYCODE_BUTTON_B;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
|
||||||
|
keyCode == KeyEvent.KEYCODE_MENU) {
|
||||||
|
// Ensure that we never use back as start if we have a real start
|
||||||
|
mapping.backIsStart = false;
|
||||||
|
}
|
||||||
|
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
// Emulate the start button with back
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
}
|
||||||
|
|
||||||
return event.getKeyCode();
|
return keyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Vector2d populateCachedVector(float x, float y) {
|
||||||
|
// Reinitialize our cached Vector2d object
|
||||||
|
inputVector.initialize(x, y);
|
||||||
|
return inputVector;
|
||||||
|
}
|
||||||
|
|
||||||
private Vector2d handleDeadZone(float x, float y, float deadzoneRadius) {
|
private void handleDeadZone(Vector2d stickVector, float deadzoneRadius) {
|
||||||
// Reinitialize our cached Vector2d object
|
if (stickVector.getMagnitude() <= deadzoneRadius) {
|
||||||
inputVector.initialize(x, y);
|
// Deadzone
|
||||||
|
stickVector.initialize(0, 0);
|
||||||
if (inputVector.getMagnitude() <= deadzoneRadius) {
|
|
||||||
// Deadzone -- return the zero vector
|
|
||||||
return Vector2d.ZERO;
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Scale the input based on the distance from the deadzone
|
// We're not normalizing here because we let the computer handle the deadzones.
|
||||||
inputVector.getNormalized(normalizedInputVector);
|
// Normalizing can make the deadzones larger than they should be after the computer also
|
||||||
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
|
// evaluates the deadzone.
|
||||||
|
}
|
||||||
// Bound the X value to -1.0 to 1.0
|
|
||||||
if (normalizedInputVector.getX() > 1.0f) {
|
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX,
|
||||||
normalizedInputVector.setX(1.0f);
|
float rsY, float lt, float rt, float hatX, float hatY) {
|
||||||
}
|
|
||||||
else if (normalizedInputVector.getX() < -1.0f) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
normalizedInputVector.setX(-1.0f);
|
Vector2d leftStickVector = populateCachedVector(lsX, lsY);
|
||||||
}
|
|
||||||
|
handleDeadZone(leftStickVector, mapping.leftStickDeadzoneRadius);
|
||||||
// Bound the Y value to -1.0 to 1.0
|
|
||||||
if (normalizedInputVector.getY() > 1.0f) {
|
leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
|
||||||
normalizedInputVector.setY(1.0f);
|
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
|
||||||
}
|
}
|
||||||
else if (normalizedInputVector.getY() < -1.0f) {
|
|
||||||
normalizedInputVector.setY(-1.0f);
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
}
|
Vector2d rightStickVector = populateCachedVector(rsX, rsY);
|
||||||
|
|
||||||
return normalizedInputVector;
|
handleDeadZone(rightStickVector, mapping.rightStickDeadzoneRadius);
|
||||||
}
|
|
||||||
}
|
rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
|
||||||
|
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
|
if (mapping.triggersIdleNegative) {
|
||||||
|
lt = (lt + 1) / 2;
|
||||||
|
rt = (rt + 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lt <= mapping.triggerDeadzone) {
|
||||||
|
lt = 0;
|
||||||
|
}
|
||||||
|
if (rt <= mapping.triggerDeadzone) {
|
||||||
|
rt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
leftTrigger = (byte)(lt * 0xFF);
|
||||||
|
rightTrigger = (byte)(rt * 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
|
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||||
|
if (hatX < -0.5) {
|
||||||
|
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
|
}
|
||||||
|
else if (hatX > 0.5) {
|
||||||
|
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||||
|
if (hatY < -0.5) {
|
||||||
|
inputMap |= ControllerPacket.UP_FLAG;
|
||||||
|
}
|
||||||
|
else if (hatY > 0.5) {
|
||||||
|
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendControllerInputPacket();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean handleMotionEvent(MotionEvent event) {
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
if (mapping == null) {
|
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle left stick events outside of the deadzone
|
|
||||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
|
||||||
Vector2d leftStickVector = handleDeadZone(event.getAxisValue(mapping.leftStickXAxis),
|
|
||||||
event.getAxisValue(mapping.leftStickYAxis), mapping.leftStickDeadzoneRadius);
|
|
||||||
|
|
||||||
leftStickX = (short)(leftStickVector.getX() * 0x7FFE);
|
|
||||||
leftStickY = (short)(-leftStickVector.getY() * 0x7FFE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle right stick events outside of the deadzone
|
|
||||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
|
||||||
Vector2d rightStickVector = handleDeadZone(event.getAxisValue(mapping.rightStickXAxis),
|
|
||||||
event.getAxisValue(mapping.rightStickYAxis), mapping.rightStickDeadzoneRadius);
|
|
||||||
|
|
||||||
rightStickX = (short)(rightStickVector.getX() * 0x7FFE);
|
|
||||||
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle controllers with analog triggers
|
|
||||||
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
|
||||||
float L2 = event.getAxisValue(mapping.leftTriggerAxis);
|
|
||||||
float R2 = event.getAxisValue(mapping.rightTriggerAxis);
|
|
||||||
|
|
||||||
if (mapping.triggersIdleNegative) {
|
|
||||||
L2 = (L2 + 1) / 2;
|
|
||||||
R2 = (R2 + 1) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
leftTrigger = (byte)(L2 * 0xFF);
|
|
||||||
rightTrigger = (byte)(R2 * 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hats emulate d-pad events
|
// We purposefully ignore the historical values in the motion event as it makes
|
||||||
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
// the controller feel sluggish for some users.
|
||||||
float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
|
||||||
float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
|
||||||
|
|
||||||
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
if (hatX < -(0.5 + mapping.hatXDeadzone)) {
|
lsX = event.getAxisValue(mapping.leftStickXAxis);
|
||||||
inputMap |= ControllerPacket.LEFT_FLAG;
|
lsY = event.getAxisValue(mapping.leftStickYAxis);
|
||||||
}
|
}
|
||||||
else if (hatX > (0.5 + mapping.hatXDeadzone)) {
|
|
||||||
inputMap |= ControllerPacket.RIGHT_FLAG;
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
}
|
rsX = event.getAxisValue(mapping.rightStickXAxis);
|
||||||
|
rsY = event.getAxisValue(mapping.rightStickYAxis);
|
||||||
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
}
|
||||||
if (hatY < -(0.5 + mapping.hatYDeadzone)) {
|
|
||||||
inputMap |= ControllerPacket.UP_FLAG;
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
}
|
lt = event.getAxisValue(mapping.leftTriggerAxis);
|
||||||
else if (hatY > (0.5 + mapping.hatYDeadzone)) {
|
rt = event.getAxisValue(mapping.rightTriggerAxis);
|
||||||
inputMap |= ControllerPacket.DOWN_FLAG;
|
}
|
||||||
}
|
|
||||||
}
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
|
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||||
|
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
|
||||||
|
|
||||||
sendControllerInputPacket();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleButtonUp(int keyCode, KeyEvent event) {
|
public boolean handleButtonUp(KeyEvent event) {
|
||||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
if (mapping == null) {
|
|
||||||
return false;
|
int keyCode = handleRemapping(mapping, event);
|
||||||
}
|
|
||||||
|
|
||||||
keyCode = handleRemapping(mapping, event);
|
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -418,7 +504,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) {
|
||||||
@@ -495,7 +581,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) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +599,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) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,13 +607,10 @@ public class ControllerHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleButtonDown(int keyCode, KeyEvent event) {
|
public boolean handleButtonDown(KeyEvent event) {
|
||||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
if (mapping == null) {
|
|
||||||
return false;
|
int keyCode = handleRemapping(mapping, event);
|
||||||
}
|
|
||||||
|
|
||||||
keyCode = handleRemapping(mapping, event);
|
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -613,8 +696,12 @@ public class ControllerHandler {
|
|||||||
|
|
||||||
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket();
|
// Send a new input packet if this is the first instance of a button down event
|
||||||
|
// or anytime if we're emulating a button
|
||||||
|
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) {
|
||||||
|
sendControllerInputPacket();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,13 +717,15 @@ public class ControllerHandler {
|
|||||||
public int leftTriggerAxis = -1;
|
public int leftTriggerAxis = -1;
|
||||||
public int rightTriggerAxis = -1;
|
public int rightTriggerAxis = -1;
|
||||||
public boolean triggersIdleNegative;
|
public boolean triggersIdleNegative;
|
||||||
|
public float triggerDeadzone;
|
||||||
|
|
||||||
public int hatXAxis = -1;
|
public int hatXAxis = -1;
|
||||||
public int hatYAxis = -1;
|
public int hatYAxis = -1;
|
||||||
public float hatXDeadzone;
|
|
||||||
public float hatYDeadzone;
|
|
||||||
|
|
||||||
public boolean isDualShock4;
|
public boolean isDualShock4;
|
||||||
public boolean isXboxController;
|
public boolean isXboxController;
|
||||||
|
public boolean backIsStart;
|
||||||
|
public boolean isRemote;
|
||||||
|
public boolean hasJoystickAxes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+19
-7
@@ -12,15 +12,23 @@ public class TouchContext {
|
|||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
private int actionIndex;
|
private int actionIndex;
|
||||||
|
private double xFactor, yFactor;
|
||||||
|
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex)
|
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||||
{
|
{
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.actionIndex = actionIndex;
|
this.actionIndex = actionIndex;
|
||||||
|
this.xFactor = xFactor;
|
||||||
|
this.yFactor = yFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getActionIndex()
|
||||||
|
{
|
||||||
|
return actionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTap()
|
private boolean isTap()
|
||||||
{
|
{
|
||||||
@@ -65,7 +73,7 @@ public class TouchContext {
|
|||||||
// do input detection by polling
|
// do input detection by polling
|
||||||
try {
|
try {
|
||||||
Thread.sleep(100);
|
Thread.sleep(100);
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
// Raise the mouse button
|
// Raise the mouse button
|
||||||
conn.sendMouseButtonUp(buttonIndex);
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
@@ -78,16 +86,20 @@ public class TouchContext {
|
|||||||
{
|
{
|
||||||
// We only send moves for the primary touch point
|
// We only send moves for the primary touch point
|
||||||
if (actionIndex == 0) {
|
if (actionIndex == 0) {
|
||||||
conn.sendMouseMove((short)(eventX - lastTouchX),
|
int deltaX = eventX - lastTouchX;
|
||||||
(short)(eventY - lastTouchY));
|
int deltaY = eventY - lastTouchY;
|
||||||
|
|
||||||
|
// Scale the deltas based on the factors passed to our constructor
|
||||||
|
deltaX = (int)Math.round((double)deltaX * xFactor);
|
||||||
|
deltaY = (int)Math.round((double)deltaY * yFactor);
|
||||||
|
|
||||||
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastTouchX = eventX;
|
lastTouchX = eventX;
|
||||||
lastTouchY = eventY;
|
lastTouchY = eventY;
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
@@ -8,6 +8,7 @@ public class EvdevEvent {
|
|||||||
public static final short EV_SYN = 0x00;
|
public static final short EV_SYN = 0x00;
|
||||||
public static final short EV_KEY = 0x01;
|
public static final short EV_KEY = 0x01;
|
||||||
public static final short EV_REL = 0x02;
|
public static final short EV_REL = 0x02;
|
||||||
|
public static final short EV_MSC = 0x04;
|
||||||
|
|
||||||
/* Relative axes */
|
/* Relative axes */
|
||||||
public static final short REL_X = 0x00;
|
public static final short REL_X = 0x00;
|
||||||
@@ -18,6 +19,15 @@ public class EvdevEvent {
|
|||||||
public static final short BTN_LEFT = 0x110;
|
public static final short BTN_LEFT = 0x110;
|
||||||
public static final short BTN_RIGHT = 0x111;
|
public static final short BTN_RIGHT = 0x111;
|
||||||
public static final short BTN_MIDDLE = 0x112;
|
public static final short BTN_MIDDLE = 0x112;
|
||||||
|
public static final short BTN_SIDE = 0x113;
|
||||||
|
public static final short BTN_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 type;
|
||||||
public short code;
|
public short code;
|
||||||
+39
-6
@@ -10,6 +10,7 @@ public class EvdevHandler {
|
|||||||
private String absolutePath;
|
private String absolutePath;
|
||||||
private EvdevListener listener;
|
private EvdevListener listener;
|
||||||
private boolean shutdown = false;
|
private boolean shutdown = false;
|
||||||
|
private int fd = -1;
|
||||||
|
|
||||||
private Thread handlerThread = new Thread() {
|
private Thread handlerThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
@@ -19,16 +20,17 @@ public class EvdevHandler {
|
|||||||
// system-wide input problems.
|
// system-wide input problems.
|
||||||
|
|
||||||
// Open the /dev/input/eventX file
|
// Open the /dev/input/eventX file
|
||||||
int fd = EvdevReader.open(absolutePath);
|
fd = EvdevReader.open(absolutePath);
|
||||||
if (fd == -1) {
|
if (fd == -1) {
|
||||||
LimeLog.warning("Unable to open "+absolutePath);
|
LimeLog.warning("Unable to open "+absolutePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if it's a mouse
|
// Check if it's a mouse or keyboard, but not a gamepad
|
||||||
if (!EvdevReader.isMouse(fd)) {
|
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
|
||||||
// We only handle mice
|
EvdevReader.isGamepad(fd)) {
|
||||||
|
// We only handle keyboards and mice
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ public class EvdevHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Grabbed device for raw mouse input: "+absolutePath);
|
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
|
||||||
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
||||||
|
|
||||||
@@ -96,7 +98,31 @@ public class EvdevHandler {
|
|||||||
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
||||||
event.value != 0);
|
event.value != 0);
|
||||||
break;
|
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 {
|
} finally {
|
||||||
@@ -120,12 +146,19 @@ public class EvdevHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
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;
|
shutdown = true;
|
||||||
handlerThread.interrupt();
|
handlerThread.interrupt();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handlerThread.join();
|
handlerThread.join();
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyDeleted() {
|
public void notifyDeleted() {
|
||||||
+1
@@ -8,4 +8,5 @@ public interface EvdevListener {
|
|||||||
public void mouseMove(int deltaX, int deltaY);
|
public void mouseMove(int deltaX, int deltaY);
|
||||||
public void mouseButtonEvent(int buttonId, boolean down);
|
public void mouseButtonEvent(int buttonId, boolean down);
|
||||||
public void mouseScroll(byte amount);
|
public void mouseScroll(byte amount);
|
||||||
|
public void keyboardEvent(boolean buttonDown, short keyCode);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class EvdevReader {
|
||||||
|
static {
|
||||||
|
System.loadLibrary("evdev_reader");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void patchSeLinuxPolicies() {
|
||||||
|
//
|
||||||
|
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
|
||||||
|
// We should probably do something clever with a separate daemon and talk via a localhost
|
||||||
|
// socket. We don't return the SELinux policies back to default after we're done which I feel
|
||||||
|
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
|
||||||
|
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
|
||||||
|
// normally be able to do with SELinux enforcing on Lollipop.
|
||||||
|
//
|
||||||
|
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
|
||||||
|
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
|
||||||
|
// 4.4 and later to do live SELinux policy changes.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
EvdevShell shell = EvdevShell.getInstance();
|
||||||
|
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir getattr\" " +
|
||||||
|
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requires root to chmod /dev/input/eventX
|
||||||
|
public static void setPermissions(String[] files, int octalPermissions) {
|
||||||
|
EvdevShell shell = EvdevShell.getInstance();
|
||||||
|
|
||||||
|
for (String file : files) {
|
||||||
|
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the fd to be passed to other function or -1 on error
|
||||||
|
public static native int open(String fileName);
|
||||||
|
|
||||||
|
// Prevent other apps (including Android itself) from using the device while "grabbed"
|
||||||
|
public static native boolean grab(int fd);
|
||||||
|
public static native boolean ungrab(int fd);
|
||||||
|
|
||||||
|
// Used for checking device capabilities
|
||||||
|
public static native boolean hasRelAxis(int fd, short axis);
|
||||||
|
public static native boolean hasAbsAxis(int fd, short axis);
|
||||||
|
public static native boolean hasKey(int fd, short key);
|
||||||
|
|
||||||
|
public static boolean isMouse(int fd) {
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
return hasRelAxis(fd, EvdevEvent.REL_X) &&
|
||||||
|
hasRelAxis(fd, EvdevEvent.REL_Y) &&
|
||||||
|
hasKey(fd, EvdevEvent.BTN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAlphaKeyboard(int fd) {
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
return hasKey(fd, EvdevEvent.KEY_Q);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGamepad(int fd) {
|
||||||
|
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the bytes read or -1 on error
|
||||||
|
private static native int read(int fd, byte[] buffer);
|
||||||
|
|
||||||
|
// Takes a byte buffer to use to read the output into.
|
||||||
|
// This buffer MUST be in native byte order and at least
|
||||||
|
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||||
|
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
||||||
|
int bytesRead = read(fd, buffer.array());
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
LimeLog.warning("Failed to read: "+bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||||
|
LimeLog.warning("Short read: "+bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.limit(bytesRead);
|
||||||
|
buffer.rewind();
|
||||||
|
|
||||||
|
// Throw away the time stamp
|
||||||
|
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||||
|
buffer.getLong();
|
||||||
|
buffer.getLong();
|
||||||
|
} else {
|
||||||
|
buffer.getInt();
|
||||||
|
buffer.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the fd from open()
|
||||||
|
public static native int close(int fd);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.Scanner;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class EvdevShell {
|
||||||
|
private OutputStream stdin;
|
||||||
|
private InputStream stdout;
|
||||||
|
private Process shell;
|
||||||
|
private final String uuidString = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
private static final EvdevShell globalShell = new EvdevShell();
|
||||||
|
|
||||||
|
public static EvdevShell getInstance() {
|
||||||
|
return globalShell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startShell() {
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("su");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Redirect stderr to stdout
|
||||||
|
builder.redirectErrorStream(true);
|
||||||
|
shell = builder.start();
|
||||||
|
|
||||||
|
stdin = shell.getOutputStream();
|
||||||
|
stdout = shell.getInputStream();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This is unexpected
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// Kill the shell if it spawned
|
||||||
|
if (stdin != null) {
|
||||||
|
try {
|
||||||
|
stdin.close();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
e1.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
stdin = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stdout != null) {
|
||||||
|
try {
|
||||||
|
stdout.close();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
e1.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
stdout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shell != null) {
|
||||||
|
shell.destroy();
|
||||||
|
shell = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runCommand(String command) {
|
||||||
|
if (shell == null) {
|
||||||
|
// Shell never started
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write the command followed by an echo with our UUID
|
||||||
|
stdin.write((command+'\n').getBytes("UTF-8"));
|
||||||
|
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
|
||||||
|
stdin.flush();
|
||||||
|
|
||||||
|
// This is the only command in flight so we can use a scanner
|
||||||
|
// without worrying about it eating too many characters
|
||||||
|
Scanner scanner = new Scanner(stdout);
|
||||||
|
while (scanner.hasNext()) {
|
||||||
|
if (scanner.next().contains(uuidString)) {
|
||||||
|
// Our command ran
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopShell() throws InterruptedException {
|
||||||
|
boolean exitWritten = false;
|
||||||
|
|
||||||
|
if (shell == null) {
|
||||||
|
// Shell never started
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
stdin.write("exit\n".getBytes("UTF-8"));
|
||||||
|
exitWritten = true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
// We'll destroy the process without
|
||||||
|
// waiting for it to terminate since
|
||||||
|
// we don't know whether our exit command made it
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exitWritten) {
|
||||||
|
try {
|
||||||
|
shell.waitFor();
|
||||||
|
} finally {
|
||||||
|
shell.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
shell.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
public class EvdevTranslator {
|
||||||
|
|
||||||
|
public static final short EVDEV_KEY_CODES[] = {
|
||||||
|
0, //KeyEvent.VK_RESERVED
|
||||||
|
KeyEvent.KEYCODE_ESCAPE,
|
||||||
|
KeyEvent.KEYCODE_1,
|
||||||
|
KeyEvent.KEYCODE_2,
|
||||||
|
KeyEvent.KEYCODE_3,
|
||||||
|
KeyEvent.KEYCODE_4,
|
||||||
|
KeyEvent.KEYCODE_5,
|
||||||
|
KeyEvent.KEYCODE_6,
|
||||||
|
KeyEvent.KEYCODE_7,
|
||||||
|
KeyEvent.KEYCODE_8,
|
||||||
|
KeyEvent.KEYCODE_9,
|
||||||
|
KeyEvent.KEYCODE_0,
|
||||||
|
KeyEvent.KEYCODE_MINUS,
|
||||||
|
KeyEvent.KEYCODE_EQUALS,
|
||||||
|
KeyEvent.KEYCODE_DEL,
|
||||||
|
KeyEvent.KEYCODE_TAB,
|
||||||
|
KeyEvent.KEYCODE_Q,
|
||||||
|
KeyEvent.KEYCODE_W,
|
||||||
|
KeyEvent.KEYCODE_E,
|
||||||
|
KeyEvent.KEYCODE_R,
|
||||||
|
KeyEvent.KEYCODE_T,
|
||||||
|
KeyEvent.KEYCODE_Y,
|
||||||
|
KeyEvent.KEYCODE_U,
|
||||||
|
KeyEvent.KEYCODE_I,
|
||||||
|
KeyEvent.KEYCODE_O,
|
||||||
|
KeyEvent.KEYCODE_P,
|
||||||
|
KeyEvent.KEYCODE_LEFT_BRACKET,
|
||||||
|
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
||||||
|
KeyEvent.KEYCODE_ENTER,
|
||||||
|
KeyEvent.KEYCODE_CTRL_LEFT,
|
||||||
|
KeyEvent.KEYCODE_A,
|
||||||
|
KeyEvent.KEYCODE_S,
|
||||||
|
KeyEvent.KEYCODE_D,
|
||||||
|
KeyEvent.KEYCODE_F,
|
||||||
|
KeyEvent.KEYCODE_G,
|
||||||
|
KeyEvent.KEYCODE_H,
|
||||||
|
KeyEvent.KEYCODE_J,
|
||||||
|
KeyEvent.KEYCODE_K,
|
||||||
|
KeyEvent.KEYCODE_L,
|
||||||
|
KeyEvent.KEYCODE_SEMICOLON,
|
||||||
|
KeyEvent.KEYCODE_APOSTROPHE,
|
||||||
|
KeyEvent.KEYCODE_GRAVE,
|
||||||
|
KeyEvent.KEYCODE_SHIFT_LEFT,
|
||||||
|
KeyEvent.KEYCODE_BACKSLASH,
|
||||||
|
KeyEvent.KEYCODE_Z,
|
||||||
|
KeyEvent.KEYCODE_X,
|
||||||
|
KeyEvent.KEYCODE_C,
|
||||||
|
KeyEvent.KEYCODE_V,
|
||||||
|
KeyEvent.KEYCODE_B,
|
||||||
|
KeyEvent.KEYCODE_N,
|
||||||
|
KeyEvent.KEYCODE_M,
|
||||||
|
KeyEvent.KEYCODE_COMMA,
|
||||||
|
KeyEvent.KEYCODE_PERIOD,
|
||||||
|
KeyEvent.KEYCODE_SLASH,
|
||||||
|
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
||||||
|
KeyEvent.KEYCODE_ALT_LEFT,
|
||||||
|
KeyEvent.KEYCODE_SPACE,
|
||||||
|
KeyEvent.KEYCODE_CAPS_LOCK,
|
||||||
|
KeyEvent.KEYCODE_F1,
|
||||||
|
KeyEvent.KEYCODE_F2,
|
||||||
|
KeyEvent.KEYCODE_F3,
|
||||||
|
KeyEvent.KEYCODE_F4,
|
||||||
|
KeyEvent.KEYCODE_F5,
|
||||||
|
KeyEvent.KEYCODE_F6,
|
||||||
|
KeyEvent.KEYCODE_F7,
|
||||||
|
KeyEvent.KEYCODE_F8,
|
||||||
|
KeyEvent.KEYCODE_F9,
|
||||||
|
KeyEvent.KEYCODE_F10,
|
||||||
|
KeyEvent.KEYCODE_NUM_LOCK,
|
||||||
|
KeyEvent.KEYCODE_SCROLL_LOCK,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_7,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_8,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_9,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_4,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_5,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_6,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ADD,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_1,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_2,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_3,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_0,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_DOT,
|
||||||
|
0,
|
||||||
|
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
||||||
|
0, //KeyEvent.VK_102ND,
|
||||||
|
KeyEvent.KEYCODE_F11,
|
||||||
|
KeyEvent.KEYCODE_F12,
|
||||||
|
0, //KeyEvent.VK_RO,
|
||||||
|
0, //KeyEvent.VK_KATAKANA,
|
||||||
|
0, //KeyEvent.VK_HIRAGANA,
|
||||||
|
0, //KeyEvent.VK_HENKAN,
|
||||||
|
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
||||||
|
0, //KeyEvent.VK_MUHENKAN,
|
||||||
|
0, //KeyEvent.VK_KPJPCOMMA,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
||||||
|
KeyEvent.KEYCODE_CTRL_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
||||||
|
KeyEvent.KEYCODE_SYSRQ,
|
||||||
|
KeyEvent.KEYCODE_ALT_RIGHT,
|
||||||
|
0, //KeyEvent.VK_LINEFEED,
|
||||||
|
KeyEvent.KEYCODE_HOME,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
|
KeyEvent.KEYCODE_PAGE_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_MOVE_END,
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||||
|
KeyEvent.KEYCODE_INSERT,
|
||||||
|
KeyEvent.KEYCODE_FORWARD_DEL,
|
||||||
|
0, //KeyEvent.VK_MACRO,
|
||||||
|
0, //KeyEvent.VK_MUTE,
|
||||||
|
0, //KeyEvent.VK_VOLUMEDOWN,
|
||||||
|
0, //KeyEvent.VK_VOLUMEUP,
|
||||||
|
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
||||||
|
0, //KeyEvent.VK_KPPLUSMINUS,
|
||||||
|
KeyEvent.KEYCODE_BREAK,
|
||||||
|
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
||||||
|
};
|
||||||
|
|
||||||
|
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
||||||
|
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
||||||
|
return EVDEV_KEY_CODES[evdevKeyCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+22
-2
@@ -8,16 +8,19 @@ import com.limelight.LimeLog;
|
|||||||
|
|
||||||
import android.os.FileObserver;
|
import android.os.FileObserver;
|
||||||
|
|
||||||
|
@SuppressWarnings("ALL")
|
||||||
public class EvdevWatcher {
|
public class EvdevWatcher {
|
||||||
private static final String PATH = "/dev/input";
|
private static final String PATH = "/dev/input";
|
||||||
private static final String REQUIRED_FILE_PREFIX = "event";
|
private static final String REQUIRED_FILE_PREFIX = "event";
|
||||||
|
|
||||||
private HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
||||||
private boolean shutdown = false;
|
private boolean shutdown = false;
|
||||||
private boolean init = false;
|
private boolean init = false;
|
||||||
private boolean ungrabbed = false;
|
private boolean ungrabbed = false;
|
||||||
private EvdevListener listener;
|
private EvdevListener listener;
|
||||||
private Thread startThread;
|
private Thread startThread;
|
||||||
|
|
||||||
|
private static boolean patchedSeLinuxPolicies = false;
|
||||||
|
|
||||||
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
|
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
|
||||||
@Override
|
@Override
|
||||||
@@ -73,6 +76,9 @@ public class EvdevWatcher {
|
|||||||
// Rundown existing files
|
// Rundown existing files
|
||||||
File devInputDir = new File(PATH);
|
File devInputDir = new File(PATH);
|
||||||
File[] files = devInputDir.listFiles();
|
File[] files = devInputDir.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
return new File[0];
|
||||||
|
}
|
||||||
|
|
||||||
// Set desired permissions
|
// Set desired permissions
|
||||||
String[] filePaths = new String[files.length];
|
String[] filePaths = new String[files.length];
|
||||||
@@ -113,6 +119,15 @@ public class EvdevWatcher {
|
|||||||
startThread = new Thread() {
|
startThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
// Initialize the root shell
|
||||||
|
EvdevShell.getInstance().startShell();
|
||||||
|
|
||||||
|
// Patch SELinux policies (if needed)
|
||||||
|
if (!patchedSeLinuxPolicies) {
|
||||||
|
EvdevReader.patchSeLinuxPolicies();
|
||||||
|
patchedSeLinuxPolicies = true;
|
||||||
|
}
|
||||||
|
|
||||||
// List all files and allow us access
|
// List all files and allow us access
|
||||||
File[] files = rundownWithPermissionsChange(0666);
|
File[] files = rundownWithPermissionsChange(0666);
|
||||||
|
|
||||||
@@ -135,7 +150,12 @@ public class EvdevWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Giveup eventX permissions
|
// Giveup eventX permissions
|
||||||
rundownWithPermissionsChange(066);
|
rundownWithPermissionsChange(0660);
|
||||||
|
|
||||||
|
// Kill the root shell
|
||||||
|
try {
|
||||||
|
EvdevShell.getInstance().stopShell();
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
startThread.start();
|
startThread.start();
|
||||||
+47
-21
@@ -5,7 +5,6 @@ import java.io.File;
|
|||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.locks.LockSupport;
|
|
||||||
|
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -18,16 +17,17 @@ 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;
|
||||||
|
|
||||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
|
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
private Thread rendererThread;
|
private Thread rendererThread, decoderThread;
|
||||||
private int targetFps;
|
private int targetFps;
|
||||||
|
|
||||||
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
||||||
private ByteBuffer decoderBuffer;
|
private ByteBuffer decoderBuffer;
|
||||||
|
|
||||||
// Only sleep if the difference is above this value
|
// Only sleep if the difference is above this value
|
||||||
private static final int WAIT_CEILING_MS = 8;
|
private static final int WAIT_CEILING_MS = 5;
|
||||||
|
|
||||||
private static final int LOW_PERF = 1;
|
private static final int LOW_PERF = 1;
|
||||||
private static final int MED_PERF = 2;
|
private static final int MED_PERF = 2;
|
||||||
@@ -107,9 +107,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
case LOW_PERF:
|
case LOW_PERF:
|
||||||
// Disable the loop filter for performance reasons
|
// Disable the loop filter for performance reasons
|
||||||
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
|
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
|
||||||
AvcDecoder.FAST_BILINEAR_FILTERING |
|
|
||||||
AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
// Use plenty of threads to try to utilize the CPU as best we can
|
// Use plenty of threads to try to utilize the CPU as best we can
|
||||||
threadCount = cpuCount - 1;
|
threadCount = cpuCount - 1;
|
||||||
@@ -117,8 +115,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
case MED_PERF:
|
case MED_PERF:
|
||||||
avcFlags = AvcDecoder.BILINEAR_FILTERING |
|
avcFlags = AvcDecoder.BILINEAR_FILTERING;
|
||||||
AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
// Only use 2 threads to minimize frame processing latency
|
// Only use 2 threads to minimize frame processing latency
|
||||||
threadCount = 2;
|
threadCount = 2;
|
||||||
@@ -144,7 +141,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
||||||
}
|
}
|
||||||
|
|
||||||
AvcDecoder.setRenderTarget(sh.getSurface());
|
if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||||
|
|
||||||
@@ -155,6 +154,26 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean start(final VideoDepacketizer depacketizer) {
|
public boolean start(final VideoDepacketizer depacketizer) {
|
||||||
|
decoderThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
DecodeUnit du;
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
try {
|
||||||
|
du = depacketizer.takeNextDecodeUnit();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitDecodeUnit(du);
|
||||||
|
depacketizer.freeDecodeUnit(du);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
decoderThread.setName("Video - Decoder (CPU)");
|
||||||
|
decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
|
||||||
|
decoderThread.start();
|
||||||
|
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -162,17 +181,15 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
DecodeUnit du;
|
DecodeUnit du;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
|
||||||
if (du != null) {
|
|
||||||
submitDecodeUnit(du);
|
|
||||||
depacketizer.freeDecodeUnit(du);
|
|
||||||
}
|
|
||||||
|
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
long diff = nextFrameTime - System.currentTimeMillis();
|
||||||
|
|
||||||
if (diff > WAIT_CEILING_MS) {
|
if (diff > WAIT_CEILING_MS) {
|
||||||
LockSupport.parkNanos(1);
|
try {
|
||||||
continue;
|
Thread.sleep(diff - WAIT_CEILING_MS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||||
@@ -193,10 +210,14 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
rendererThread.interrupt();
|
rendererThread.interrupt();
|
||||||
|
decoderThread.interrupt();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException e) { }
|
} catch (InterruptedException e) { }
|
||||||
|
try {
|
||||||
|
decoderThread.join();
|
||||||
|
} catch (InterruptedException e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -233,7 +254,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
||||||
if (delta >= 0 && delta < 300) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
totalTimeMs += delta;
|
totalTimeMs += delta;
|
||||||
totalFrames++;
|
totalFrames++;
|
||||||
}
|
}
|
||||||
@@ -259,4 +280,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
return (int)(totalTimeMs / totalFrames);
|
return (int)(totalTimeMs / totalFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoderName() {
|
||||||
|
return "CPU decoding";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+12
-2
@@ -3,9 +3,9 @@ package com.limelight.binding.video;
|
|||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
private VideoDecoderRenderer decoderRenderer;
|
private EnhancedDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
@@ -74,4 +74,14 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoderName() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getDecoderName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
|
||||||
|
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
|
||||||
|
public abstract String getDecoderName();
|
||||||
|
}
|
||||||
+100
-10
@@ -22,15 +22,19 @@ import android.media.MediaCodec.CodecException;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
@SuppressWarnings("unused")
|
||||||
|
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
private ByteBuffer[] videoDecoderInputBuffers;
|
private ByteBuffer[] videoDecoderInputBuffers;
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private boolean needsSpsBitstreamFixup;
|
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private VideoDepacketizer depacketizer;
|
private VideoDepacketizer depacketizer;
|
||||||
private boolean adaptivePlayback;
|
private boolean adaptivePlayback;
|
||||||
private int initialWidth, initialHeight;
|
private int initialWidth, initialHeight;
|
||||||
|
|
||||||
|
private boolean needsBaselineSpsHack;
|
||||||
|
private SeqParameterSet savedSps;
|
||||||
|
|
||||||
private long lastTimestampUs;
|
private long lastTimestampUs;
|
||||||
private long totalTimeMs;
|
private long totalTimeMs;
|
||||||
@@ -62,9 +66,17 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// Set decoder-specific attributes
|
// Set decoder-specific attributes
|
||||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
||||||
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
||||||
if (needsSpsBitstreamFixup) {
|
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
|
||||||
|
isExynos4 = MediaCodecHelper.isExynos4Device();
|
||||||
|
if (needsSpsBitstreamFixup) {
|
||||||
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
||||||
}
|
}
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
|
||||||
|
}
|
||||||
|
if (isExynos4) {
|
||||||
|
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
@@ -261,7 +273,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
|
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
|
||||||
if (delta > 5 && delta < 300) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
decoderTimeMs += delta;
|
decoderTimeMs += delta;
|
||||||
totalTimeMs += delta;
|
totalTimeMs += delta;
|
||||||
}
|
}
|
||||||
@@ -319,7 +331,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
rendererThread.interrupt();
|
rendererThread.interrupt();
|
||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException e) { }
|
} catch (InterruptedException ignored) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the decoder
|
// Stop the decoder
|
||||||
@@ -359,7 +371,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
||||||
if (delta >= 0 && delta < 300) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
|
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
|
||||||
totalFrames++;
|
totalFrames++;
|
||||||
}
|
}
|
||||||
@@ -384,6 +396,8 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
|
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
|
||||||
numIframeIn++;
|
numIframeIn++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean needsSpsReplay = false;
|
||||||
|
|
||||||
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||||
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
||||||
@@ -396,6 +410,23 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
spsBuf.position(header.offset+5);
|
spsBuf.position(header.offset+5);
|
||||||
|
|
||||||
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
|
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
|
||||||
|
|
||||||
|
// Some decoders rely on H264 level to decide how many buffers are needed
|
||||||
|
// Since we only need one frame buffered, we'll set the level as low as we can
|
||||||
|
// for known resolution combinations
|
||||||
|
if (initialWidth == 1280 && initialHeight == 720) {
|
||||||
|
// Max 5 buffered frames at 1280x720x60
|
||||||
|
LimeLog.info("Patching level_idc to 32");
|
||||||
|
sps.level_idc = 32;
|
||||||
|
}
|
||||||
|
else if (initialWidth == 1920 && initialHeight == 1080) {
|
||||||
|
// Max 4 buffered frames at 1920x1080x64
|
||||||
|
LimeLog.info("Patching level_idc to 42");
|
||||||
|
sps.level_idc = 42;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Leave the profile alone (currently 5.0)
|
||||||
|
}
|
||||||
|
|
||||||
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
|
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
|
||||||
// also requires this fixup.
|
// also requires this fixup.
|
||||||
@@ -406,7 +437,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
LimeLog.info("Patching num_ref_frames in SPS");
|
LimeLog.info("Patching num_ref_frames in SPS");
|
||||||
sps.num_ref_frames = 1;
|
sps.num_ref_frames = 1;
|
||||||
|
|
||||||
if (needsSpsBitstreamFixup) {
|
if (needsSpsBitstreamFixup || isExynos4) {
|
||||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
// 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.
|
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||||
LimeLog.info("Adding bitstream restrictions");
|
LimeLog.info("Adding bitstream restrictions");
|
||||||
@@ -420,6 +451,13 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
||||||
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
|
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we need to hack this SPS to say we're baseline, do so now
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Hacking SPS to baseline");
|
||||||
|
sps.profile_idc = 66;
|
||||||
|
savedSps = sps;
|
||||||
|
}
|
||||||
|
|
||||||
// Write the annex B header
|
// Write the annex B header
|
||||||
buf.put(header.data, header.offset, 5);
|
buf.put(header.data, header.offset, 5);
|
||||||
@@ -435,6 +473,14 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
return;
|
return;
|
||||||
} else if (header.data[header.offset+4] == 0x68) {
|
} else if (header.data[header.offset+4] == 0x68) {
|
||||||
numPpsIn++;
|
numPpsIn++;
|
||||||
|
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Saw PPS; disabling SPS hack");
|
||||||
|
needsBaselineSpsHack = false;
|
||||||
|
|
||||||
|
// Give the decoder the SPS again with the proper profile now
|
||||||
|
needsSpsReplay = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,9 +495,39 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
|
|
||||||
depacketizer.freeDecodeUnit(decodeUnit);
|
depacketizer.freeDecodeUnit(decodeUnit);
|
||||||
return;
|
|
||||||
|
if (needsSpsReplay) {
|
||||||
|
replaySps();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void replaySps() {
|
||||||
|
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
||||||
|
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
||||||
|
|
||||||
|
inputBuffer.clear();
|
||||||
|
|
||||||
|
// Write the Annex B header
|
||||||
|
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||||
|
|
||||||
|
// Switch the H264 profile back to high
|
||||||
|
savedSps.profile_idc = 100;
|
||||||
|
|
||||||
|
// Write the SPS data
|
||||||
|
savedSps.write(inputBuffer);
|
||||||
|
|
||||||
|
// No need for the SPS anymore
|
||||||
|
savedSps = null;
|
||||||
|
|
||||||
|
// Queue the new SPS
|
||||||
|
queueInputBuffer(inputIndex,
|
||||||
|
0, inputBuffer.position(),
|
||||||
|
System.currentTimeMillis() * 1000,
|
||||||
|
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
||||||
|
|
||||||
|
LimeLog.info("SPS replay complete");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
return adaptivePlayback ?
|
return adaptivePlayback ?
|
||||||
@@ -473,8 +549,13 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
return (int)(totalTimeMs / totalFrames);
|
return (int)(totalTimeMs / totalFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RendererException extends RuntimeException {
|
@Override
|
||||||
|
public String getDecoderName() {
|
||||||
|
return decoderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RendererException extends RuntimeException {
|
||||||
private static final long serialVersionUID = 8985937536997012406L;
|
private static final long serialVersionUID = 8985937536997012406L;
|
||||||
|
|
||||||
private Exception originalException;
|
private Exception originalException;
|
||||||
@@ -512,6 +593,15 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
str += "Buffer codec flags: "+currentCodecFlags+"\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";
|
str += "Full decoder dump:\n";
|
||||||
try {
|
try {
|
||||||
str += MediaCodecHelper.dumpDecoders();
|
str += MediaCodecHelper.dumpDecoders();
|
||||||
+84
-7
@@ -1,7 +1,12 @@
|
|||||||
package com.limelight.binding.video;
|
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.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
@@ -20,6 +25,7 @@ public class MediaCodecHelper {
|
|||||||
public static final List<String> blacklistedDecoderPrefixes;
|
public static final List<String> blacklistedDecoderPrefixes;
|
||||||
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||||
|
public static final List<String> baselineProfileHackPrefixes;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<String>();
|
preferredDecoders = new LinkedList<String>();
|
||||||
@@ -38,7 +44,11 @@ public class MediaCodecHelper {
|
|||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
||||||
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||||
|
|
||||||
|
baselineProfileHackPrefixes = new LinkedList<String>();
|
||||||
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
@@ -61,6 +71,10 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
|
/*
|
||||||
|
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
||||||
|
so we'll keep it off for now, since we don't know whether other devices also do the same
|
||||||
|
|
||||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||||
return true;
|
return true;
|
||||||
@@ -79,7 +93,7 @@ public class MediaCodecHelper {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Tolerate buggy codecs
|
// Tolerate buggy codecs
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -87,6 +101,10 @@ public class MediaCodecHelper {
|
|||||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
@@ -95,9 +113,7 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
for (MediaCodecInfo info : mcl.getCodecInfos()) {
|
Collections.addAll(infoList, mcl.getCodecInfos());
|
||||||
infoList.add(info);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
||||||
@@ -108,7 +124,8 @@ public class MediaCodecHelper {
|
|||||||
return infoList;
|
return infoList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String dumpDecoders() throws Exception {
|
@SuppressWarnings("RedundantThrows")
|
||||||
|
public static String dumpDecoders() throws Exception {
|
||||||
String str = "";
|
String str = "";
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
// Skip encoders
|
// Skip encoders
|
||||||
@@ -199,7 +216,8 @@ public class MediaCodecHelper {
|
|||||||
// We declare this method as explicitly throwing Exception
|
// We declare this method as explicitly throwing Exception
|
||||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
||||||
// and we want to be sure all callers are handling this possibility
|
// and we want to be sure all callers are handling this possibility
|
||||||
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
@SuppressWarnings("RedundantThrows")
|
||||||
|
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
// Skip encoders
|
// Skip encoders
|
||||||
if (codecInfo.isEncoder()) {
|
if (codecInfo.isEncoder()) {
|
||||||
@@ -233,4 +251,63 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+8
-3
@@ -99,15 +99,17 @@ public class ComputerDatabaseManager {
|
|||||||
details.macAddress = c.getString(4);
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
// If a field is corrupt or missing, skip the database entry
|
// If a field is corrupt or missing, skip 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) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
computerList.add(details);
|
|
||||||
|
computerList.add(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
@@ -151,6 +153,9 @@ public class ComputerDatabaseManager {
|
|||||||
details.macAddress = c.getString(4);
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
|
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
// 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 ||
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.discovery.DiscoveryService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
public class ComputerManagerService extends Service {
|
||||||
|
private static final int POLLING_PERIOD_MS = 3000;
|
||||||
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
|
|
||||||
|
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
|
private ComputerDatabaseManager dbManager;
|
||||||
|
private AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private IdentityManager idManager;
|
||||||
|
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
||||||
|
private ComputerManagerListener listener = null;
|
||||||
|
private AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
|
private boolean pollingActive = false;
|
||||||
|
|
||||||
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
|
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
|
synchronized (discoveryServiceConnection) {
|
||||||
|
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
|
||||||
|
|
||||||
|
// Set us as the event listener
|
||||||
|
privateBinder.setListener(createDiscoveryListener());
|
||||||
|
|
||||||
|
// Signal a possible waiter that we're all setup
|
||||||
|
discoveryBinder = privateBinder;
|
||||||
|
discoveryServiceConnection.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
discoveryBinder = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns true if the details object was modified
|
||||||
|
private boolean runPoll(ComputerDetails details)
|
||||||
|
{
|
||||||
|
boolean newPc = details.name.isEmpty();
|
||||||
|
|
||||||
|
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() && pollingActive) {
|
||||||
|
// Check if this poll has modified the details
|
||||||
|
runPoll(details);
|
||||||
|
|
||||||
|
// Wait until the next polling interval
|
||||||
|
try {
|
||||||
|
Thread.sleep(POLLING_PERIOD_MS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.setName("Polling thread for "+details.localIp.getHostAddress());
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ComputerManagerBinder extends Binder {
|
||||||
|
public void startPolling(ComputerManagerListener listener) {
|
||||||
|
// Polling is active
|
||||||
|
pollingActive = true;
|
||||||
|
|
||||||
|
// Set the listener
|
||||||
|
ComputerManagerService.this.listener = listener;
|
||||||
|
|
||||||
|
// Start mDNS autodiscovery too
|
||||||
|
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||||
|
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
// This polling thread might already be there
|
||||||
|
if (tuple.thread == null) {
|
||||||
|
// Report this computer initially
|
||||||
|
listener.notifyComputerUpdated(tuple.computer);
|
||||||
|
|
||||||
|
tuple.thread = createPollingThread(tuple.computer);
|
||||||
|
tuple.thread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void waitForReady() {
|
||||||
|
synchronized (discoveryServiceConnection) {
|
||||||
|
try {
|
||||||
|
while (discoveryBinder == null) {
|
||||||
|
// Wait for the bind notification
|
||||||
|
discoveryServiceConnection.wait(1000);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void waitForPollingStopped() {
|
||||||
|
while (activePolls.get() != 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(250);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
|
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(InetAddress addr) {
|
||||||
|
ComputerManagerService.this.addComputer(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeComputer(String name) {
|
||||||
|
ComputerManagerService.this.removeComputer(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopPolling() {
|
||||||
|
// Just call the unbind handler to cleanup
|
||||||
|
ComputerManagerService.this.onUnbind(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUniqueId() {
|
||||||
|
return idManager.getUniqueId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUnbind(Intent intent) {
|
||||||
|
// Stop mDNS autodiscovery
|
||||||
|
discoveryBinder.stopDiscovery();
|
||||||
|
|
||||||
|
// Stop polling
|
||||||
|
pollingActive = false;
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
// Interrupt and remove the thread
|
||||||
|
tuple.thread.interrupt();
|
||||||
|
tuple.thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the listener
|
||||||
|
listener = null;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||||
|
return new MdnsDiscoveryListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
|
// Kick off a serverinfo poll on this machine
|
||||||
|
addComputer(computer.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyDiscoveryFailure(Exception e) {
|
||||||
|
LimeLog.severe("mDNS discovery failed");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(InetAddress addr) {
|
||||||
|
// Setup a placeholder
|
||||||
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
|
fakeDetails.localIp = addr;
|
||||||
|
fakeDetails.remoteIp = addr;
|
||||||
|
fakeDetails.name = "";
|
||||||
|
|
||||||
|
addTuple(fakeDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTuple(ComputerDetails details) {
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
// Check if this is the same computer
|
||||||
|
if (tuple.computer == details ||
|
||||||
|
// If there's no name on one of these computers, compare with the local IP
|
||||||
|
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
|
||||||
|
tuple.computer.localIp.equals(details.localIp)) ||
|
||||||
|
// If there is a name on both computers, compare with name
|
||||||
|
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
|
||||||
|
tuple.computer.name.equals(details.name))) {
|
||||||
|
|
||||||
|
// Start a polling thread if polling is active
|
||||||
|
if (pollingActive && tuple.thread == null) {
|
||||||
|
tuple.thread = createPollingThread(details);
|
||||||
|
tuple.thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found an entry so we're done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, we didn't find an entry
|
||||||
|
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
|
||||||
|
pollingTuples.add(tuple);
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
tuple.thread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
|
// Setup a placeholder
|
||||||
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
|
fakeDetails.localIp = addr;
|
||||||
|
fakeDetails.remoteIp = addr;
|
||||||
|
fakeDetails.name = "";
|
||||||
|
|
||||||
|
// Block while we try to fill the details
|
||||||
|
runPoll(fakeDetails);
|
||||||
|
|
||||||
|
// If the machine is reachable, it was successful
|
||||||
|
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
||||||
|
// Start a polling thread for this machine
|
||||||
|
addTuple(fakeDetails);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeComputer(String name) {
|
||||||
|
if (!getLocalDatabaseReference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove it from the database
|
||||||
|
dbManager.deleteComputer(name);
|
||||||
|
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
// Remove the computer from the computer list
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (tuple.computer.name.equals(name)) {
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
// Interrupt the thread on this entry
|
||||||
|
tuple.thread.interrupt();
|
||||||
|
}
|
||||||
|
pollingTuples.remove(tuple);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean getLocalDatabaseReference() {
|
||||||
|
if (dbRefCount.get() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRefCount.incrementAndGet();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseLocalDatabaseReference() {
|
||||||
|
if (dbRefCount.decrementAndGet() == 0) {
|
||||||
|
dbManager.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
||||||
|
try {
|
||||||
|
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||||
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
|
ComputerDetails newDetails = http.getComputerDetails();
|
||||||
|
|
||||||
|
// Check if this is the PC we expected
|
||||||
|
if (details.uuid != null && newDetails.uuid != null &&
|
||||||
|
!details.uuid.equals(newDetails.uuid)) {
|
||||||
|
// We got the wrong PC!
|
||||||
|
LimeLog.info("Polling returned the wrong PC!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDetails;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
||||||
|
ComputerDetails polledDetails;
|
||||||
|
|
||||||
|
// If the local address is routable across the Internet,
|
||||||
|
// always consider this PC remote to be conservative
|
||||||
|
if (details.localIp.equals(details.remoteIp)) {
|
||||||
|
localFirst = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localFirst) {
|
||||||
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails = tryPollIp(details, details.remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||||
|
// Failed, so let's try the fallback
|
||||||
|
if (!localFirst) {
|
||||||
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails = tryPollIp(details, details.remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The fallback poll worked
|
||||||
|
if (polledDetails != null) {
|
||||||
|
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||||
|
ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (polledDetails != null) {
|
||||||
|
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||||
|
ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Machine was unreachable both tries
|
||||||
|
if (polledDetails == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, it's reachable
|
||||||
|
details.update(polledDetails);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean doPollMachine(ComputerDetails details) {
|
||||||
|
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||||
|
details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
// Always try local first to avoid potential UDP issues when
|
||||||
|
// attempting to stream via the router's external IP address
|
||||||
|
// behind its NAT
|
||||||
|
return pollComputer(details, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we're already reached a machine via a particular IP address,
|
||||||
|
// always try that one first
|
||||||
|
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
// Bind to the discovery service
|
||||||
|
bindService(new Intent(this, DiscoveryService.class),
|
||||||
|
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
// Lookup or generate this device's UID
|
||||||
|
idManager = new IdentityManager(this);
|
||||||
|
|
||||||
|
// Initialize the DB
|
||||||
|
dbManager = new ComputerDatabaseManager(this);
|
||||||
|
dbRefCount.set(1);
|
||||||
|
|
||||||
|
// Grab known machines into our computer list
|
||||||
|
if (!getLocalDatabaseReference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||||
|
// Add tuples for each computer
|
||||||
|
addTuple(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (discoveryBinder != null) {
|
||||||
|
// Unbind from the discovery service
|
||||||
|
unbindService(discoveryServiceConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
|
||||||
|
|
||||||
|
// Remove the initial DB reference
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollingTuple {
|
||||||
|
public Thread thread;
|
||||||
|
public ComputerDetails computer;
|
||||||
|
|
||||||
|
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.thread = thread;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -54,7 +54,7 @@ public class IdentityManager {
|
|||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
try {
|
try {
|
||||||
reader.close();
|
reader.close();
|
||||||
} catch (IOException e) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ public class IdentityManager {
|
|||||||
if (writer != null) {
|
if (writer != null) {
|
||||||
try {
|
try {
|
||||||
writer.close();
|
writer.close();
|
||||||
} catch (IOException e) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.koushikdutta.async.future.FutureCallback;
|
||||||
|
import com.koushikdutta.ion.Ion;
|
||||||
|
import com.limelight.AppView;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.KeyManager;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509KeyManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
|
|
||||||
|
private InetAddress address;
|
||||||
|
private String uniqueId;
|
||||||
|
private LimelightCryptoProvider cryptoProvider;
|
||||||
|
private SSLContext sslContext;
|
||||||
|
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
|
||||||
|
|
||||||
|
public AppGridAdapter(Context context, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
||||||
|
super(context, R.layout.app_grid_item, R.drawable.image_loading);
|
||||||
|
|
||||||
|
this.address = address;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
|
||||||
|
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
||||||
|
|
||||||
|
sslContext = SSLContext.getInstance("SSL");
|
||||||
|
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
||||||
|
}
|
||||||
|
|
||||||
|
TrustManager[] trustAllCerts = new TrustManager[] {
|
||||||
|
new X509TrustManager() {
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return new X509Certificate[0];
|
||||||
|
}
|
||||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||||
|
}};
|
||||||
|
|
||||||
|
KeyManager[] ourKeyman = new KeyManager[] {
|
||||||
|
new X509KeyManager() {
|
||||||
|
public String chooseClientAlias(String[] keyTypes,
|
||||||
|
Principal[] issuers, Socket socket) {
|
||||||
|
return "Limelight-RSA";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||||
|
Socket socket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate[] getCertificateChain(String alias) {
|
||||||
|
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateKey getPrivateKey(String alias) {
|
||||||
|
return cryptoProvider.getClientPrivateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ignore differences between given hostname and certificate hostname
|
||||||
|
HostnameVerifier hv = new HostnameVerifier() {
|
||||||
|
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
public void addApp(AppView.AppObject app) {
|
||||||
|
itemList.add(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abortPendingRequests() {
|
||||||
|
HashMap<ImageView, Future> tempMap;
|
||||||
|
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
// Copy the pending requests under a lock
|
||||||
|
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Future f : tempMap.values()) {
|
||||||
|
f.cancel(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
// Remove cancelled requests
|
||||||
|
for (ImageView v : tempMap.keySet()) {
|
||||||
|
pendingRequests.remove(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateImageView(final ImageView imgView, AppView.AppObject obj) {
|
||||||
|
|
||||||
|
// Set SSL contexts correctly to allow us to authenticate
|
||||||
|
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
|
||||||
|
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
|
||||||
|
|
||||||
|
// Set off the deferred image load
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
Future f = Ion.with(imgView)
|
||||||
|
.placeholder(defaultImageRes)
|
||||||
|
.error(defaultImageRes)
|
||||||
|
.load("https://" + address.getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||||
|
obj.app.getAppId() + "&AssetType=2&AssetIdx=0")
|
||||||
|
.setCallback(new FutureCallback<ImageView>() {
|
||||||
|
@Override
|
||||||
|
public void onCompleted(Exception e, ImageView result) {
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
pendingRequests.remove(imgView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pendingRequests.put(imgView, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
|
||||||
|
// Select the text view so it starts marquee mode
|
||||||
|
txtView.setSelected(true);
|
||||||
|
|
||||||
|
// Return false to use the app's toString method
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
||||||
|
if (obj.app.getIsRunning()) {
|
||||||
|
// Show the play button overlay
|
||||||
|
overlayView.setImageResource(R.drawable.play);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No overlay
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.BaseAdapter;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||||
|
protected Context context;
|
||||||
|
protected int defaultImageRes;
|
||||||
|
protected int layoutId;
|
||||||
|
protected ArrayList<T> itemList = new ArrayList<T>();
|
||||||
|
protected LayoutInflater inflater;
|
||||||
|
|
||||||
|
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||||
|
this.context = context;
|
||||||
|
this.layoutId = layoutId;
|
||||||
|
this.defaultImageRes = defaultImageRes;
|
||||||
|
|
||||||
|
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
itemList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return itemList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getItem(int i) {
|
||||||
|
return itemList.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int i) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean populateImageView(ImageView imgView, T obj);
|
||||||
|
public abstract boolean populateTextView(TextView txtView, T obj);
|
||||||
|
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
||||||
|
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
||||||
|
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
||||||
|
|
||||||
|
if (!populateImageView(imgView, itemList.get(i))) {
|
||||||
|
imgView.setImageResource(defaultImageRes);
|
||||||
|
}
|
||||||
|
if (!populateTextView(txtView, itemList.get(i))) {
|
||||||
|
txtView.setText(itemList.get(i).toString());
|
||||||
|
}
|
||||||
|
if (!populateOverlayView(overlayView, itemList.get(i))) {
|
||||||
|
overlayView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.PcView;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||||
|
|
||||||
|
public PcGridAdapter(Context context) {
|
||||||
|
super(context, R.layout.pc_grid_item, R.drawable.computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(PcView.ComputerObject computer) {
|
||||||
|
itemList.add(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeComputer(PcView.ComputerObject computer) {
|
||||||
|
return itemList.remove(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
|
||||||
|
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
imgView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
imgView.setAlpha(0.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false to use the default drawable
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
||||||
|
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
txtView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
txtView.setAlpha(0.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false to use the computer's toString method
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
||||||
|
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
|
// Still refreshing this PC so display the overlay
|
||||||
|
overlayView.setImageResource(R.drawable.image_loading);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No overlay
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-27
@@ -1,11 +1,14 @@
|
|||||||
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 com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
@@ -14,14 +17,12 @@ import android.content.Intent;
|
|||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.View;
|
import android.view.KeyEvent;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
public class AddComputerManually extends Activity {
|
public class AddComputerManually extends Activity {
|
||||||
private Button addPcButton;
|
|
||||||
private TextView hostText;
|
private TextView hostText;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||||
@@ -41,21 +42,27 @@ public class AddComputerManually extends Activity {
|
|||||||
private void doAddPc(String host) {
|
private void doAddPc(String host) {
|
||||||
String msg;
|
String msg;
|
||||||
boolean finish = false;
|
boolean finish = false;
|
||||||
|
|
||||||
|
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||||
|
getResources().getString(R.string.msg_add_pc), false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InetAddress addr = InetAddress.getByName(host);
|
InetAddress addr = InetAddress.getByName(host);
|
||||||
|
|
||||||
if (!managerBinder.addComputerBlocking(addr)){
|
if (!managerBinder.addComputerBlocking(addr)){
|
||||||
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
|
msg = getResources().getString(R.string.addpc_fail);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
msg = "Successfully added computer";
|
msg = getResources().getString(R.string.addpc_success);
|
||||||
finish = true;
|
finish = true;
|
||||||
}
|
}
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
|
msg = getResources().getString(R.string.addpc_unknown_host);
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean toastFinish = finish;
|
dialog.dismiss();
|
||||||
|
|
||||||
|
final boolean toastFinish = finish;
|
||||||
final String toastMsg = msg;
|
final String toastMsg = msg;
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -97,7 +104,7 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
addThread.join();
|
addThread.join();
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
addThread = null;
|
addThread = null;
|
||||||
}
|
}
|
||||||
@@ -108,6 +115,7 @@ public class AddComputerManually extends Activity {
|
|||||||
super.onStop();
|
super.onStop();
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -125,25 +133,32 @@ public class AddComputerManually extends Activity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
setContentView(R.layout.activity_add_computer_manually);
|
setContentView(R.layout.activity_add_computer_manually);
|
||||||
|
|
||||||
this.addPcButton = (Button) findViewById(R.id.addPc);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||||
|
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
|
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE ||
|
||||||
|
(keyEvent != null &&
|
||||||
|
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||||
|
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
||||||
|
if (hostText.getText().length() == 0) {
|
||||||
|
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
computersToAdd.add(hostText.getText().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Bind to the ComputerManager service
|
// Bind to the ComputerManager service
|
||||||
bindService(new Intent(AddComputerManually.this,
|
bindService(new Intent(AddComputerManually.this,
|
||||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
addPcButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (hostText.getText().length() == 0) {
|
|
||||||
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
|
|
||||||
computersToAdd.add(hostText.getText().toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
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 String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
||||||
|
|
||||||
|
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 = 20;
|
||||||
|
|
||||||
|
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;
|
||||||
|
private static final int DEFAULT_DEADZONE = 15;
|
||||||
|
|
||||||
|
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 int deadzonePercentage;
|
||||||
|
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);
|
||||||
|
|
||||||
|
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||||
|
|
||||||
|
// 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,165 @@
|
|||||||
|
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 valueText;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
private String dialogMessage, suffix;
|
||||||
|
private int defaultValue, maxValue, minValue, 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, min, and max seekbar values
|
||||||
|
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||||
|
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
||||||
|
minValue = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View onCreateDialogView() {
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params;
|
||||||
|
LinearLayout layout = new LinearLayout(context);
|
||||||
|
layout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
layout.setPadding(6, 6, 6, 6);
|
||||||
|
|
||||||
|
TextView 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.MATCH_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) {
|
||||||
|
if (value < minValue) {
|
||||||
|
seekBar.setProgress(minValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String t = String.valueOf(value);
|
||||||
|
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_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(seekBar.getProgress());
|
||||||
|
}
|
||||||
|
|
||||||
|
getDialog().dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-1
@@ -14,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)
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.UiModeManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
public class UiHelper {
|
||||||
|
|
||||||
|
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||||
|
private static final int TV_VERTICAL_PADDING_DP = 27;
|
||||||
|
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
||||||
|
|
||||||
|
public static void notifyNewRootView(Activity activity)
|
||||||
|
{
|
||||||
|
View rootView = activity.findViewById(android.R.id.content);
|
||||||
|
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
||||||
|
|
||||||
|
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
|
||||||
|
{
|
||||||
|
// Increase view padding on TVs
|
||||||
|
float scale = activity.getResources().getDisplayMetrics().density;
|
||||||
|
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
|
||||||
|
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
|
||||||
|
|
||||||
|
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||||
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,5 +8,6 @@ LOCAL_PATH := $(MY_LOCAL_PATH)
|
|||||||
include $(CLEAR_VARS)
|
include $(CLEAR_VARS)
|
||||||
LOCAL_MODULE := evdev_reader
|
LOCAL_MODULE := evdev_reader
|
||||||
LOCAL_SRC_FILES := evdev_reader.c
|
LOCAL_SRC_FILES := evdev_reader.c
|
||||||
|
LOCAL_LDLIBS := -llog
|
||||||
|
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <linux/input.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
|
||||||
|
const char *path;
|
||||||
|
|
||||||
|
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
|
||||||
|
|
||||||
|
return open(path, O_RDWR);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
|
||||||
|
return ioctl(fd, EVIOCGRAB, 1) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
|
||||||
|
return ioctl(fd, EVIOCGRAB, 0) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// has*() and friends are based on Android's EventHub.cpp
|
||||||
|
|
||||||
|
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
||||||
|
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
||||||
|
|
||||||
|
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
||||||
|
|
||||||
|
return test_bit(axis, relBitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasAbsAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
||||||
|
unsigned char absBitmask[(ABS_MAX + 1) / 8];
|
||||||
|
|
||||||
|
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask);
|
||||||
|
|
||||||
|
return test_bit(axis, absBitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject this, jint fd, jshort key) {
|
||||||
|
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
|
||||||
|
|
||||||
|
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
||||||
|
|
||||||
|
return test_bit(key, keyBitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
|
||||||
|
jint ret;
|
||||||
|
jbyte *data;
|
||||||
|
int pollres;
|
||||||
|
struct pollfd pollinfo;
|
||||||
|
|
||||||
|
data = (*env)->GetByteArrayElements(env, buffer, NULL);
|
||||||
|
if (data == NULL) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"Failed to get byte array");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Unwait every 250 ms to return to caller if the fd is closed
|
||||||
|
pollinfo.fd = fd;
|
||||||
|
pollinfo.events = POLLIN;
|
||||||
|
pollinfo.revents = 0;
|
||||||
|
pollres = poll(&pollinfo, 1, 250);
|
||||||
|
}
|
||||||
|
while (pollres == 0);
|
||||||
|
|
||||||
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
|
// We'll have data available now
|
||||||
|
ret = read(fd, data, sizeof(struct input_event));
|
||||||
|
if (ret < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"read() failed: %d", errno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// There must have been a failure
|
||||||
|
ret = -1;
|
||||||
|
|
||||||
|
if (pollres < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"poll() failed: %d", errno);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"Unexpected revents: %d", pollinfo.revents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
|
||||||
|
return close(fd);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user