Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65b492f581 |
@@ -0,0 +1,9 @@
|
|||||||
|
<?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>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
|
||||||
|
|
||||||
daysUntilClose: 7
|
|
||||||
responseRequiredLabel: 'need more info'
|
|
||||||
closeComment: >
|
|
||||||
This issue has been automatically closed because there was no response to a
|
|
||||||
request for more information from the issue opener. Please leave a comment or
|
|
||||||
open a new issue if you have additional information related to this issue.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# ProBot Stale (https://probot.github.io/apps/stale/)
|
|
||||||
|
|
||||||
daysUntilStale: 90
|
|
||||||
daysUntilClose: 7
|
|
||||||
exemptLabels:
|
|
||||||
- accepted
|
|
||||||
- bug
|
|
||||||
- enhancement
|
|
||||||
- meta
|
|
||||||
staleLabel: stale
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had
|
|
||||||
recent activity. It will be closed if no further activity occurs.
|
|
||||||
closeComment: false
|
|
||||||
+1
-40
@@ -1,40 +1 @@
|
|||||||
# built application files
|
/bin/*
|
||||||
*.apk
|
|
||||||
*.ap_
|
|
||||||
*.aab
|
|
||||||
output.json
|
|
||||||
out/
|
|
||||||
|
|
||||||
# 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/
|
|
||||||
*.iml
|
|
||||||
|
|
||||||
# Compiled JNI libraries folder
|
|
||||||
**/jniLibs
|
|
||||||
app/.externalNativeBuild/
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
[submodule "moonlight-common"]
|
|
||||||
path = moonlight-common
|
|
||||||
url = https://github.com/moonlight-stream/moonlight-common.git
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*** SESSION Sep 21, 2013 18:55:11.17 -------------------------------------------
|
||||||
|
*** SESSION Sep 21, 2013 18:55:55.08 -------------------------------------------
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
eclipse.preferences.version=1
|
||||||
|
spelling_locale_initialized=true
|
||||||
|
useAnnotationsPrefPage=true
|
||||||
|
useQuickDiffPrefPage=true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
eclipse.preferences.version=1
|
||||||
|
version=1
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
PROBLEMS_FILTERS_MIGRATE=true
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
platformState=1379804095671
|
||||||
|
quickStart=false
|
||||||
|
tipsAndTricks=true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
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.
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<typeInfoHistroy/>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<qualifiedTypeNameHistroy/>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
org.eclipse.core.runtime=1
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.limelight"
|
||||||
|
android:versionCode="18"
|
||||||
|
android:versionName="2.4" >
|
||||||
|
|
||||||
|
<uses-sdk
|
||||||
|
android:minSdkVersion="16"
|
||||||
|
android:targetSdkVersion="19" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/AppTheme" >
|
||||||
|
<activity
|
||||||
|
android:name="com.limelight.Connection"
|
||||||
|
android:label="@string/app_name" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="com.limelight.Game"
|
||||||
|
android:screenOrientation="sensorLandscape"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
|
android:label="@string/title_activity_game"
|
||||||
|
android:parentActivityName="com.limelight.Connection"
|
||||||
|
android:theme="@style/FullscreenTheme" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.limelight.Connection" />
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -20,7 +20,7 @@ function p_h264raw.dissector(buf, pkt, root)
|
|||||||
|
|
||||||
local i = 0
|
local i = 0
|
||||||
local data_start = -1
|
local data_start = -1
|
||||||
while i < buf:len() do
|
while i < buf:len do
|
||||||
-- Make sure we have a potential start sequence and type
|
-- Make sure we have a potential start sequence and type
|
||||||
if buf:len() - i < 5 then
|
if buf:len() - i < 5 then
|
||||||
-- We need more data
|
-- We need more data
|
||||||
|
|||||||
@@ -1,42 +1,48 @@
|
|||||||
# Moonlight Android
|
#Limelight
|
||||||
|
|
||||||
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software, and created a version that can be run on any Android device.
|
||||||
|
|
||||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
Limelight will allow you to stream your full collection of Steam games from your Windows PC to your Android device,
|
||||||
whether in your own home or over the internet.
|
in your own home, or over the internet.
|
||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows Phone](https://github.com/limelight-stream/limelight-wp) are also in development.
|
||||||
|
|
||||||
## Features
|
##Features
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
* Streams Steam and all of your games from your PC to your Android device
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
|
||||||
|
|
||||||
## Installation
|
##Features in development
|
||||||
|
|
||||||
* Download and install Moonlight for Android from
|
* Use mDNS to scan for compatible GeForce Experience (GFE) machines on the network
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
* Choose from the list of available games instead of just launching Steam
|
||||||
|
* Keyboard input
|
||||||
|
|
||||||
|
##Installation
|
||||||
|
|
||||||
|
* Download and install Limelight for Android from
|
||||||
|
[XDA](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||||
|
|
||||||
## Requirements
|
##Requirements
|
||||||
|
|
||||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
* [GFE 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
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
* Turn on Shield Streaming in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
* In Limelight, enter your PC's IP or Hostname and click "Pair"
|
||||||
* Accept the pairing confirmation on your PC and add the PIN if needed
|
* Accept the pairing confirmation on your PC
|
||||||
* Tap your PC again to view the list of apps to stream
|
* In Limelight, click "Start Streaming"
|
||||||
* Play games!
|
* Play games!
|
||||||
|
|
||||||
## Contribute
|
##Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
|
|
||||||
@@ -44,18 +50,14 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
## Building
|
Check out our [website](http://limelight-stream.com) for project links and information.
|
||||||
* Install Android Studio and the Android NDK
|
|
||||||
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
|
||||||
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
|
||||||
* Build the APK using Android Studio
|
|
||||||
|
|
||||||
## Authors
|
##Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
||||||
* [Aaron Neyer](https://github.com/Aaronneyer)
|
* [Aaron Neyer](https://github.com/Aaronneyer)
|
||||||
* [Andrew Hennessy](https://github.com/yetanothername)
|
* [Andrew Hennessy](https://github.com/yetanothername)
|
||||||
|
|
||||||
Moonlight is the work of students at [Case Western](http://case.edu) and was
|
Limelight is the work of students at [Case Western](http://case.edu) and was
|
||||||
started as a project at [MHacks](http://mhacks.org).
|
started as a project at [MHacks](http://mhacks.org).
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
android {
|
|
||||||
buildToolsVersion '28.0.3'
|
|
||||||
compileSdkVersion 28
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdkVersion 16
|
|
||||||
targetSdkVersion 28
|
|
||||||
|
|
||||||
versionName "7.2"
|
|
||||||
versionCode = 191
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "root"
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
root {
|
|
||||||
// Android O has native mouse capture, so don't show the rooted
|
|
||||||
// version to devices running O on the Play Store.
|
|
||||||
maxSdkVersion 25
|
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
ndkBuild {
|
|
||||||
arguments "PRODUCT_FLAVOR=root"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationId "com.limelight.root"
|
|
||||||
dimension "root"
|
|
||||||
}
|
|
||||||
|
|
||||||
nonRoot {
|
|
||||||
externalNativeBuild {
|
|
||||||
ndkBuild {
|
|
||||||
arguments "PRODUCT_FLAVOR=nonRoot"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationId "com.limelight"
|
|
||||||
dimension "root"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
disable 'MissingTranslation'
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle {
|
|
||||||
language {
|
|
||||||
// Avoid splitting by language, since we allow users
|
|
||||||
// to manually switch language in settings.
|
|
||||||
enableSplit = false
|
|
||||||
}
|
|
||||||
density {
|
|
||||||
// FIXME: This should not be neccessary but we get
|
|
||||||
// weird crashes due to missing drawable resources
|
|
||||||
// when this split is enabled.
|
|
||||||
enableSplit = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
|
|
||||||
minifyEnabled true
|
|
||||||
useProguard false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
// To whomever is releasing/using an APK in release mode with
|
|
||||||
// Moonlight's official application ID, please stop. I see every
|
|
||||||
// single one of your crashes in my Play Console and it makes
|
|
||||||
// Moonlight's reliability look worse and makes it more difficult
|
|
||||||
// to distinguish real crashes from your crashy VR app. Seriously,
|
|
||||||
// 44 of the *same* native crash in 72 hours and a few each of
|
|
||||||
// several other crashes.
|
|
||||||
//
|
|
||||||
// This is technically not your fault. I would have hoped Google
|
|
||||||
// would validate the signature of the APK before attributing
|
|
||||||
// the crash to it. I asked their Play Store support about this
|
|
||||||
// and they said they don't and don't have plans to, so that sucks.
|
|
||||||
//
|
|
||||||
// In any case, it's bad form to release an APK using someone
|
|
||||||
// else's application ID. There is no legitimate reason, that
|
|
||||||
// anyone would need to comment out the following line, except me
|
|
||||||
// when I release an official signed Moonlight build. If you feel
|
|
||||||
// like doing so would solve something, I can tell you it will not.
|
|
||||||
// You can't upgrade an app while retaining data without having the
|
|
||||||
// same signature as the official version. Nor can you post it on
|
|
||||||
// the Play Store, since that application ID is already taken.
|
|
||||||
// Reputable APK hosting websites similarly validate the signature
|
|
||||||
// is consistent with the Play Store and won't allow an APK that
|
|
||||||
// isn't signed the same as the original.
|
|
||||||
//
|
|
||||||
// I wish any and all people using Moonlight as the basis of other
|
|
||||||
// cool projects the best of luck with their efforts. All I ask
|
|
||||||
// is to please change the applicationId before you publish.
|
|
||||||
//
|
|
||||||
// TL;DR: Leave the following line alone!
|
|
||||||
applicationIdSuffix ".unofficial"
|
|
||||||
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
ndkBuild {
|
|
||||||
path "src/main/jni/Android.mk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
|
||||||
implementation 'org.jcodec:jcodec:0.2.3'
|
|
||||||
|
|
||||||
implementation project(':moonlight-common')
|
|
||||||
}
|
|
||||||
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
# Don't obfuscate code
|
|
||||||
-dontobfuscate
|
|
||||||
|
|
||||||
# Our code
|
|
||||||
-keep class com.limelight.binding.input.evdev.* {*;}
|
|
||||||
|
|
||||||
# Moonlight common
|
|
||||||
-keep class com.limelight.nvstream.jni.* {*;}
|
|
||||||
|
|
||||||
# Okio
|
|
||||||
-keep class sun.misc.Unsafe {*;}
|
|
||||||
-dontwarn java.nio.file.*
|
|
||||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
|
||||||
-dontwarn okio.**
|
|
||||||
|
|
||||||
# BouncyCastle
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.* {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.util.* {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.rsa.* {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.digest.** {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.symmetric.** {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.spec.* {*;}
|
|
||||||
-keep class org.bouncycastle.jce.** {*;}
|
|
||||||
-dontwarn javax.naming.**
|
|
||||||
|
|
||||||
# jMDNS
|
|
||||||
-dontwarn javax.jmdns.impl.DNSCache
|
|
||||||
-dontwarn org.slf4j.**
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="app_label" translatable="false">Moonlight (Debug)</string>
|
|
||||||
<string name="app_label_root" translatable="false">Moonlight (Root Debug)</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.limelight">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
||||||
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.touchscreen"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.wifi"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.gamepad"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.usb.host"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.software.leanback"
|
|
||||||
android:required="false" />
|
|
||||||
|
|
||||||
<!-- Disable legacy input emulation on ChromeOS -->
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.type.pc"
|
|
||||||
android:required="false"/>
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:isGame="true"
|
|
||||||
android:banner="@drawable/atv_banner"
|
|
||||||
android:appCategory="game"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
|
||||||
android:theme="@style/AppTheme">
|
|
||||||
|
|
||||||
<!-- Samsung multi-window support -->
|
|
||||||
<uses-library
|
|
||||||
android:name="com.sec.android.app.multiwindow"
|
|
||||||
android:required="false" />
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.sec.android.support.multiwindow"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<!-- Samsung DeX support requires explicit placement of android:resizeableActivity="true"
|
|
||||||
in each activity even though it is implied by targeting API 24+ -->
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".PcView"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
<category android:name="tv.ouya.intent.category.APP" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
|
||||||
<activity
|
|
||||||
android:name=".ShortcutTrampoline"
|
|
||||||
android:noHistory="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="com.limelight.PcView" />
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".AppView"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="com.limelight.PcView" />
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".preferences.StreamSettings"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:label="Streaming Settings">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="com.limelight.PcView" />
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".preferences.AddComputerManually"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:label="Add Computer Manually">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="com.limelight.PcView" />
|
|
||||||
</activity>
|
|
||||||
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
|
|
||||||
<activity
|
|
||||||
android:name=".Game"
|
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
|
||||||
android:screenOrientation="userLandscape"
|
|
||||||
android:noHistory="true"
|
|
||||||
android:supportsPictureInPicture="true"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:theme="@style/StreamTheme">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="com.limelight.AppView" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".discovery.DiscoveryService"
|
|
||||||
android:label="mDNS PC Auto-Discovery Service" />
|
|
||||||
<service
|
|
||||||
android:name=".computers.ComputerManagerService"
|
|
||||||
android:label="Computer Management Service" />
|
|
||||||
<service
|
|
||||||
android:name=".binding.input.driver.UsbDriverService"
|
|
||||||
android:label="Usb Driver Service" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".HelpActivity"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="com.limelight.PcView" />
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
@@ -1,581 +0,0 @@
|
|||||||
package com.limelight;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringReader;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
|
||||||
import com.limelight.grid.AppGridAdapter;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
import com.limelight.ui.AdapterFragment;
|
|
||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
|
||||||
import com.limelight.utils.CacheHelper;
|
|
||||||
import com.limelight.utils.Dialog;
|
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
import com.limelight.utils.ShortcutHelper;
|
|
||||||
import com.limelight.utils.SpinnerDialog;
|
|
||||||
import com.limelight.utils.UiHelper;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.view.ContextMenu;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|
||||||
private AppGridAdapter appGridAdapter;
|
|
||||||
private String uuidString;
|
|
||||||
private ShortcutHelper shortcutHelper;
|
|
||||||
|
|
||||||
private ComputerDetails computer;
|
|
||||||
private ComputerManagerService.ApplistPoller poller;
|
|
||||||
private SpinnerDialog blockingLoadSpinner;
|
|
||||||
private String lastRawApplist;
|
|
||||||
private int lastRunningAppId;
|
|
||||||
private boolean suspendGridUpdates;
|
|
||||||
private boolean inForeground;
|
|
||||||
|
|
||||||
private final static int START_OR_RESUME_ID = 1;
|
|
||||||
private final static int QUIT_ID = 2;
|
|
||||||
private final static int CANCEL_ID = 3;
|
|
||||||
private final static int START_WITH_QUIT = 4;
|
|
||||||
private final static int VIEW_DETAILS_ID = 5;
|
|
||||||
private final static int CREATE_SHORTCUT_ID = 6;
|
|
||||||
|
|
||||||
public final static String NAME_EXTRA = "Name";
|
|
||||||
public final static String UUID_EXTRA = "UUID";
|
|
||||||
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
|
||||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
|
||||||
|
|
||||||
// Wait in a separate thread to avoid stalling the UI
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Wait for the binder to be ready
|
|
||||||
localBinder.waitForReady();
|
|
||||||
|
|
||||||
// Get the computer object
|
|
||||||
computer = localBinder.getComputer(uuidString);
|
|
||||||
if (computer == null) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
|
||||||
computer, localBinder.getUniqueId());
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now make the binder visible. We must do this after appGridAdapter
|
|
||||||
// is set to prevent us from reaching updateUiWithServerinfo() and
|
|
||||||
// touching the appGridAdapter prior to initialization.
|
|
||||||
managerBinder = localBinder;
|
|
||||||
|
|
||||||
// Load the app grid with cached data (if possible).
|
|
||||||
// This must be done _before_ startComputerUpdates()
|
|
||||||
// so the initial serverinfo response can update the running
|
|
||||||
// icon.
|
|
||||||
populateAppGridWithCache();
|
|
||||||
|
|
||||||
// Start updates
|
|
||||||
startComputerUpdates();
|
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (isFinishing() || isChangingConfigurations()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Despite my best efforts to catch all conditions that could
|
|
||||||
// cause the activity to be destroyed when we try to commit
|
|
||||||
// I haven't been able to, so we have this try-catch block.
|
|
||||||
try {
|
|
||||||
getFragmentManager().beginTransaction()
|
|
||||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
|
||||||
.commitAllowingStateLoss();
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onServiceDisconnected(ComponentName className) {
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
|
||||||
// Don't start polling if we're not bound or in the foreground
|
|
||||||
if (managerBinder == null || !inForeground) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
|
||||||
@Override
|
|
||||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
|
||||||
// Do nothing if updates are suspended
|
|
||||||
if (suspendGridUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't care about other computers
|
|
||||||
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (details.state == ComputerDetails.State.OFFLINE) {
|
|
||||||
// The PC is unreachable now
|
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Display a toast to the user and quit the activity
|
|
||||||
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close immediately if the PC is no longer paired
|
|
||||||
if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) {
|
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Disable shortcuts referencing this PC for now
|
|
||||||
shortcutHelper.disableShortcut(details.uuid,
|
|
||||||
getResources().getString(R.string.scut_not_paired));
|
|
||||||
|
|
||||||
// Display a toast to the user and quit the activity
|
|
||||||
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// App list is the same or empty
|
|
||||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
|
||||||
|
|
||||||
// Let's check if the running app ID changed
|
|
||||||
if (details.runningGameId != lastRunningAppId) {
|
|
||||||
// Update the currently running game using the app ID
|
|
||||||
lastRunningAppId = details.runningGameId;
|
|
||||||
updateUiWithServerinfo(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastRunningAppId = details.runningGameId;
|
|
||||||
lastRawApplist = details.rawAppList;
|
|
||||||
|
|
||||||
try {
|
|
||||||
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
|
||||||
updateUiWithServerinfo(details);
|
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
|
||||||
blockingLoadSpinner.dismiss();
|
|
||||||
blockingLoadSpinner = null;
|
|
||||||
}
|
|
||||||
} catch (XmlPullParserException | IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (poller == null) {
|
|
||||||
poller = managerBinder.createAppListPoller(computer);
|
|
||||||
}
|
|
||||||
poller.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopComputerUpdates() {
|
|
||||||
if (poller != null) {
|
|
||||||
poller.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
managerBinder.stopPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appGridAdapter != null) {
|
|
||||||
appGridAdapter.cancelQueuedOperations();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// Assume we're in the foreground when created to avoid a race
|
|
||||||
// between binding to CMS and onResume()
|
|
||||||
inForeground = true;
|
|
||||||
|
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_app_view);
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
|
||||||
|
|
||||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
|
||||||
|
|
||||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
|
||||||
|
|
||||||
TextView label = findViewById(R.id.appListText);
|
|
||||||
setTitle(computerName);
|
|
||||||
label.setText(computerName);
|
|
||||||
|
|
||||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
|
||||||
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
|
||||||
shortcutHelper.reportShortcutUsed(uuidString);
|
|
||||||
|
|
||||||
// Bind to the computer manager service
|
|
||||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
|
||||||
Service.BIND_AUTO_CREATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void populateAppGridWithCache() {
|
|
||||||
try {
|
|
||||||
// Try to load from cache
|
|
||||||
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
|
|
||||||
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
|
||||||
updateUiWithAppList(applist);
|
|
||||||
LimeLog.info("Loaded applist from cache");
|
|
||||||
} catch (IOException | XmlPullParserException e) {
|
|
||||||
if (lastRawApplist != null) {
|
|
||||||
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
LimeLog.info("Loading applist from the network");
|
|
||||||
// We'll need to load from the network
|
|
||||||
loadAppsBlocking();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadAppsBlocking() {
|
|
||||||
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
|
|
||||||
getResources().getString(R.string.applist_refresh_msg), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
|
|
||||||
SpinnerDialog.closeDialogs(this);
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
// Display a decoder crash notification if we've returned after a crash
|
|
||||||
UiHelper.showDecoderCrashDialog(this);
|
|
||||||
|
|
||||||
inForeground = true;
|
|
||||||
startComputerUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
|
|
||||||
inForeground = false;
|
|
||||||
stopComputerUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
|
||||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
|
||||||
if (lastRunningAppId != 0) {
|
|
||||||
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
|
||||||
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
|
||||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
|
||||||
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
// Only add an option to create shortcut if box art is loaded
|
|
||||||
// and when we're in grid-mode (not list-mode).
|
|
||||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
|
||||||
if (appImageView != null) {
|
|
||||||
// We have a grid ImageView, so we must be in grid-mode
|
|
||||||
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
|
||||||
if (drawable != null && drawable.getBitmap() != null) {
|
|
||||||
// We have a bitmap loaded too
|
|
||||||
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onContextMenuClosed(Menu menu) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
|
||||||
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case START_WITH_QUIT:
|
|
||||||
// Display a confirmation dialog first
|
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
|
||||||
}
|
|
||||||
}, null);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case START_OR_RESUME_ID:
|
|
||||||
// Resume is the same as start for us
|
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case QUIT_ID:
|
|
||||||
// Display a confirmation dialog first
|
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
suspendGridUpdates = true;
|
|
||||||
ServerHelper.doQuit(AppView.this, computer,
|
|
||||||
app.app, managerBinder, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Trigger a poll immediately
|
|
||||||
suspendGridUpdates = false;
|
|
||||||
if (poller != null) {
|
|
||||||
poller.pollNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, null);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case CANCEL_ID:
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case VIEW_DETAILS_ID:
|
|
||||||
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
|
|
||||||
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case CREATE_SHORTCUT_ID:
|
|
||||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
|
||||||
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
|
|
||||||
if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
|
|
||||||
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return super.onContextItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUiWithServerinfo(final ComputerDetails details) {
|
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean updated = false;
|
|
||||||
|
|
||||||
// Look through our current app list to tag the running app
|
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
|
||||||
|
|
||||||
// There can only be one or zero apps running.
|
|
||||||
if (existingApp.isRunning &&
|
|
||||||
existingApp.app.getAppId() == details.runningGameId) {
|
|
||||||
// This app was running and still is, so we're done now
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (existingApp.app.getAppId() == details.runningGameId) {
|
|
||||||
// This app wasn't running but now is
|
|
||||||
existingApp.isRunning = true;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
else if (existingApp.isRunning) {
|
|
||||||
// This app was running but now isn't
|
|
||||||
existingApp.isRunning = false;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// This app wasn't running and still isn't
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
appGridAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUiWithAppList(final List<NvApp> appList) {
|
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean updated = false;
|
|
||||||
|
|
||||||
// First handle app updates and additions
|
|
||||||
for (NvApp app : appList) {
|
|
||||||
boolean foundExistingApp = false;
|
|
||||||
|
|
||||||
// Try to update an existing app in the list first
|
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
|
||||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
|
||||||
// Found the app; update its properties
|
|
||||||
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
|
||||||
existingApp.app.setAppName(app.getAppName());
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
foundExistingApp = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundExistingApp) {
|
|
||||||
// This app must be new
|
|
||||||
appGridAdapter.addApp(new AppObject(app));
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next handle app removals
|
|
||||||
int i = 0;
|
|
||||||
while (i < appGridAdapter.getCount()) {
|
|
||||||
boolean foundExistingApp = false;
|
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
|
||||||
|
|
||||||
// Check if this app is in the latest list
|
|
||||||
for (NvApp app : appList) {
|
|
||||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
|
||||||
foundExistingApp = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This app was removed in the latest app list
|
|
||||||
if (!foundExistingApp) {
|
|
||||||
appGridAdapter.removeApp(existingApp);
|
|
||||||
updated = true;
|
|
||||||
|
|
||||||
// Check this same index again because the item at i+1 is now at i after
|
|
||||||
// the removal
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move on to the next item
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
appGridAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getAdapterFragmentLayoutId() {
|
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
|
||||||
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
|
||||||
R.layout.app_grid_view_small : R.layout.app_grid_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void receiveAbsListView(AbsListView listView) {
|
|
||||||
listView.setAdapter(appGridAdapter);
|
|
||||||
listView.setOnItemClickListener(new OnItemClickListener() {
|
|
||||||
@Override
|
|
||||||
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 (lastRunningAppId != 0) {
|
|
||||||
openContextMenu(arg1);
|
|
||||||
} else {
|
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerForContextMenu(listView);
|
|
||||||
listView.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AppObject {
|
|
||||||
public final NvApp app;
|
|
||||||
public boolean isRunning;
|
|
||||||
|
|
||||||
public AppObject(NvApp app) {
|
|
||||||
if (app == null) {
|
|
||||||
throw new IllegalArgumentException("app must not be null");
|
|
||||||
}
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return app.getAppName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
|||||||
package com.limelight;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.webkit.WebView;
|
|
||||||
import android.webkit.WebViewClient;
|
|
||||||
|
|
||||||
import com.limelight.utils.SpinnerDialog;
|
|
||||||
|
|
||||||
public class HelpActivity extends Activity {
|
|
||||||
|
|
||||||
private SpinnerDialog loadingDialog;
|
|
||||||
private WebView webView;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
webView = new WebView(this);
|
|
||||||
setContentView(webView);
|
|
||||||
|
|
||||||
// These allow the user to zoom the page
|
|
||||||
webView.getSettings().setBuiltInZoomControls(true);
|
|
||||||
webView.getSettings().setDisplayZoomControls(false);
|
|
||||||
|
|
||||||
// This sets the view to display the whole page by default
|
|
||||||
webView.getSettings().setUseWideViewPort(true);
|
|
||||||
webView.getSettings().setLoadWithOverviewMode(true);
|
|
||||||
|
|
||||||
// This allows the links to places on the same page to work
|
|
||||||
webView.getSettings().setJavaScriptEnabled(true);
|
|
||||||
|
|
||||||
webView.setWebViewClient(new WebViewClient() {
|
|
||||||
@Override
|
|
||||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
|
||||||
if (loadingDialog == null) {
|
|
||||||
loadingDialog = SpinnerDialog.displayDialog(HelpActivity.this,
|
|
||||||
getResources().getString(R.string.help_loading_title),
|
|
||||||
getResources().getString(R.string.help_loading_msg), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageFinished(WebView view, String url) {
|
|
||||||
if (loadingDialog != null) {
|
|
||||||
loadingDialog.dismiss();
|
|
||||||
loadingDialog = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
|
||||||
return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
|
||||||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
webView.loadUrl(getIntent().getData().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
// Back goes back through the WebView history
|
|
||||||
// until no more history remains
|
|
||||||
if (webView.canGoBack()) {
|
|
||||||
webView.goBack();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,732 +0,0 @@
|
|||||||
package com.limelight;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
|
||||||
import com.limelight.grid.PcGridAdapter;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
|
||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
|
||||||
import com.limelight.nvstream.wol.WakeOnLanSender;
|
|
||||||
import com.limelight.preferences.AddComputerManually;
|
|
||||||
import com.limelight.preferences.GlPreferences;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
import com.limelight.preferences.StreamSettings;
|
|
||||||
import com.limelight.ui.AdapterFragment;
|
|
||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
|
||||||
import com.limelight.utils.Dialog;
|
|
||||||
import com.limelight.utils.HelpLauncher;
|
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
import com.limelight.utils.ShortcutHelper;
|
|
||||||
import com.limelight.utils.UiHelper;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.opengl.GLSurfaceView;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.ContextMenu;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig;
|
|
||||||
import javax.microedition.khronos.opengles.GL10;
|
|
||||||
|
|
||||||
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|
||||||
private RelativeLayout noPcFoundLayout;
|
|
||||||
private PcGridAdapter pcGridAdapter;
|
|
||||||
private ShortcutHelper shortcutHelper;
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
|
||||||
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
|
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
|
||||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
|
||||||
|
|
||||||
// Wait in a separate thread to avoid stalling the UI
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Wait for the binder to be ready
|
|
||||||
localBinder.waitForReady();
|
|
||||||
|
|
||||||
// Now make the binder visible
|
|
||||||
managerBinder = localBinder;
|
|
||||||
|
|
||||||
// Start updates
|
|
||||||
startComputerUpdates();
|
|
||||||
|
|
||||||
// Force a keypair to be generated early to avoid discovery delays
|
|
||||||
new AndroidCryptoProvider(PcView.this).getClientCertificate();
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onServiceDisconnected(ComponentName className) {
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
|
||||||
super.onConfigurationChanged(newConfig);
|
|
||||||
|
|
||||||
// Only reinitialize views if completeOnCreate() was called
|
|
||||||
// before this callback. If it was not, completeOnCreate() will
|
|
||||||
// handle initializing views with the config change accounted for.
|
|
||||||
// This is not prone to races because both callbacks are invoked
|
|
||||||
// in the main thread.
|
|
||||||
if (completeOnCreateCalled) {
|
|
||||||
// Reinitialize views just in case orientation changed
|
|
||||||
initializeViews();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final static int APP_LIST_ID = 1;
|
|
||||||
private final static int PAIR_ID = 2;
|
|
||||||
private final static int UNPAIR_ID = 3;
|
|
||||||
private final static int WOL_ID = 4;
|
|
||||||
private final static int DELETE_ID = 5;
|
|
||||||
private final static int RESUME_ID = 6;
|
|
||||||
private final static int QUIT_ID = 7;
|
|
||||||
private final static int VIEW_DETAILS_ID = 8;
|
|
||||||
|
|
||||||
private void initializeViews() {
|
|
||||||
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
|
|
||||||
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
|
||||||
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
|
||||||
ImageButton helpButton = findViewById(R.id.helpButton);
|
|
||||||
|
|
||||||
settingsButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
startActivity(new Intent(PcView.this, StreamSettings.class));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addComputerButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
Intent i = new Intent(PcView.this, AddComputerManually.class);
|
|
||||||
startActivity(i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
helpButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
HelpLauncher.launchSetupGuide(PcView.this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
getFragmentManager().beginTransaction()
|
|
||||||
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
|
||||||
.commitAllowingStateLoss();
|
|
||||||
|
|
||||||
noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
|
|
||||||
if (pcGridAdapter.getCount() == 0) {
|
|
||||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
noPcFoundLayout.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
pcGridAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// Assume we're in the foreground when created to avoid a race
|
|
||||||
// between binding to CMS and onResume()
|
|
||||||
inForeground = true;
|
|
||||||
|
|
||||||
// Create a GLSurfaceView to fetch GLRenderer unless we have
|
|
||||||
// a cached result already.
|
|
||||||
final GlPreferences glPrefs = GlPreferences.readPreferences(this);
|
|
||||||
if (!glPrefs.savedFingerprint.equals(Build.FINGERPRINT) || glPrefs.glRenderer.isEmpty()) {
|
|
||||||
GLSurfaceView surfaceView = new GLSurfaceView(this);
|
|
||||||
surfaceView.setRenderer(new GLSurfaceView.Renderer() {
|
|
||||||
@Override
|
|
||||||
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
|
|
||||||
// Save the GLRenderer string so we don't need to do this next time
|
|
||||||
glPrefs.glRenderer = gl10.glGetString(GL10.GL_RENDERER);
|
|
||||||
glPrefs.savedFingerprint = Build.FINGERPRINT;
|
|
||||||
glPrefs.writePreferences();
|
|
||||||
|
|
||||||
LimeLog.info("Fetched GL Renderer: " + glPrefs.glRenderer);
|
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
completeOnCreate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDrawFrame(GL10 gl10) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setContentView(surfaceView);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.info("Cached GL Renderer: " + glPrefs.glRenderer);
|
|
||||||
completeOnCreate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void completeOnCreate() {
|
|
||||||
completeOnCreateCalled = true;
|
|
||||||
|
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
|
||||||
|
|
||||||
// Bind to the computer manager service
|
|
||||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
|
||||||
Service.BIND_AUTO_CREATE);
|
|
||||||
|
|
||||||
pcGridAdapter = new PcGridAdapter(this,
|
|
||||||
PreferenceConfiguration.readPreferences(this).listMode,
|
|
||||||
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
|
||||||
|
|
||||||
initializeViews();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
|
||||||
// Only allow polling to start if we're bound to CMS, polling is not already running,
|
|
||||||
// and our activity is in the foreground.
|
|
||||||
if (managerBinder != null && !runningPolling && inForeground) {
|
|
||||||
freezeUpdates = false;
|
|
||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
|
||||||
@Override
|
|
||||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
|
||||||
if (!freezeUpdates) {
|
|
||||||
PcView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
updateComputer(details);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
runningPolling = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopComputerUpdates(boolean wait) {
|
|
||||||
if (managerBinder != null) {
|
|
||||||
if (!runningPolling) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
freezeUpdates = true;
|
|
||||||
|
|
||||||
managerBinder.stopPolling();
|
|
||||||
|
|
||||||
if (wait) {
|
|
||||||
managerBinder.waitForPollingStopped();
|
|
||||||
}
|
|
||||||
|
|
||||||
runningPolling = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
// Display a decoder crash notification if we've returned after a crash
|
|
||||||
UiHelper.showDecoderCrashDialog(this);
|
|
||||||
|
|
||||||
inForeground = true;
|
|
||||||
startComputerUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
|
|
||||||
inForeground = false;
|
|
||||||
stopComputerUpdates(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
|
||||||
stopComputerUpdates(false);
|
|
||||||
|
|
||||||
// Call superclass
|
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
|
||||||
|
|
||||||
// Inflate the context menu
|
|
||||||
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
|
||||||
computer.details.state == ComputerDetails.State.UNKNOWN) {
|
|
||||||
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
|
||||||
else if (computer.details.pairState != PairState.PAIRED) {
|
|
||||||
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
|
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (computer.details.runningGameId != 0) {
|
|
||||||
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
|
||||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
|
|
||||||
|
|
||||||
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
|
|
||||||
// it with delete which actually work
|
|
||||||
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
|
||||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onContextMenuClosed(Menu menu) {
|
|
||||||
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
|
||||||
// startComputerUpdates() manages this and won't actual start polling until the activity
|
|
||||||
// returns to the foreground.
|
|
||||||
startComputerUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
|
||||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (computer.runningGameId != 0) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
NvHTTP httpConn;
|
|
||||||
String message;
|
|
||||||
boolean success = false;
|
|
||||||
try {
|
|
||||||
// Stop updates and wait while pairing
|
|
||||||
stopComputerUpdates(true);
|
|
||||||
|
|
||||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
|
||||||
managerBinder.getUniqueId(),
|
|
||||||
computer.serverCert,
|
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
|
||||||
if (httpConn.getPairState() == PairState.PAIRED) {
|
|
||||||
// Don't display any toast, but open the app list
|
|
||||||
message = null;
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
final String pinStr = PairingManager.generatePinString();
|
|
||||||
|
|
||||||
// Spin the dialog off in a thread because it blocks
|
|
||||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
|
||||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
|
||||||
|
|
||||||
PairingManager pm = httpConn.getPairingManager();
|
|
||||||
|
|
||||||
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
|
|
||||||
if (pairState == PairState.PIN_WRONG) {
|
|
||||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
|
||||||
}
|
|
||||||
else if (pairState == PairState.FAILED) {
|
|
||||||
message = getResources().getString(R.string.pair_fail);
|
|
||||||
}
|
|
||||||
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
|
|
||||||
message = getResources().getString(R.string.pair_already_in_progress);
|
|
||||||
}
|
|
||||||
else if (pairState == PairState.PAIRED) {
|
|
||||||
// Just navigate to the app view without displaying a toast
|
|
||||||
message = null;
|
|
||||||
success = true;
|
|
||||||
|
|
||||||
// Pin this certificate for later HTTPS use
|
|
||||||
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
|
|
||||||
|
|
||||||
// Invalidate reachability information after pairing to force
|
|
||||||
// a refresh before reading pair state again
|
|
||||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should be no other values
|
|
||||||
message = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
message = getResources().getString(R.string.error_unknown_host);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
message = getResources().getString(R.string.error_404);
|
|
||||||
} catch (XmlPullParserException | IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
message = e.getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
|
|
||||||
final String toastMessage = message;
|
|
||||||
final boolean toastSuccess = success;
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (toastMessage != null) {
|
|
||||||
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toastSuccess) {
|
|
||||||
// Open the app list after a successful pairing attempt
|
|
||||||
doAppList(computer);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Start polling again if we're still in the foreground
|
|
||||||
startComputerUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doWakeOnLan(final ComputerDetails computer) {
|
|
||||||
if (computer.state == ComputerDetails.State.ONLINE) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (computer.macAddress == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
String message;
|
|
||||||
try {
|
|
||||||
WakeOnLanSender.sendWolPacket(computer);
|
|
||||||
message = getResources().getString(R.string.wol_waking_msg);
|
|
||||||
} catch (IOException e) {
|
|
||||||
message = getResources().getString(R.string.wol_fail);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String toastMessage = message;
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doUnpair(final ComputerDetails computer) {
|
|
||||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
NvHTTP httpConn;
|
|
||||||
String message;
|
|
||||||
try {
|
|
||||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
|
||||||
managerBinder.getUniqueId(),
|
|
||||||
computer.serverCert,
|
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
|
||||||
httpConn.unpair();
|
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
|
|
||||||
message = getResources().getString(R.string.unpair_success);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = getResources().getString(R.string.unpair_fail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = getResources().getString(R.string.unpair_error);
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
message = getResources().getString(R.string.error_unknown_host);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
message = getResources().getString(R.string.error_404);
|
|
||||||
} catch (XmlPullParserException | IOException e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
final String toastMessage = message;
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doAppList(ComputerDetails computer) {
|
|
||||||
if (computer.state == ComputerDetails.State.OFFLINE) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent i = new Intent(this, AppView.class);
|
|
||||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
|
||||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
|
||||||
startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
|
||||||
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case PAIR_ID:
|
|
||||||
doPair(computer.details);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case UNPAIR_ID:
|
|
||||||
doUnpair(computer.details);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case WOL_ID:
|
|
||||||
doWakeOnLan(computer.details);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case DELETE_ID:
|
|
||||||
if (ActivityManager.isUserAMonkey()) {
|
|
||||||
LimeLog.info("Ignoring delete PC request from monkey");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
UiHelper.displayDeletePcConfirmationDialog(this, computer.details, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
managerBinder.removeComputer(computer.details.name);
|
|
||||||
removeComputer(computer.details);
|
|
||||||
}
|
|
||||||
}, null);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case APP_LIST_ID:
|
|
||||||
doAppList(computer.details);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case RESUME_ID:
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case QUIT_ID:
|
|
||||||
if (managerBinder == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display a confirmation dialog first
|
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ServerHelper.doQuit(PcView.this, computer.details,
|
|
||||||
new NvApp("app", 0, false), managerBinder, null);
|
|
||||||
}
|
|
||||||
}, null);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case VIEW_DETAILS_ID:
|
|
||||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return super.onContextItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeComputer(ComputerDetails details) {
|
|
||||||
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
|
||||||
|
|
||||||
if (details.equals(computer.details)) {
|
|
||||||
// Disable or delete shortcuts referencing this PC
|
|
||||||
shortcutHelper.disableShortcut(details.uuid,
|
|
||||||
getResources().getString(R.string.scut_deleted_pc));
|
|
||||||
|
|
||||||
pcGridAdapter.removeComputer(computer);
|
|
||||||
pcGridAdapter.notifyDataSetChanged();
|
|
||||||
|
|
||||||
if (pcGridAdapter.getCount() == 0) {
|
|
||||||
// Show the "Discovery in progress" view
|
|
||||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateComputer(ComputerDetails details) {
|
|
||||||
ComputerObject existingEntry = null;
|
|
||||||
|
|
||||||
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
|
||||||
|
|
||||||
// Check if this is the same computer
|
|
||||||
if (details.uuid.equals(computer.details.uuid)) {
|
|
||||||
existingEntry = computer;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a launcher shortcut for this PC
|
|
||||||
if (details.pairState == PairState.PAIRED) {
|
|
||||||
shortcutHelper.createAppViewShortcut(details.uuid, details, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingEntry != null) {
|
|
||||||
// Replace the information in the existing entry
|
|
||||||
existingEntry.details = details;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Add a new entry
|
|
||||||
pcGridAdapter.addComputer(new ComputerObject(details));
|
|
||||||
|
|
||||||
// Remove the "Discovery in progress" view
|
|
||||||
noPcFoundLayout.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify the view that the data has changed
|
|
||||||
pcGridAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getAdapterFragmentLayoutId() {
|
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
|
||||||
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
|
|
||||||
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void receiveAbsListView(AbsListView listView) {
|
|
||||||
listView.setAdapter(pcGridAdapter);
|
|
||||||
listView.setOnItemClickListener(new OnItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
|
||||||
long id) {
|
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
|
||||||
if (computer.details.state == ComputerDetails.State.UNKNOWN ||
|
|
||||||
computer.details.state == ComputerDetails.State.OFFLINE) {
|
|
||||||
// Open the context menu if a PC is offline or refreshing
|
|
||||||
openContextMenu(arg1);
|
|
||||||
} else if (computer.details.pairState != PairState.PAIRED) {
|
|
||||||
// Pair an unpaired machine by default
|
|
||||||
doPair(computer.details);
|
|
||||||
} else {
|
|
||||||
doAppList(computer.details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerForContextMenu(listView);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ComputerObject {
|
|
||||||
public ComputerDetails details;
|
|
||||||
|
|
||||||
public ComputerObject(ComputerDetails details) {
|
|
||||||
if (details == null) {
|
|
||||||
throw new IllegalArgumentException("details must not be null");
|
|
||||||
}
|
|
||||||
this.details = details;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return details.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
package com.limelight;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.IBinder;
|
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
|
||||||
import com.limelight.utils.Dialog;
|
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
import com.limelight.utils.SpinnerDialog;
|
|
||||||
import com.limelight.utils.UiHelper;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class ShortcutTrampoline extends Activity {
|
|
||||||
private String uuidString;
|
|
||||||
private String appIdString;
|
|
||||||
private ArrayList<Intent> intentStack = new ArrayList<>();
|
|
||||||
|
|
||||||
private ComputerDetails computer;
|
|
||||||
private SpinnerDialog blockingLoadSpinner;
|
|
||||||
|
|
||||||
public final static String APP_ID_EXTRA = "AppId";
|
|
||||||
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
|
||||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
|
||||||
|
|
||||||
// Wait in a separate thread to avoid stalling the UI
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Wait for the binder to be ready
|
|
||||||
localBinder.waitForReady();
|
|
||||||
|
|
||||||
// Now make the binder visible
|
|
||||||
managerBinder = localBinder;
|
|
||||||
|
|
||||||
// Get the computer object
|
|
||||||
computer = managerBinder.getComputer(uuidString);
|
|
||||||
|
|
||||||
if (computer == null) {
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_pc_not_found),
|
|
||||||
true);
|
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
|
||||||
blockingLoadSpinner.dismiss();
|
|
||||||
blockingLoadSpinner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force CMS to repoll this machine
|
|
||||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
|
||||||
|
|
||||||
// Start polling
|
|
||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
|
||||||
@Override
|
|
||||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
|
||||||
// Don't care about other computers
|
|
||||||
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (details.state != ComputerDetails.State.UNKNOWN) {
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Stop showing the spinner
|
|
||||||
if (blockingLoadSpinner != null) {
|
|
||||||
blockingLoadSpinner.dismiss();
|
|
||||||
blockingLoadSpinner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the managerBinder was destroyed before this callback,
|
|
||||||
// just finish the activity.
|
|
||||||
if (managerBinder == null) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
|
|
||||||
|
|
||||||
// Launch game if provided app ID, otherwise launch app view
|
|
||||||
if (appIdString != null && appIdString.length() > 0) {
|
|
||||||
if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) {
|
|
||||||
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
|
||||||
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder));
|
|
||||||
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
|
|
||||||
// Now start the activities
|
|
||||||
startActivities(intentStack.toArray(new Intent[]{}));
|
|
||||||
} else {
|
|
||||||
// Create the start intent immediately, so we can safely unbind the managerBinder
|
|
||||||
// below before we return.
|
|
||||||
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
|
||||||
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder);
|
|
||||||
|
|
||||||
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
intentStack.add(startIntent);
|
|
||||||
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
|
|
||||||
// Now start the activities
|
|
||||||
startActivities(intentStack.toArray(new Intent[]{}));
|
|
||||||
}
|
|
||||||
}, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
|
|
||||||
// Add the PC view at the back (and clear the task)
|
|
||||||
Intent i;
|
|
||||||
i = new Intent(ShortcutTrampoline.this, PcView.class);
|
|
||||||
i.setAction(Intent.ACTION_MAIN);
|
|
||||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
intentStack.add(i);
|
|
||||||
|
|
||||||
// Take this intent's data and create an intent to start the app view
|
|
||||||
i = new Intent(getIntent());
|
|
||||||
i.setClass(ShortcutTrampoline.this, AppView.class);
|
|
||||||
intentStack.add(i);
|
|
||||||
|
|
||||||
// If a game is running, we'll make the stream the top level activity
|
|
||||||
if (details.runningGameId != 0) {
|
|
||||||
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
|
||||||
new NvApp("app", details.runningGameId, false), details, managerBinder));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now start the activities
|
|
||||||
startActivities(intentStack.toArray(new Intent[]{}));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else if (details.state == ComputerDetails.State.OFFLINE) {
|
|
||||||
// Computer offline - display an error dialog
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.error_pc_offline),
|
|
||||||
true);
|
|
||||||
} else if (details.pairState != PairingManager.PairState.PAIRED) {
|
|
||||||
// Computer not paired - display an error dialog
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_not_paired),
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't want any more callbacks from now on, so go ahead
|
|
||||||
// and unbind from the service
|
|
||||||
if (managerBinder != null) {
|
|
||||||
managerBinder.stopPolling();
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onServiceDisconnected(ComponentName className) {
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected boolean validateInput() {
|
|
||||||
// Validate UUID
|
|
||||||
if (uuidString == null) {
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_invalid_uuid),
|
|
||||||
true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
UUID.fromString(uuidString);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_invalid_uuid),
|
|
||||||
true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate App ID (if provided)
|
|
||||||
if (appIdString != null && !appIdString.isEmpty()) {
|
|
||||||
try {
|
|
||||||
Integer.parseInt(appIdString);
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_invalid_app_id),
|
|
||||||
true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
|
||||||
|
|
||||||
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
|
|
||||||
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
|
|
||||||
|
|
||||||
if (validateInput()) {
|
|
||||||
// Bind to the computer manager service
|
|
||||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
|
||||||
Service.BIND_AUTO_CREATE);
|
|
||||||
|
|
||||||
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
|
|
||||||
getResources().getString(R.string.applist_connect_msg), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
|
||||||
blockingLoadSpinner.dismiss();
|
|
||||||
blockingLoadSpinner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
managerBinder.stopPolling();
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
package com.limelight.binding.audio;
|
|
||||||
|
|
||||||
import android.media.AudioAttributes;
|
|
||||||
import android.media.AudioFormat;
|
|
||||||
import android.media.AudioManager;
|
|
||||||
import android.media.AudioTrack;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
|
||||||
import com.limelight.nvstream.jni.MoonBridge;
|
|
||||||
|
|
||||||
public class AndroidAudioRenderer implements AudioRenderer {
|
|
||||||
|
|
||||||
private AudioTrack track;
|
|
||||||
|
|
||||||
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
|
||||||
48000,
|
|
||||||
channelConfig,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
|
|
||||||
.setUsage(AudioAttributes.USAGE_GAME);
|
|
||||||
AudioFormat format = new AudioFormat.Builder()
|
|
||||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
||||||
.setSampleRate(48000)
|
|
||||||
.setChannelMask(channelConfig)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
// Use FLAG_LOW_LATENCY on L through N
|
|
||||||
if (lowLatency) {
|
|
||||||
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
|
|
||||||
.setAudioFormat(format)
|
|
||||||
.setAudioAttributes(attributesBuilder.build())
|
|
||||||
.setTransferMode(AudioTrack.MODE_STREAM)
|
|
||||||
.setBufferSizeInBytes(bufferSize);
|
|
||||||
|
|
||||||
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
|
|
||||||
if (lowLatency) {
|
|
||||||
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trackBuilder.build();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return new AudioTrack(attributesBuilder.build(),
|
|
||||||
format,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM,
|
|
||||||
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int setup(int audioConfiguration) {
|
|
||||||
int channelConfig;
|
|
||||||
int bytesPerFrame;
|
|
||||||
|
|
||||||
switch (audioConfiguration)
|
|
||||||
{
|
|
||||||
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
|
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
|
||||||
bytesPerFrame = 2 * 240 * 2;
|
|
||||||
break;
|
|
||||||
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
|
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
|
||||||
bytesPerFrame = 6 * 240 * 2;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LimeLog.severe("Decoder returned unhandled channel count");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're not supposed to request less than the minimum
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
boolean lowLatency;
|
|
||||||
int bufferSize;
|
|
||||||
|
|
||||||
// We will try:
|
|
||||||
// 1) Small buffer, low latency mode
|
|
||||||
// 2) Large buffer, low latency mode
|
|
||||||
// 3) Small buffer, standard mode
|
|
||||||
// 4) Large buffer, standard mode
|
|
||||||
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
lowLatency = true;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
lowLatency = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Unreachable
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
case 2:
|
|
||||||
bufferSize = bytesPerFrame * 2;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
case 3:
|
|
||||||
// Try the larger buffer size
|
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
|
||||||
channelConfig,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
|
||||||
bytesPerFrame * 2);
|
|
||||||
|
|
||||||
// Round to next frame
|
|
||||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Unreachable
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip low latency options if hardware sample rate isn't 48000Hz
|
|
||||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
|
||||||
track.play();
|
|
||||||
|
|
||||||
// Successfully created working AudioTrack. We're done here.
|
|
||||||
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Try to release the AudioTrack if we got far enough
|
|
||||||
e.printStackTrace();
|
|
||||||
try {
|
|
||||||
if (track != null) {
|
|
||||||
track.release();
|
|
||||||
track = null;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track == null) {
|
|
||||||
// Couldn't create any audio track for playback
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playDecodedAudio(byte[] audioData) {
|
|
||||||
track.write(audioData, 0, audioData.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void cleanup() {
|
|
||||||
// Immediately drop all pending data
|
|
||||||
track.pause();
|
|
||||||
track.flush();
|
|
||||||
|
|
||||||
track.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
package com.limelight.binding.crypto;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.KeyPair;
|
|
||||||
import java.security.KeyPairGenerator;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.Provider;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name;
|
|
||||||
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
|
||||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
|
||||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
|
||||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
|
||||||
import org.bouncycastle.operator.ContentSigner;
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Base64;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
|
||||||
|
|
||||||
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|
||||||
|
|
||||||
private final File certFile;
|
|
||||||
private final File keyFile;
|
|
||||||
|
|
||||||
private X509Certificate cert;
|
|
||||||
private RSAPrivateKey key;
|
|
||||||
private byte[] pemCertBytes;
|
|
||||||
|
|
||||||
private static final Object globalCryptoLock = new Object();
|
|
||||||
|
|
||||||
private static final Provider bcProvider = new BouncyCastleProvider();
|
|
||||||
|
|
||||||
public AndroidCryptoProvider(Context c) {
|
|
||||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
|
||||||
|
|
||||||
certFile = new File(dataPath + File.separator + "client.crt");
|
|
||||||
keyFile = new File(dataPath + File.separator + "client.key");
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] loadFileToBytes(File f) {
|
|
||||||
if (!f.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
FileInputStream fin = new FileInputStream(f);
|
|
||||||
byte[] fileData = new byte[(int) f.length()];
|
|
||||||
if (fin.read(fileData) != f.length()) {
|
|
||||||
// Failed to read
|
|
||||||
fileData = null;
|
|
||||||
}
|
|
||||||
fin.close();
|
|
||||||
return fileData;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean loadCertKeyPair() {
|
|
||||||
byte[] certBytes = loadFileToBytes(certFile);
|
|
||||||
byte[] keyBytes = loadFileToBytes(keyFile);
|
|
||||||
|
|
||||||
// If either file was missing, we definitely can't succeed
|
|
||||||
if (certBytes == null || keyBytes == null) {
|
|
||||||
LimeLog.info("Missing cert or key; need to generate a new one");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
|
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
|
||||||
pemCertBytes = certBytes;
|
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
|
|
||||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
// May happen if the cert is corrupt
|
|
||||||
LimeLog.warning("Corrupted certificate");
|
|
||||||
return false;
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
// May happen if the key is corrupt
|
|
||||||
LimeLog.warning("Corrupted key");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("TrulyRandom")
|
|
||||||
private boolean generateCertKeyPair() {
|
|
||||||
byte[] snBytes = new byte[8];
|
|
||||||
new SecureRandom().nextBytes(snBytes);
|
|
||||||
|
|
||||||
KeyPair keyPair;
|
|
||||||
try {
|
|
||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
|
||||||
keyPairGenerator.initialize(2048);
|
|
||||||
keyPair = keyPairGenerator.generateKeyPair();
|
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
|
||||||
// Should never happen
|
|
||||||
e1.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Date now = new Date();
|
|
||||||
|
|
||||||
// Expires in 20 years
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.setTime(now);
|
|
||||||
calendar.add(Calendar.YEAR, 20);
|
|
||||||
Date expirationDate = calendar.getTime();
|
|
||||||
|
|
||||||
BigInteger serial = new BigInteger(snBytes).abs();
|
|
||||||
|
|
||||||
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
|
||||||
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
|
||||||
X500Name name = nameBuilder.build();
|
|
||||||
|
|
||||||
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
|
||||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
|
||||||
|
|
||||||
try {
|
|
||||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
|
||||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
|
||||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Nothing should go wrong here
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Generated a new key pair");
|
|
||||||
|
|
||||||
// Save the resulting pair
|
|
||||||
saveCertKeyPair();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveCertKeyPair() {
|
|
||||||
try {
|
|
||||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
|
||||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
|
||||||
|
|
||||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
|
||||||
StringWriter strWriter = new StringWriter();
|
|
||||||
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
|
||||||
pemWriter.writeObject(cert);
|
|
||||||
pemWriter.close();
|
|
||||||
|
|
||||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
|
||||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
|
||||||
String pemStr = strWriter.getBuffer().toString();
|
|
||||||
for (int i = 0; i < pemStr.length(); i++) {
|
|
||||||
char c = pemStr.charAt(i);
|
|
||||||
if (c != '\r')
|
|
||||||
certWriter.append(c);
|
|
||||||
}
|
|
||||||
certWriter.close();
|
|
||||||
|
|
||||||
// Write the private out in PKCS8 format
|
|
||||||
keyOut.write(key.getEncoded());
|
|
||||||
|
|
||||||
certOut.close();
|
|
||||||
keyOut.close();
|
|
||||||
|
|
||||||
LimeLog.info("Saved generated key pair to disk");
|
|
||||||
} catch (IOException e) {
|
|
||||||
// This isn't good because it means we'll have
|
|
||||||
// to re-pair next time
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate getClientCertificate() {
|
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
|
||||||
// the certificate and key at a time
|
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Return a loaded cert if we have one
|
|
||||||
if (cert != null) {
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No loaded cert yet, let's see if we have one on disk
|
|
||||||
if (loadCertKeyPair()) {
|
|
||||||
// Got one
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to generate a new key pair
|
|
||||||
if (!generateCertKeyPair()) {
|
|
||||||
// Failed
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the generated pair
|
|
||||||
loadCertKeyPair();
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public RSAPrivateKey getClientPrivateKey() {
|
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
|
||||||
// the certificate and key at a time
|
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Return a loaded key if we have one
|
|
||||||
if (key != null) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No loaded key yet, let's see if we have one on disk
|
|
||||||
if (loadCertKeyPair()) {
|
|
||||||
// Got one
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to generate a new key pair
|
|
||||||
if (!generateCertKeyPair()) {
|
|
||||||
// Failed
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the generated pair
|
|
||||||
loadCertKeyPair();
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getPemEncodedClientCertificate() {
|
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Call our helper function to do the cert loading/generation for us
|
|
||||||
getClientCertificate();
|
|
||||||
|
|
||||||
// Return a cached value if we have it
|
|
||||||
return pemCertBytes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String encodeBase64String(byte[] data) {
|
|
||||||
return Base64.encodeToString(data, Base64.NO_WRAP);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,241 +0,0 @@
|
|||||||
package com.limelight.binding.input;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
|
||||||
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
|
|
||||||
public class TouchContext {
|
|
||||||
private int lastTouchX = 0;
|
|
||||||
private int lastTouchY = 0;
|
|
||||||
private int originalTouchX = 0;
|
|
||||||
private int originalTouchY = 0;
|
|
||||||
private long originalTouchTime = 0;
|
|
||||||
private boolean cancelled;
|
|
||||||
private boolean confirmedMove;
|
|
||||||
private boolean confirmedDrag;
|
|
||||||
private Timer dragTimer;
|
|
||||||
private double distanceMoved;
|
|
||||||
private double xFactor, yFactor;
|
|
||||||
|
|
||||||
private final NvConnection conn;
|
|
||||||
private final int actionIndex;
|
|
||||||
private final int referenceWidth;
|
|
||||||
private final int referenceHeight;
|
|
||||||
private final View targetView;
|
|
||||||
|
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
|
||||||
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
|
||||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex,
|
|
||||||
int referenceWidth, int referenceHeight, View view)
|
|
||||||
{
|
|
||||||
this.conn = conn;
|
|
||||||
this.actionIndex = actionIndex;
|
|
||||||
this.referenceWidth = referenceWidth;
|
|
||||||
this.referenceHeight = referenceHeight;
|
|
||||||
this.targetView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getActionIndex()
|
|
||||||
{
|
|
||||||
return actionIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isWithinTapBounds(int touchX, int touchY)
|
|
||||||
{
|
|
||||||
int xDelta = Math.abs(touchX - originalTouchX);
|
|
||||||
int yDelta = Math.abs(touchY - originalTouchY);
|
|
||||||
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
|
||||||
yDelta <= TAP_MOVEMENT_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTap()
|
|
||||||
{
|
|
||||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
|
||||||
|
|
||||||
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte getMouseButtonIndex()
|
|
||||||
{
|
|
||||||
if (actionIndex == 1) {
|
|
||||||
return MouseButtonPacket.BUTTON_RIGHT;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return MouseButtonPacket.BUTTON_LEFT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
|
||||||
{
|
|
||||||
// Get the view dimensions to scale inputs on this touch
|
|
||||||
xFactor = referenceWidth / (double)targetView.getWidth();
|
|
||||||
yFactor = referenceHeight / (double)targetView.getHeight();
|
|
||||||
|
|
||||||
originalTouchX = lastTouchX = eventX;
|
|
||||||
originalTouchY = lastTouchY = eventY;
|
|
||||||
originalTouchTime = System.currentTimeMillis();
|
|
||||||
cancelled = confirmedDrag = confirmedMove = false;
|
|
||||||
distanceMoved = 0;
|
|
||||||
|
|
||||||
if (actionIndex == 0) {
|
|
||||||
// Start the timer for engaging a drag
|
|
||||||
startDragTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void touchUpEvent(int eventX, int eventY)
|
|
||||||
{
|
|
||||||
if (cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel the drag timer
|
|
||||||
cancelDragTimer();
|
|
||||||
|
|
||||||
byte buttonIndex = getMouseButtonIndex();
|
|
||||||
|
|
||||||
if (confirmedDrag) {
|
|
||||||
// Raise the button after a drag
|
|
||||||
conn.sendMouseButtonUp(buttonIndex);
|
|
||||||
}
|
|
||||||
else if (isTap())
|
|
||||||
{
|
|
||||||
// Lower the mouse button
|
|
||||||
conn.sendMouseButtonDown(buttonIndex);
|
|
||||||
|
|
||||||
// We need to sleep a bit here because some games
|
|
||||||
// do input detection by polling
|
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
|
|
||||||
// Raise the mouse button
|
|
||||||
conn.sendMouseButtonUp(buttonIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void startDragTimer() {
|
|
||||||
dragTimer = new Timer(true);
|
|
||||||
dragTimer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
synchronized (TouchContext.this) {
|
|
||||||
// Check if someone already set move
|
|
||||||
if (confirmedMove) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if someone cancelled us
|
|
||||||
if (dragTimer == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uncancellable now
|
|
||||||
dragTimer = null;
|
|
||||||
|
|
||||||
// We haven't been cancelled before the timer expired so begin dragging
|
|
||||||
confirmedDrag = true;
|
|
||||||
conn.sendMouseButtonDown(getMouseButtonIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, DRAG_TIME_THRESHOLD);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void cancelDragTimer() {
|
|
||||||
if (dragTimer != null) {
|
|
||||||
dragTimer.cancel();
|
|
||||||
dragTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
|
|
||||||
// If we've already confirmed something, get out now
|
|
||||||
if (confirmedMove || confirmedDrag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it leaves the tap bounds before the drag time expires, it's a move.
|
|
||||||
if (!isWithinTapBounds(eventX, eventY)) {
|
|
||||||
confirmedMove = true;
|
|
||||||
cancelDragTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've exceeded the maximum distance moved
|
|
||||||
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
|
|
||||||
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
|
|
||||||
confirmedMove = true;
|
|
||||||
cancelDragTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean touchMoveEvent(int eventX, int eventY)
|
|
||||||
{
|
|
||||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
|
||||||
{
|
|
||||||
// We only send moves and drags for the primary touch point
|
|
||||||
if (actionIndex == 0) {
|
|
||||||
checkForConfirmedMove(eventX, eventY);
|
|
||||||
|
|
||||||
int deltaX = eventX - lastTouchX;
|
|
||||||
int deltaY = eventY - lastTouchY;
|
|
||||||
|
|
||||||
// Scale the deltas based on the factors passed to our constructor
|
|
||||||
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
|
|
||||||
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
|
|
||||||
|
|
||||||
// Fix up the signs
|
|
||||||
if (eventX < lastTouchX) {
|
|
||||||
deltaX = -deltaX;
|
|
||||||
}
|
|
||||||
if (eventY < lastTouchY) {
|
|
||||||
deltaY = -deltaY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
|
||||||
// non-zero to update lastTouch that way devices that report small touch events often
|
|
||||||
// will work correctly
|
|
||||||
if (deltaX != 0) {
|
|
||||||
lastTouchX = eventX;
|
|
||||||
}
|
|
||||||
if (deltaY != 0) {
|
|
||||||
lastTouchY = eventY;
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lastTouchX = eventX;
|
|
||||||
lastTouchY = eventY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelTouch() {
|
|
||||||
cancelled = true;
|
|
||||||
|
|
||||||
// Cancel the drag timer
|
|
||||||
cancelDragTimer();
|
|
||||||
|
|
||||||
// If it was a confirmed drag, we'll need to raise the button now
|
|
||||||
if (confirmedDrag) {
|
|
||||||
conn.sendMouseButtonUp(getMouseButtonIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCancelled() {
|
|
||||||
return cancelled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
|
||||||
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
|
||||||
|
|
||||||
private View targetView;
|
|
||||||
|
|
||||||
public AndroidNativePointerCaptureProvider(View targetView) {
|
|
||||||
this.targetView = targetView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enableCapture() {
|
|
||||||
super.enableCapture();
|
|
||||||
targetView.requestPointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {
|
|
||||||
super.disableCapture();
|
|
||||||
targetView.releasePointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCapturingActive() {
|
|
||||||
return targetView.hasPointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
float x = event.getX();
|
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
|
||||||
x += event.getHistoricalX(i);
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
float y = event.getY();
|
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
|
||||||
y += event.getHistoricalY(i);
|
|
||||||
}
|
|
||||||
return y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.PointerIcon;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
|
||||||
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
|
||||||
private ViewGroup rootViewGroup;
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
public AndroidPointerIconCaptureProvider(Activity activity) {
|
|
||||||
this.context = activity;
|
|
||||||
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPointerIconOnAllViews(PointerIcon icon) {
|
|
||||||
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
|
|
||||||
View view = rootViewGroup.getChildAt(i);
|
|
||||||
view.setPointerIcon(icon);
|
|
||||||
}
|
|
||||||
rootViewGroup.setPointerIcon(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enableCapture() {
|
|
||||||
super.enableCapture();
|
|
||||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {
|
|
||||||
super.disableCapture();
|
|
||||||
setPointerIconOnAllViews(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
|
|
||||||
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.LimelightBuildProps;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
|
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
|
||||||
|
|
||||||
public class InputCaptureManager {
|
|
||||||
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
|
||||||
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
|
||||||
LimeLog.info("Using Android O+ native mouse capture");
|
|
||||||
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
|
||||||
}
|
|
||||||
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
|
|
||||||
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
|
|
||||||
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
|
|
||||||
LimeLog.info("Using NVIDIA mouse capture extension");
|
|
||||||
return new ShieldCaptureProvider(activity);
|
|
||||||
}
|
|
||||||
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
|
|
||||||
LimeLog.info("Using Evdev mouse capture");
|
|
||||||
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
|
|
||||||
}
|
|
||||||
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
|
||||||
// Android N's native capture can't capture over system UI elements
|
|
||||||
// so we want to only use it if there's no other option.
|
|
||||||
LimeLog.info("Using Android N+ pointer hiding");
|
|
||||||
return new AndroidPointerIconCaptureProvider(activity);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.info("Mouse capture not available");
|
|
||||||
return new NullCaptureProvider();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
public abstract class InputCaptureProvider {
|
|
||||||
protected boolean isCapturing;
|
|
||||||
|
|
||||||
public void enableCapture() {
|
|
||||||
isCapturing = true;
|
|
||||||
}
|
|
||||||
public void disableCapture() {
|
|
||||||
isCapturing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void destroy() {}
|
|
||||||
|
|
||||||
public boolean isCapturingEnabled() {
|
|
||||||
return isCapturing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCapturingActive() {
|
|
||||||
return isCapturing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
|
|
||||||
public class NullCaptureProvider extends InputCaptureProvider {}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.hardware.input.InputManager;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
// NVIDIA extended the Android input APIs with support for using an attached mouse in relative
|
|
||||||
// mode without having to grab the input device (which requires root). The data comes in the form
|
|
||||||
// of new AXIS_RELATIVE_X and AXIS_RELATIVE_Y constants in the mouse's MotionEvent objects and
|
|
||||||
// a new function, InputManager.setCursorVisibility(), that allows the cursor to be hidden.
|
|
||||||
//
|
|
||||||
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
|
|
||||||
|
|
||||||
public class ShieldCaptureProvider extends InputCaptureProvider {
|
|
||||||
private static boolean nvExtensionSupported;
|
|
||||||
private static Method methodSetCursorVisibility;
|
|
||||||
private static int AXIS_RELATIVE_X;
|
|
||||||
private static int AXIS_RELATIVE_Y;
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
methodSetCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class);
|
|
||||||
|
|
||||||
Field fieldRelX = MotionEvent.class.getField("AXIS_RELATIVE_X");
|
|
||||||
Field fieldRelY = MotionEvent.class.getField("AXIS_RELATIVE_Y");
|
|
||||||
|
|
||||||
AXIS_RELATIVE_X = (Integer) fieldRelX.get(null);
|
|
||||||
AXIS_RELATIVE_Y = (Integer) fieldRelY.get(null);
|
|
||||||
|
|
||||||
nvExtensionSupported = true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
nvExtensionSupported = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ShieldCaptureProvider(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return nvExtensionSupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean setCursorVisibility(boolean visible) {
|
|
||||||
try {
|
|
||||||
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
|
|
||||||
return true;
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enableCapture() {
|
|
||||||
super.enableCapture();
|
|
||||||
setCursorVisibility(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {
|
|
||||||
super.disableCapture();
|
|
||||||
setCursorVisibility(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
|
||||||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
return event.getAxisValue(AXIS_RELATIVE_X);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
return event.getAxisValue(AXIS_RELATIVE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
public abstract class AbstractController {
|
|
||||||
|
|
||||||
private final int deviceId;
|
|
||||||
|
|
||||||
private UsbDriverListener listener;
|
|
||||||
|
|
||||||
protected short buttonFlags;
|
|
||||||
protected float leftTrigger, rightTrigger;
|
|
||||||
protected float rightStickX, rightStickY;
|
|
||||||
protected float leftStickX, leftStickY;
|
|
||||||
|
|
||||||
public int getControllerId() {
|
|
||||||
return deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setButtonFlag(int buttonFlag, int data) {
|
|
||||||
if (data != 0) {
|
|
||||||
buttonFlags |= buttonFlag;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
buttonFlags &= ~buttonFlag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void reportInput() {
|
|
||||||
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
|
||||||
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract boolean start();
|
|
||||||
public abstract void stop();
|
|
||||||
|
|
||||||
public AbstractController(int deviceId, UsbDriverListener listener) {
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
|
||||||
|
|
||||||
protected void notifyDeviceRemoved() {
|
|
||||||
listener.deviceRemoved(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void notifyDeviceAdded() {
|
|
||||||
listener.deviceAdded(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
import android.hardware.usb.UsbConstants;
|
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
|
||||||
import android.hardware.usb.UsbEndpoint;
|
|
||||||
import android.hardware.usb.UsbInterface;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
|
|
||||||
public abstract class AbstractXboxController extends AbstractController {
|
|
||||||
protected final UsbDevice device;
|
|
||||||
protected final UsbDeviceConnection connection;
|
|
||||||
|
|
||||||
private Thread inputThread;
|
|
||||||
private boolean stopped;
|
|
||||||
|
|
||||||
protected UsbEndpoint inEndpt, outEndpt;
|
|
||||||
|
|
||||||
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
|
||||||
super(deviceId, listener);
|
|
||||||
this.device = device;
|
|
||||||
this.connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Thread createInputThread() {
|
|
||||||
return new Thread() {
|
|
||||||
public void run() {
|
|
||||||
while (!isInterrupted() && !stopped) {
|
|
||||||
byte[] buffer = new byte[64];
|
|
||||||
|
|
||||||
int res;
|
|
||||||
|
|
||||||
//
|
|
||||||
// There's no way that I can tell to determine if a device has failed
|
|
||||||
// or if the timeout has simply expired. We'll check how long the transfer
|
|
||||||
// took to fail and assume the device failed if it happened before the timeout
|
|
||||||
// expired.
|
|
||||||
//
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Read the next input state packet
|
|
||||||
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
|
||||||
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
|
||||||
|
|
||||||
// If we get a zero length response, treat it as an error
|
|
||||||
if (res == 0) {
|
|
||||||
res = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
|
||||||
LimeLog.warning("Detected device I/O error");
|
|
||||||
AbstractXboxController.this.stop();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (res == -1 && !isInterrupted() && !stopped);
|
|
||||||
|
|
||||||
if (res == -1 || stopped) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
|
|
||||||
// Report input if handleRead() returns true
|
|
||||||
reportInput();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean start() {
|
|
||||||
// Force claim all interfaces
|
|
||||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
|
||||||
UsbInterface iface = device.getInterface(i);
|
|
||||||
|
|
||||||
if (!connection.claimInterface(iface, true)) {
|
|
||||||
LimeLog.warning("Failed to claim interfaces");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the endpoints
|
|
||||||
UsbInterface iface = device.getInterface(0);
|
|
||||||
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
|
||||||
UsbEndpoint endpt = iface.getEndpoint(i);
|
|
||||||
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
|
||||||
if (inEndpt != null) {
|
|
||||||
LimeLog.warning("Found duplicate IN endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
inEndpt = endpt;
|
|
||||||
}
|
|
||||||
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
|
||||||
if (outEndpt != null) {
|
|
||||||
LimeLog.warning("Found duplicate OUT endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
outEndpt = endpt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the required endpoints were present
|
|
||||||
if (inEndpt == null || outEndpt == null) {
|
|
||||||
LimeLog.warning("Missing required endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the init function
|
|
||||||
if (!doInit()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report that we're added _before_ starting the input thread
|
|
||||||
notifyDeviceAdded();
|
|
||||||
|
|
||||||
// Start listening for controller input
|
|
||||||
inputThread = createInputThread();
|
|
||||||
inputThread.start();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (stopped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopped = true;
|
|
||||||
|
|
||||||
// Cancel any rumble effects
|
|
||||||
rumble((short)0, (short)0);
|
|
||||||
|
|
||||||
// Stop the input thread
|
|
||||||
if (inputThread != null) {
|
|
||||||
inputThread.interrupt();
|
|
||||||
inputThread = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the USB connection
|
|
||||||
connection.close();
|
|
||||||
|
|
||||||
// Report the device removed
|
|
||||||
notifyDeviceRemoved();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract boolean handleRead(ByteBuffer buffer);
|
|
||||||
protected abstract boolean doInit();
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
public interface UsbDriverListener {
|
|
||||||
void reportControllerState(int controllerId, short buttonFlags,
|
|
||||||
float leftStickX, float leftStickY,
|
|
||||||
float rightStickX, float rightStickY,
|
|
||||||
float leftTrigger, float rightTrigger);
|
|
||||||
|
|
||||||
void deviceRemoved(AbstractController controller);
|
|
||||||
void deviceAdded(AbstractController controller);
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
|
||||||
import android.hardware.usb.UsbManager;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class UsbDriverService extends Service implements UsbDriverListener {
|
|
||||||
|
|
||||||
private static final String ACTION_USB_PERMISSION =
|
|
||||||
"com.limelight.USB_PERMISSION";
|
|
||||||
|
|
||||||
private UsbManager usbManager;
|
|
||||||
private PreferenceConfiguration prefConfig;
|
|
||||||
|
|
||||||
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
|
||||||
private final UsbDriverBinder binder = new UsbDriverBinder();
|
|
||||||
|
|
||||||
private final ArrayList<AbstractController> controllers = new ArrayList<>();
|
|
||||||
|
|
||||||
private UsbDriverListener listener;
|
|
||||||
private int nextDeviceId;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
|
||||||
// Call through to the client's listener
|
|
||||||
if (listener != null) {
|
|
||||||
listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deviceRemoved(AbstractController controller) {
|
|
||||||
// Remove the the controller from our list (if not removed already)
|
|
||||||
controllers.remove(controller);
|
|
||||||
|
|
||||||
// Call through to the client's listener
|
|
||||||
if (listener != null) {
|
|
||||||
listener.deviceRemoved(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deviceAdded(AbstractController controller) {
|
|
||||||
// Call through to the client's listener
|
|
||||||
if (listener != null) {
|
|
||||||
listener.deviceAdded(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UsbEventReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
String action = intent.getAction();
|
|
||||||
|
|
||||||
// Initial attachment broadcast
|
|
||||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
|
||||||
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
|
||||||
|
|
||||||
// shouldClaimDevice() looks at the kernel's enumerated input
|
|
||||||
// devices to make its decision about whether to prompt to take
|
|
||||||
// control of the device. The kernel bringing up the input stack
|
|
||||||
// may race with this callback and cause us to prompt when the
|
|
||||||
// kernel is capable of running the device. Let's post a delayed
|
|
||||||
// message to process this state change to allow the kernel
|
|
||||||
// some time to bring up the stack.
|
|
||||||
new Handler().postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Continue the state machine
|
|
||||||
handleUsbDeviceState(device);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
// Subsequent permission dialog completion intent
|
|
||||||
else if (action.equals(ACTION_USB_PERMISSION)) {
|
|
||||||
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
|
||||||
|
|
||||||
// If we got this far, we've already found we're able to handle this device
|
|
||||||
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
|
||||||
handleUsbDeviceState(device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UsbDriverBinder extends Binder {
|
|
||||||
public void setListener(UsbDriverListener listener) {
|
|
||||||
UsbDriverService.this.listener = listener;
|
|
||||||
|
|
||||||
// Report all controllerMap that already exist
|
|
||||||
if (listener != null) {
|
|
||||||
for (AbstractController controller : controllers) {
|
|
||||||
listener.deviceAdded(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleUsbDeviceState(UsbDevice device) {
|
|
||||||
// Are we able to operate it?
|
|
||||||
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
|
|
||||||
// Do we have permission yet?
|
|
||||||
if (!usbManager.hasPermission(device)) {
|
|
||||||
// Let's ask for permission
|
|
||||||
try {
|
|
||||||
// This function is not documented as throwing any exceptions (denying access
|
|
||||||
// is indicated by calling the PendingIntent with a false result). However,
|
|
||||||
// Samsung Knox has some policies which block this request, but rather than
|
|
||||||
// just returning a false result or returning 0 enumerated devices,
|
|
||||||
// they throw an undocumented SecurityException from this call, crashing
|
|
||||||
// the whole app. :(
|
|
||||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the device
|
|
||||||
UsbDeviceConnection connection = usbManager.openDevice(device);
|
|
||||||
if (connection == null) {
|
|
||||||
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AbstractController controller;
|
|
||||||
|
|
||||||
if (XboxOneController.canClaimDevice(device)) {
|
|
||||||
controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
|
||||||
}
|
|
||||||
else if (Xbox360Controller.canClaimDevice(device)) {
|
|
||||||
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Unreachable
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the controller
|
|
||||||
if (!controller.start()) {
|
|
||||||
connection.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this controller to the list
|
|
||||||
controllers.add(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isRecognizedInputDevice(UsbDevice device) {
|
|
||||||
// On KitKat and later, we can determine if this VID and PID combo
|
|
||||||
// matches an existing input device and defer to the built-in controller
|
|
||||||
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
for (int id : InputDevice.getDeviceIds()) {
|
|
||||||
InputDevice inputDev = InputDevice.getDevice(id);
|
|
||||||
if (inputDev == null) {
|
|
||||||
// Device was removed while looping
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputDev.getVendorId() == device.getVendorId() &&
|
|
||||||
inputDev.getProductId() == device.getProductId()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
|
||||||
// We always bind to XB1 controllers but only bind to XB360 controllers
|
|
||||||
// if we know the kernel isn't already driving this device.
|
|
||||||
return XboxOneController.canClaimDevice(device) ||
|
|
||||||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
|
||||||
this.prefConfig = PreferenceConfiguration.readPreferences(this);
|
|
||||||
|
|
||||||
// Register for USB attach broadcasts and permission completions
|
|
||||||
IntentFilter filter = new IntentFilter();
|
|
||||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
|
||||||
filter.addAction(ACTION_USB_PERMISSION);
|
|
||||||
registerReceiver(receiver, filter);
|
|
||||||
|
|
||||||
// Enumerate existing devices
|
|
||||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
|
||||||
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
|
|
||||||
// Start the process of claiming this device
|
|
||||||
handleUsbDeviceState(dev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
// Stop the attachment receiver
|
|
||||||
unregisterReceiver(receiver);
|
|
||||||
|
|
||||||
// Remove listeners
|
|
||||||
listener = null;
|
|
||||||
|
|
||||||
// Stop all controllers
|
|
||||||
while (controllers.size() > 0) {
|
|
||||||
// Stop and remove the controller
|
|
||||||
controllers.remove(0).stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return binder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
import android.hardware.usb.UsbConstants;
|
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
public class Xbox360Controller extends AbstractXboxController {
|
|
||||||
private static final int XB360_IFACE_SUBCLASS = 93;
|
|
||||||
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
|
||||||
|
|
||||||
private static final int[] SUPPORTED_VENDORS = {
|
|
||||||
0x0079, // GPD Win 2
|
|
||||||
0x044f, // Thrustmaster
|
|
||||||
0x045e, // Microsoft
|
|
||||||
0x046d, // Logitech
|
|
||||||
0x056e, // Elecom
|
|
||||||
0x06a3, // Saitek
|
|
||||||
0x0738, // Mad Catz
|
|
||||||
0x07ff, // Mad Catz
|
|
||||||
0x0e6f, // Unknown
|
|
||||||
0x0f0d, // Hori
|
|
||||||
0x1038, // SteelSeries
|
|
||||||
0x11c9, // Nacon
|
|
||||||
0x12ab, // Unknown
|
|
||||||
0x1430, // RedOctane
|
|
||||||
0x146b, // BigBen
|
|
||||||
0x1532, // Razer Sabertooth
|
|
||||||
0x15e4, // Numark
|
|
||||||
0x162e, // Joytech
|
|
||||||
0x1689, // Razer Onza
|
|
||||||
0x1bad, // Harmonix
|
|
||||||
0x24c6, // PowerA
|
|
||||||
};
|
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
|
||||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
|
||||||
if (device.getVendorId() == supportedVid &&
|
|
||||||
device.getInterfaceCount() >= 1 &&
|
|
||||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
|
||||||
device.getInterface(0).getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
|
||||||
device.getInterface(0).getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
|
||||||
super(device, connection, deviceId, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int unsignByte(byte b) {
|
|
||||||
if (b < 0) {
|
|
||||||
return b + 256;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean handleRead(ByteBuffer buffer) {
|
|
||||||
if (buffer.limit() < 14) {
|
|
||||||
LimeLog.severe("Read too small: "+buffer.limit());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip first short
|
|
||||||
buffer.position(buffer.position() + 2);
|
|
||||||
|
|
||||||
// DPAD
|
|
||||||
byte b = buffer.get();
|
|
||||||
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
|
||||||
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
|
||||||
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
|
||||||
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
|
||||||
|
|
||||||
// Start/Select
|
|
||||||
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
|
|
||||||
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
|
|
||||||
|
|
||||||
// LS/RS
|
|
||||||
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
|
||||||
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
|
||||||
|
|
||||||
// ABXY buttons
|
|
||||||
b = buffer.get();
|
|
||||||
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
|
||||||
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
|
||||||
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
|
||||||
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
|
||||||
|
|
||||||
// LB/RB
|
|
||||||
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
|
|
||||||
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
|
|
||||||
|
|
||||||
// Xbox button
|
|
||||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
|
|
||||||
|
|
||||||
// Triggers
|
|
||||||
leftTrigger = unsignByte(buffer.get()) / 255.0f;
|
|
||||||
rightTrigger = unsignByte(buffer.get()) / 255.0f;
|
|
||||||
|
|
||||||
// Left stick
|
|
||||||
leftStickX = buffer.getShort() / 32767.0f;
|
|
||||||
leftStickY = ~buffer.getShort() / 32767.0f;
|
|
||||||
|
|
||||||
// Right stick
|
|
||||||
rightStickX = buffer.getShort() / 32767.0f;
|
|
||||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
|
||||||
|
|
||||||
// Return true to send input
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean sendLedCommand(byte command) {
|
|
||||||
byte[] commandBuffer = {0x01, 0x03, command};
|
|
||||||
|
|
||||||
int res = connection.bulkTransfer(outEndpt, commandBuffer, commandBuffer.length, 3000);
|
|
||||||
if (res != commandBuffer.length) {
|
|
||||||
LimeLog.warning("LED set transfer failed: "+res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean doInit() {
|
|
||||||
// Turn the LED on corresponding to our device ID
|
|
||||||
sendLedCommand((byte)(2 + (getControllerId() % 4)));
|
|
||||||
|
|
||||||
// No need to fail init if the LED command fails
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
|
||||||
byte[] data = {
|
|
||||||
0x00, 0x08, 0x00,
|
|
||||||
(byte)(lowFreqMotor >> 8), (byte)(highFreqMotor >> 8),
|
|
||||||
0x00, 0x00, 0x00
|
|
||||||
};
|
|
||||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
|
||||||
if (res != data.length) {
|
|
||||||
LimeLog.warning("Rumble transfer failed: "+res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
import android.hardware.usb.UsbConstants;
|
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class XboxOneController extends AbstractXboxController {
|
|
||||||
|
|
||||||
private static final int XB1_IFACE_SUBCLASS = 71;
|
|
||||||
private static final int XB1_IFACE_PROTOCOL = 208;
|
|
||||||
|
|
||||||
private static final int[] SUPPORTED_VENDORS = {
|
|
||||||
0x045e, // Microsoft
|
|
||||||
0x0738, // Mad Catz
|
|
||||||
0x0e6f, // Unknown
|
|
||||||
0x0f0d, // Hori
|
|
||||||
0x1532, // Razer Wildcat
|
|
||||||
0x24c6, // PowerA
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
|
||||||
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
|
|
||||||
0x00, 0x00, 0x00, (byte)0x80, 0x00};
|
|
||||||
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
|
|
||||||
private static final byte[] PDP_INIT2 = {0x06, 0x20, 0x00, 0x02, 0x01, 0x00};
|
|
||||||
private static final byte[] RUMBLE_INIT1 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
|
|
||||||
0x1D, 0x1D, (byte)0xFF, 0x00, 0x00};
|
|
||||||
private static final byte[] RUMBLE_INIT2 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
|
||||||
|
|
||||||
private static InitPacket[] INIT_PKTS = {
|
|
||||||
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
|
|
||||||
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
|
|
||||||
new InitPacket(0x0000, 0x0000, FW2015_INIT),
|
|
||||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
|
|
||||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
|
|
||||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
|
|
||||||
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT1),
|
|
||||||
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT1),
|
|
||||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT2),
|
|
||||||
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT2),
|
|
||||||
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT2),
|
|
||||||
};
|
|
||||||
|
|
||||||
private byte seqNum = 0;
|
|
||||||
|
|
||||||
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
|
||||||
super(device, connection, deviceId, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processButtons(ByteBuffer buffer) {
|
|
||||||
byte b = buffer.get();
|
|
||||||
|
|
||||||
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
|
|
||||||
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
|
|
||||||
|
|
||||||
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
|
||||||
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
|
||||||
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
|
||||||
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
|
||||||
|
|
||||||
b = buffer.get();
|
|
||||||
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
|
||||||
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
|
||||||
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
|
||||||
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
|
||||||
|
|
||||||
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
|
|
||||||
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
|
|
||||||
|
|
||||||
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
|
||||||
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
|
||||||
|
|
||||||
leftTrigger = buffer.getShort() / 1023.0f;
|
|
||||||
rightTrigger = buffer.getShort() / 1023.0f;
|
|
||||||
|
|
||||||
leftStickX = buffer.getShort() / 32767.0f;
|
|
||||||
leftStickY = ~buffer.getShort() / 32767.0f;
|
|
||||||
|
|
||||||
rightStickX = buffer.getShort() / 32767.0f;
|
|
||||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ackModeReport(byte seqNum) {
|
|
||||||
byte[] payload = {0x01, 0x20, seqNum, 0x09, 0x00, 0x07, 0x20, 0x02,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
|
||||||
connection.bulkTransfer(outEndpt, payload, payload.length, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean handleRead(ByteBuffer buffer) {
|
|
||||||
switch (buffer.get())
|
|
||||||
{
|
|
||||||
case 0x20:
|
|
||||||
buffer.position(buffer.position()+3);
|
|
||||||
processButtons(buffer);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 0x07:
|
|
||||||
// The Xbox One S controller needs acks for mode reports otherwise
|
|
||||||
// it retransmits them forever.
|
|
||||||
if (buffer.get() == 0x30) {
|
|
||||||
ackModeReport(buffer.get());
|
|
||||||
buffer.position(buffer.position() + 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
buffer.position(buffer.position() + 2);
|
|
||||||
}
|
|
||||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
|
||||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
|
||||||
if (device.getVendorId() == supportedVid &&
|
|
||||||
device.getInterfaceCount() >= 1 &&
|
|
||||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
|
||||||
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
|
||||||
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean doInit() {
|
|
||||||
// Send all applicable init packets
|
|
||||||
for (InitPacket pkt : INIT_PKTS) {
|
|
||||||
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pkt.productId != 0 && device.getProductId() != pkt.productId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] data = Arrays.copyOf(pkt.data, pkt.data.length);
|
|
||||||
|
|
||||||
// Populate sequence number
|
|
||||||
data[2] = seqNum++;
|
|
||||||
|
|
||||||
// Send the initialization packet
|
|
||||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 3000);
|
|
||||||
if (res != data.length) {
|
|
||||||
LimeLog.warning("Initialization transfer failed: "+res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
|
||||||
byte[] data = {
|
|
||||||
0x09, 0x00, seqNum++, 0x09, 0x00,
|
|
||||||
0x0F, 0x00, 0x00,
|
|
||||||
(byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9),
|
|
||||||
(byte)0xFF, 0x00, (byte)0xFF
|
|
||||||
};
|
|
||||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
|
||||||
if (res != data.length) {
|
|
||||||
LimeLog.warning("Rumble transfer failed: "+res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class InitPacket {
|
|
||||||
final int vendorId;
|
|
||||||
final int productId;
|
|
||||||
final byte[] data;
|
|
||||||
|
|
||||||
InitPacket(int vendorId, int productId, byte[] data) {
|
|
||||||
this.vendorId = vendorId;
|
|
||||||
this.productId = productId;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.limelight.LimelightBuildProps;
|
|
||||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
|
||||||
|
|
||||||
public class EvdevCaptureProviderShim {
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return LimelightBuildProps.ROOT_BUILD;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to construct our capture provider using reflection because it isn't included in non-root builds
|
|
||||||
public static InputCaptureProvider createEvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
|
||||||
try {
|
|
||||||
Class providerClass = Class.forName("com.limelight.binding.input.evdev.EvdevCaptureProvider");
|
|
||||||
return (InputCaptureProvider) providerClass.getConstructors()[0].newInstance(activity, listener);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
public interface EvdevListener {
|
|
||||||
int BUTTON_LEFT = 1;
|
|
||||||
int BUTTON_MIDDLE = 2;
|
|
||||||
int BUTTON_RIGHT = 3;
|
|
||||||
int BUTTON_X1 = 4;
|
|
||||||
int BUTTON_X2 = 5;
|
|
||||||
|
|
||||||
void mouseMove(int deltaX, int deltaY);
|
|
||||||
void mouseButtonEvent(int buttonId, boolean down);
|
|
||||||
void mouseScroll(byte amount);
|
|
||||||
void keyboardEvent(boolean buttonDown, short keyCode);
|
|
||||||
}
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a analog stick on screen element. It is used to get 2-Axis user input.
|
|
||||||
*/
|
|
||||||
public class AnalogStick extends VirtualControllerElement {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* outer radius size in percent of the ui element
|
|
||||||
*/
|
|
||||||
public static final int SIZE_RADIUS_COMPLETE = 90;
|
|
||||||
/**
|
|
||||||
* analog stick size in percent of the ui element
|
|
||||||
*/
|
|
||||||
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
|
|
||||||
/**
|
|
||||||
* dead zone size in percent of the ui element
|
|
||||||
*/
|
|
||||||
public static final int SIZE_RADIUS_DEADZONE = 90;
|
|
||||||
/**
|
|
||||||
* time frame for a double click
|
|
||||||
*/
|
|
||||||
public final static long timeoutDoubleClick = 350;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
|
|
||||||
*/
|
|
||||||
public final static long timeoutDeadzone = 150;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener interface to update registered observers.
|
|
||||||
*/
|
|
||||||
public interface AnalogStickListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
|
|
||||||
*
|
|
||||||
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
|
|
||||||
* @param y vertical position, value from -1.0 ... 0 .. 1.0
|
|
||||||
*/
|
|
||||||
void onMovement(float x, float y);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onClick event will be fired on click on the analog stick
|
|
||||||
*/
|
|
||||||
void onClick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onDoubleClick event will be fired on a double click in a short time frame on the analog
|
|
||||||
* stick.
|
|
||||||
*/
|
|
||||||
void onDoubleClick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onRevoke event will be fired on unpress of the analog stick.
|
|
||||||
*/
|
|
||||||
void onRevoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Movement states of the analog sick.
|
|
||||||
*/
|
|
||||||
private enum STICK_STATE {
|
|
||||||
NO_MOVEMENT,
|
|
||||||
MOVED_IN_DEAD_ZONE,
|
|
||||||
MOVED_ACTIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click type states.
|
|
||||||
*/
|
|
||||||
private enum CLICK_STATE {
|
|
||||||
SINGLE,
|
|
||||||
DOUBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* configuration if the analog stick should be displayed as circle or square
|
|
||||||
*/
|
|
||||||
private boolean circle_stick = true; // TODO: implement square sick for simulations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* outer radius, this size will be automatically updated on resize
|
|
||||||
*/
|
|
||||||
private float radius_complete = 0;
|
|
||||||
/**
|
|
||||||
* analog stick radius, this size will be automatically updated on resize
|
|
||||||
*/
|
|
||||||
private float radius_analog_stick = 0;
|
|
||||||
/**
|
|
||||||
* dead zone radius, this size will be automatically updated on resize
|
|
||||||
*/
|
|
||||||
private float radius_dead_zone = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* horizontal position in relation to the center of the element
|
|
||||||
*/
|
|
||||||
private float relative_x = 0;
|
|
||||||
/**
|
|
||||||
* vertical position in relation to the center of the element
|
|
||||||
*/
|
|
||||||
private float relative_y = 0;
|
|
||||||
|
|
||||||
|
|
||||||
private double movement_radius = 0;
|
|
||||||
private double movement_angle = 0;
|
|
||||||
|
|
||||||
private float position_stick_x = 0;
|
|
||||||
private float position_stick_y = 0;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
|
||||||
|
|
||||||
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
|
|
||||||
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
|
|
||||||
|
|
||||||
private List<AnalogStickListener> listeners = new ArrayList<>();
|
|
||||||
private long timeLastClick = 0;
|
|
||||||
|
|
||||||
private static double getMovementRadius(float x, float y) {
|
|
||||||
return Math.sqrt(x * x + y * y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double getAngle(float way_x, float way_y) {
|
|
||||||
// prevent divisions by zero for corner cases
|
|
||||||
if (way_x == 0) {
|
|
||||||
return way_y < 0 ? Math.PI : 0;
|
|
||||||
} else if (way_y == 0) {
|
|
||||||
if (way_x > 0) {
|
|
||||||
return Math.PI * 3 / 2;
|
|
||||||
} else if (way_x < 0) {
|
|
||||||
return Math.PI * 1 / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// return correct calculated angle for each quadrant
|
|
||||||
if (way_x > 0) {
|
|
||||||
if (way_y < 0) {
|
|
||||||
// first quadrant
|
|
||||||
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
|
|
||||||
} else {
|
|
||||||
// second quadrant
|
|
||||||
return Math.PI + Math.atan((double) (way_x / way_y));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (way_y > 0) {
|
|
||||||
// third quadrant
|
|
||||||
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
|
|
||||||
} else {
|
|
||||||
// fourth quadrant
|
|
||||||
return 0 + Math.atan((double) (-way_x / -way_y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AnalogStick(VirtualController controller, Context context, int elementId) {
|
|
||||||
super(controller, context, elementId);
|
|
||||||
// reset stick position
|
|
||||||
position_stick_x = getWidth() / 2;
|
|
||||||
position_stick_y = getHeight() / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAnalogStickListener(AnalogStickListener listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnMovement(float x, float y) {
|
|
||||||
_DBG("movement x: " + x + " movement y: " + y);
|
|
||||||
// notify listeners
|
|
||||||
for (AnalogStickListener listener : listeners) {
|
|
||||||
listener.onMovement(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnClick() {
|
|
||||||
_DBG("click");
|
|
||||||
// notify listeners
|
|
||||||
for (AnalogStickListener listener : listeners) {
|
|
||||||
listener.onClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnDoubleClick() {
|
|
||||||
_DBG("double click");
|
|
||||||
// notify listeners
|
|
||||||
for (AnalogStickListener listener : listeners) {
|
|
||||||
listener.onDoubleClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnRevoke() {
|
|
||||||
_DBG("revoke");
|
|
||||||
// notify listeners
|
|
||||||
for (AnalogStickListener listener : listeners) {
|
|
||||||
listener.onRevoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
||||||
// calculate new radius sizes depending
|
|
||||||
radius_complete = getPercent(getCorrectWidth() / 2, 100);
|
|
||||||
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
|
||||||
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
|
||||||
|
|
||||||
super.onSizeChanged(w, h, oldw, oldh);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onElementDraw(Canvas canvas) {
|
|
||||||
// set transparent background
|
|
||||||
canvas.drawColor(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
|
||||||
|
|
||||||
// draw outer circle
|
|
||||||
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
|
|
||||||
paint.setColor(getDefaultColor());
|
|
||||||
} else {
|
|
||||||
paint.setColor(pressedColor);
|
|
||||||
}
|
|
||||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
|
|
||||||
|
|
||||||
paint.setColor(getDefaultColor());
|
|
||||||
// draw dead zone
|
|
||||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
|
|
||||||
|
|
||||||
// draw stick depending on state
|
|
||||||
switch (stick_state) {
|
|
||||||
case NO_MOVEMENT: {
|
|
||||||
paint.setColor(getDefaultColor());
|
|
||||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MOVED_IN_DEAD_ZONE:
|
|
||||||
case MOVED_ACTIVE: {
|
|
||||||
paint.setColor(pressedColor);
|
|
||||||
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePosition() {
|
|
||||||
// get 100% way
|
|
||||||
float complete = radius_complete - radius_analog_stick;
|
|
||||||
|
|
||||||
// calculate relative way
|
|
||||||
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
|
|
||||||
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
|
|
||||||
|
|
||||||
// update positions
|
|
||||||
position_stick_x = getWidth() / 2 - correlated_x;
|
|
||||||
position_stick_y = getHeight() / 2 - correlated_y;
|
|
||||||
|
|
||||||
// Stay active even if we're back in the deadzone because we know the user is actively
|
|
||||||
// giving analog stick input and we don't want to snap back into the deadzone.
|
|
||||||
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
|
||||||
// them to make precise movements.
|
|
||||||
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
|
||||||
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
|
||||||
movement_radius > radius_dead_zone) ?
|
|
||||||
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
|
||||||
|
|
||||||
// trigger move event if state active
|
|
||||||
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
|
|
||||||
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onElementTouchEvent(MotionEvent event) {
|
|
||||||
// save last click state
|
|
||||||
CLICK_STATE lastClickState = click_state;
|
|
||||||
|
|
||||||
// get absolute way for each axis
|
|
||||||
relative_x = -(getWidth() / 2 - event.getX());
|
|
||||||
relative_y = -(getHeight() / 2 - event.getY());
|
|
||||||
|
|
||||||
// get radius and angel of movement from center
|
|
||||||
movement_radius = getMovementRadius(relative_x, relative_y);
|
|
||||||
movement_angle = getAngle(relative_x, relative_y);
|
|
||||||
|
|
||||||
// chop radius if out of outer circle and already pressed
|
|
||||||
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
|
||||||
// not pressed already, so ignore event from outer circle
|
|
||||||
if (!isPressed()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
movement_radius = radius_complete - radius_analog_stick;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle event depending on action
|
|
||||||
switch (event.getActionMasked()) {
|
|
||||||
// down event (touch event)
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
|
||||||
// set to dead zoned, will be corrected in update position if necessary
|
|
||||||
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
|
||||||
// check for double click
|
|
||||||
if (lastClickState == CLICK_STATE.SINGLE &&
|
|
||||||
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
|
||||||
click_state = CLICK_STATE.DOUBLE;
|
|
||||||
notifyOnDoubleClick();
|
|
||||||
} else {
|
|
||||||
click_state = CLICK_STATE.SINGLE;
|
|
||||||
notifyOnClick();
|
|
||||||
}
|
|
||||||
// reset last click timestamp
|
|
||||||
timeLastClick = System.currentTimeMillis();
|
|
||||||
// set item pressed and update
|
|
||||||
setPressed(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// up event (revoke touch)
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_POINTER_UP: {
|
|
||||||
setPressed(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPressed()) {
|
|
||||||
// when is pressed calculate new positions (will trigger movement if necessary)
|
|
||||||
updatePosition();
|
|
||||||
} else {
|
|
||||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
|
||||||
notifyOnRevoke();
|
|
||||||
|
|
||||||
// not longer pressed reset analog stick
|
|
||||||
notifyOnMovement(0, 0);
|
|
||||||
}
|
|
||||||
// refresh view
|
|
||||||
invalidate();
|
|
||||||
// accept the touch event
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a digital button on screen element. It is used to get click and double click user input.
|
|
||||||
*/
|
|
||||||
public class DigitalButton extends VirtualControllerElement {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener interface to update registered observers.
|
|
||||||
*/
|
|
||||||
public interface DigitalButtonListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onClick event will be fired on button click.
|
|
||||||
*/
|
|
||||||
void onClick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onLongClick event will be fired on button long click.
|
|
||||||
*/
|
|
||||||
void onLongClick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onRelease event will be fired on button unpress.
|
|
||||||
*/
|
|
||||||
void onRelease();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private class TimerLongClickTimerTask extends TimerTask {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
onLongClickCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<DigitalButtonListener> listeners = new ArrayList<>();
|
|
||||||
private String text = "";
|
|
||||||
private int icon = -1;
|
|
||||||
private long timerLongClickTimeout = 3000;
|
|
||||||
private Timer timerLongClick = null;
|
|
||||||
private TimerLongClickTimerTask longClickTimerTask = null;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
|
||||||
|
|
||||||
private int layer;
|
|
||||||
private DigitalButton movingButton = null;
|
|
||||||
|
|
||||||
boolean inRange(float x, float y) {
|
|
||||||
return (this.getX() < x && this.getX() + this.getWidth() > x) &&
|
|
||||||
(this.getY() < y && this.getY() + this.getHeight() > y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkMovement(float x, float y, DigitalButton movingButton) {
|
|
||||||
// check if the movement happened in the same layer
|
|
||||||
if (movingButton.layer != this.layer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save current pressed state
|
|
||||||
boolean wasPressed = isPressed();
|
|
||||||
|
|
||||||
// check if the movement directly happened on the button
|
|
||||||
if ((this.movingButton == null || movingButton == this.movingButton)
|
|
||||||
&& this.inRange(x, y)) {
|
|
||||||
// set button pressed state depending on moving button pressed state
|
|
||||||
if (this.isPressed() != movingButton.isPressed()) {
|
|
||||||
this.setPressed(movingButton.isPressed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check if the movement is outside of the range and the movement button
|
|
||||||
// is the saved moving button
|
|
||||||
else if (movingButton == this.movingButton) {
|
|
||||||
this.setPressed(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a change occurred
|
|
||||||
if (wasPressed != isPressed()) {
|
|
||||||
if (isPressed()) {
|
|
||||||
// is pressed set moving button and emit click event
|
|
||||||
this.movingButton = movingButton;
|
|
||||||
onClickCallback();
|
|
||||||
} else {
|
|
||||||
// no longer pressed reset moving button and emit release event
|
|
||||||
this.movingButton = null;
|
|
||||||
onReleaseCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkMovementForAllButtons(float x, float y) {
|
|
||||||
for (VirtualControllerElement element : virtualController.getElements()) {
|
|
||||||
if (element != this && element instanceof DigitalButton) {
|
|
||||||
((DigitalButton) element).checkMovement(x, y, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public DigitalButton(VirtualController controller, int elementId, int layer, Context context) {
|
|
||||||
super(controller, context, elementId);
|
|
||||||
this.layer = layer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addDigitalButtonListener(DigitalButtonListener listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setText(String text) {
|
|
||||||
this.text = text;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIcon(int id) {
|
|
||||||
this.icon = id;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onElementDraw(Canvas canvas) {
|
|
||||||
// set transparent background
|
|
||||||
canvas.drawColor(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
paint.setTextSize(getPercent(getWidth(), 30));
|
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
|
||||||
|
|
||||||
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
|
||||||
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
|
||||||
|
|
||||||
if (icon != -1) {
|
|
||||||
Drawable d = getResources().getDrawable(icon);
|
|
||||||
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
|
|
||||||
d.draw(canvas);
|
|
||||||
} else {
|
|
||||||
paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
|
|
||||||
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onClickCallback() {
|
|
||||||
_DBG("clicked");
|
|
||||||
// notify listeners
|
|
||||||
for (DigitalButtonListener listener : listeners) {
|
|
||||||
listener.onClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
timerLongClick = new Timer();
|
|
||||||
longClickTimerTask = new TimerLongClickTimerTask();
|
|
||||||
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLongClickCallback() {
|
|
||||||
_DBG("long click");
|
|
||||||
// notify listeners
|
|
||||||
for (DigitalButtonListener listener : listeners) {
|
|
||||||
listener.onLongClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onReleaseCallback() {
|
|
||||||
_DBG("released");
|
|
||||||
// notify listeners
|
|
||||||
for (DigitalButtonListener listener : listeners) {
|
|
||||||
listener.onRelease();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We may be called for a release without a prior click
|
|
||||||
if (timerLongClick != null) {
|
|
||||||
timerLongClick.cancel();
|
|
||||||
}
|
|
||||||
if (longClickTimerTask != null) {
|
|
||||||
longClickTimerTask.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onElementTouchEvent(MotionEvent event) {
|
|
||||||
// get masked (not specific to a pointer) action
|
|
||||||
float x = getX() + event.getX();
|
|
||||||
float y = getY() + event.getY();
|
|
||||||
int action = event.getActionMasked();
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
|
||||||
movingButton = null;
|
|
||||||
setPressed(true);
|
|
||||||
onClickCallback();
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_MOVE: {
|
|
||||||
checkMovementForAllButtons(x, y);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_POINTER_UP: {
|
|
||||||
setPressed(false);
|
|
||||||
onReleaseCallback();
|
|
||||||
|
|
||||||
checkMovementForAllButtons(x, y);
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class DigitalPad extends VirtualControllerElement {
|
|
||||||
public final static int DIGITAL_PAD_DIRECTION_NO_DIRECTION = 0;
|
|
||||||
int direction = DIGITAL_PAD_DIRECTION_NO_DIRECTION;
|
|
||||||
public final static int DIGITAL_PAD_DIRECTION_LEFT = 1;
|
|
||||||
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
|
|
||||||
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
|
|
||||||
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
|
|
||||||
List<DigitalPadListener> listeners = new ArrayList<>();
|
|
||||||
|
|
||||||
private static final int DPAD_MARGIN = 5;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
|
||||||
|
|
||||||
public DigitalPad(VirtualController controller, Context context) {
|
|
||||||
super(controller, context, EID_DPAD);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addDigitalPadListener(DigitalPadListener listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onElementDraw(Canvas canvas) {
|
|
||||||
// set transparent background
|
|
||||||
canvas.drawColor(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
paint.setTextSize(getPercent(getCorrectWidth(), 20));
|
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
|
||||||
|
|
||||||
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
|
||||||
// draw no direction rect
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
paint.setColor(getDefaultColor());
|
|
||||||
canvas.drawRect(
|
|
||||||
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
|
|
||||||
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw left rect
|
|
||||||
paint.setColor(
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(
|
|
||||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
|
||||||
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// draw up rect
|
|
||||||
paint.setColor(
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(
|
|
||||||
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
|
||||||
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw right rect
|
|
||||||
paint.setColor(
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(
|
|
||||||
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
|
||||||
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw down rect
|
|
||||||
paint.setColor(
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(
|
|
||||||
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
|
||||||
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw left up line
|
|
||||||
paint.setColor((
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
|
|
||||||
) ? pressedColor : getDefaultColor()
|
|
||||||
);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawLine(
|
|
||||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
|
||||||
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw up right line
|
|
||||||
paint.setColor((
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
|
|
||||||
) ? pressedColor : getDefaultColor()
|
|
||||||
);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawLine(
|
|
||||||
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
|
|
||||||
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw right down line
|
|
||||||
paint.setColor((
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
|
|
||||||
) ? pressedColor : getDefaultColor()
|
|
||||||
);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawLine(
|
|
||||||
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
|
|
||||||
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw down left line
|
|
||||||
paint.setColor((
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
|
|
||||||
) ? pressedColor : getDefaultColor()
|
|
||||||
);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawLine(
|
|
||||||
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
|
||||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void newDirectionCallback(int direction) {
|
|
||||||
_DBG("direction: " + direction);
|
|
||||||
|
|
||||||
// notify listeners
|
|
||||||
for (DigitalPadListener listener : listeners) {
|
|
||||||
listener.onDirectionChange(direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onElementTouchEvent(MotionEvent event) {
|
|
||||||
// get masked (not specific to a pointer) action
|
|
||||||
switch (event.getActionMasked()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN:
|
|
||||||
case MotionEvent.ACTION_MOVE: {
|
|
||||||
direction = 0;
|
|
||||||
|
|
||||||
if (event.getX() < getPercent(getWidth(), 33)) {
|
|
||||||
direction |= DIGITAL_PAD_DIRECTION_LEFT;
|
|
||||||
}
|
|
||||||
if (event.getX() > getPercent(getWidth(), 66)) {
|
|
||||||
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
|
|
||||||
}
|
|
||||||
if (event.getY() > getPercent(getHeight(), 66)) {
|
|
||||||
direction |= DIGITAL_PAD_DIRECTION_DOWN;
|
|
||||||
}
|
|
||||||
if (event.getY() < getPercent(getHeight(), 33)) {
|
|
||||||
direction |= DIGITAL_PAD_DIRECTION_UP;
|
|
||||||
}
|
|
||||||
newDirectionCallback(direction);
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_POINTER_UP: {
|
|
||||||
direction = 0;
|
|
||||||
newDirectionCallback(direction);
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface DigitalPadListener {
|
|
||||||
void onDirectionChange(int direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
|
|
||||||
public class LeftAnalogStick extends AnalogStick {
|
|
||||||
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
|
||||||
super(controller, context, EID_LS);
|
|
||||||
|
|
||||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
|
||||||
@Override
|
|
||||||
public void onMovement(float x, float y) {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.leftStickX = (short) (x * 0x7FFE);
|
|
||||||
inputContext.leftStickY = (short) (y * 0x7FFE);
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDoubleClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRevoke() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public class LeftTrigger extends DigitalButton {
|
|
||||||
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
|
||||||
super(controller, EID_LT, layer, context);
|
|
||||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.leftTrigger = (byte) 0xFF;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLongClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRelease() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.leftTrigger = (byte) 0x00;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
|
|
||||||
public class RightAnalogStick extends AnalogStick {
|
|
||||||
public RightAnalogStick(final VirtualController controller, final Context context) {
|
|
||||||
super(controller, context, EID_RS);
|
|
||||||
|
|
||||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
|
||||||
@Override
|
|
||||||
public void onMovement(float x, float y) {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.rightStickX = (short) (x * 0x7FFE);
|
|
||||||
inputContext.rightStickY = (short) (y * 0x7FFE);
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDoubleClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRevoke() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public class RightTrigger extends DigitalButton {
|
|
||||||
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
|
||||||
super(controller, EID_RT, layer, context);
|
|
||||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.rightTrigger = (byte) 0xFF;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLongClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRelease() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.rightTrigger = (byte) 0x00;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-189
@@ -1,189 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.binding.input.ControllerHandler;
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
|
|
||||||
public class VirtualController {
|
|
||||||
public class ControllerInputContext {
|
|
||||||
public short inputMap = 0x0000;
|
|
||||||
public byte leftTrigger = 0x00;
|
|
||||||
public byte rightTrigger = 0x00;
|
|
||||||
public short rightStickX = 0x0000;
|
|
||||||
public short rightStickY = 0x0000;
|
|
||||||
public short leftStickX = 0x0000;
|
|
||||||
public short leftStickY = 0x0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ControllerMode {
|
|
||||||
Active,
|
|
||||||
Configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
|
||||||
|
|
||||||
private ControllerHandler controllerHandler;
|
|
||||||
private Context context = null;
|
|
||||||
|
|
||||||
private FrameLayout frame_layout = null;
|
|
||||||
private RelativeLayout relative_layout = null;
|
|
||||||
|
|
||||||
private Timer retransmitTimer;
|
|
||||||
|
|
||||||
ControllerMode currentMode = ControllerMode.Active;
|
|
||||||
ControllerInputContext inputContext = new ControllerInputContext();
|
|
||||||
|
|
||||||
private Button buttonConfigure = null;
|
|
||||||
|
|
||||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
|
||||||
|
|
||||||
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
|
|
||||||
this.controllerHandler = controllerHandler;
|
|
||||||
this.frame_layout = layout;
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
relative_layout = new RelativeLayout(context);
|
|
||||||
|
|
||||||
frame_layout.addView(relative_layout);
|
|
||||||
|
|
||||||
buttonConfigure = new Button(context);
|
|
||||||
buttonConfigure.setAlpha(0.25f);
|
|
||||||
buttonConfigure.setFocusable(false);
|
|
||||||
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
|
||||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
String message;
|
|
||||||
|
|
||||||
if (currentMode == ControllerMode.Configuration) {
|
|
||||||
currentMode = ControllerMode.Active;
|
|
||||||
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
|
||||||
message = "Exiting configuration mode";
|
|
||||||
} else {
|
|
||||||
currentMode = ControllerMode.Configuration;
|
|
||||||
message = "Entering configuration mode";
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
relative_layout.invalidate();
|
|
||||||
|
|
||||||
for (VirtualControllerElement element : elements) {
|
|
||||||
element.invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void hide() {
|
|
||||||
retransmitTimer.cancel();
|
|
||||||
relative_layout.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show() {
|
|
||||||
relative_layout.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
// HACK: GFE sometimes discards gamepad packets when they are received
|
|
||||||
// very shortly after another. This can be critical if an axis zeroing packet
|
|
||||||
// is lost and causes an analog stick to get stuck. To avoid this, we send
|
|
||||||
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
|
|
||||||
retransmitTimer = new Timer("OSC timer", true);
|
|
||||||
retransmitTimer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
sendControllerInputContext();
|
|
||||||
}
|
|
||||||
}, 100, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeElements() {
|
|
||||||
for (VirtualControllerElement element : elements) {
|
|
||||||
relative_layout.removeView(element);
|
|
||||||
}
|
|
||||||
elements.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
|
||||||
elements.add(element);
|
|
||||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
|
||||||
layoutParams.setMargins(x, y, 0, 0);
|
|
||||||
|
|
||||||
relative_layout.addView(element, layoutParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<VirtualControllerElement> getElements() {
|
|
||||||
return elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final void _DBG(String text) {
|
|
||||||
if (_PRINT_DEBUG_INFORMATION) {
|
|
||||||
System.out.println("VirtualController: " + text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshLayout() {
|
|
||||||
relative_layout.removeAllViews();
|
|
||||||
removeElements();
|
|
||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
|
||||||
|
|
||||||
int buttonSize = (int)(screen.heightPixels*0.06f);
|
|
||||||
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
|
||||||
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
|
|
||||||
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
|
|
||||||
params.leftMargin = 15;
|
|
||||||
params.topMargin = 15;
|
|
||||||
relative_layout.addView(buttonConfigure, params);
|
|
||||||
|
|
||||||
// Start with the default layout
|
|
||||||
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
|
||||||
|
|
||||||
// Apply user preferences onto the default layout
|
|
||||||
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ControllerMode getControllerMode() {
|
|
||||||
return currentMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ControllerInputContext getControllerInputContext() {
|
|
||||||
return inputContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendControllerInputContext() {
|
|
||||||
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
|
||||||
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
|
||||||
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
|
||||||
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
|
||||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
|
||||||
|
|
||||||
if (controllerHandler != null) {
|
|
||||||
controllerHandler.reportOscState(
|
|
||||||
inputContext.inputMap,
|
|
||||||
inputContext.leftStickX,
|
|
||||||
inputContext.leftStickY,
|
|
||||||
inputContext.rightStickX,
|
|
||||||
inputContext.rightStickY,
|
|
||||||
inputContext.leftTrigger,
|
|
||||||
inputContext.rightTrigger
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-328
@@ -1,328 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public class VirtualControllerConfigurationLoader {
|
|
||||||
public static final String OSC_PREFERENCE = "OSC";
|
|
||||||
|
|
||||||
private static int getPercent(
|
|
||||||
int percent,
|
|
||||||
int total) {
|
|
||||||
return (int) (((float) total / (float) 100) * (float) percent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DigitalPad createDigitalPad(
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
|
|
||||||
DigitalPad digitalPad = new DigitalPad(controller, context);
|
|
||||||
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
|
|
||||||
@Override
|
|
||||||
public void onDirectionChange(int direction) {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
|
|
||||||
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
|
|
||||||
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
|
|
||||||
}
|
|
||||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
|
|
||||||
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
|
|
||||||
}
|
|
||||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
|
|
||||||
inputContext.inputMap |= ControllerPacket.UP_FLAG;
|
|
||||||
}
|
|
||||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
|
|
||||||
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
|
|
||||||
}
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return digitalPad;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DigitalButton createDigitalButton(
|
|
||||||
final int elementId,
|
|
||||||
final int keyShort,
|
|
||||||
final int keyLong,
|
|
||||||
final int layer,
|
|
||||||
final String text,
|
|
||||||
final int icon,
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
DigitalButton button = new DigitalButton(controller, elementId, layer, context);
|
|
||||||
button.setText(text);
|
|
||||||
button.setIcon(icon);
|
|
||||||
|
|
||||||
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap |= keyShort;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLongClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap |= keyLong;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRelease() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap &= ~keyShort;
|
|
||||||
inputContext.inputMap &= ~keyLong;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DigitalButton createLeftTrigger(
|
|
||||||
final int layer,
|
|
||||||
final String text,
|
|
||||||
final int icon,
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
LeftTrigger button = new LeftTrigger(controller, layer, context);
|
|
||||||
button.setText(text);
|
|
||||||
button.setIcon(icon);
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DigitalButton createRightTrigger(
|
|
||||||
final int layer,
|
|
||||||
final String text,
|
|
||||||
final int icon,
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
RightTrigger button = new RightTrigger(controller, layer, context);
|
|
||||||
button.setText(text);
|
|
||||||
button.setIcon(icon);
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AnalogStick createLeftStick(
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
return new LeftAnalogStick(controller, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AnalogStick createRightStick(
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
return new RightAnalogStick(controller, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int BUTTON_BASE_X = 65;
|
|
||||||
private static final int BUTTON_BASE_Y = 5;
|
|
||||||
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
|
||||||
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
|
||||||
|
|
||||||
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
|
||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
|
||||||
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
|
||||||
|
|
||||||
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
|
||||||
// into a single call. Due to floating point rounding, this isn't actually possible.
|
|
||||||
|
|
||||||
if (!config.onlyL3R3)
|
|
||||||
{
|
|
||||||
controller.addElement(createDigitalPad(controller, context),
|
|
||||||
getPercent(5, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
|
||||||
getPercent(30, screen.widthPixels),
|
|
||||||
getPercent(40, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_A,
|
|
||||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_B,
|
|
||||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_X,
|
|
||||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_Y,
|
|
||||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createLeftTrigger(
|
|
||||||
0, "LT", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createRightTrigger(
|
|
||||||
0, "RT", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_LB,
|
|
||||||
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_RB,
|
|
||||||
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createLeftStick(controller, context),
|
|
||||||
getPercent(5, screen.widthPixels),
|
|
||||||
getPercent(50, screen.heightPixels),
|
|
||||||
getPercent(40, screen.widthPixels),
|
|
||||||
getPercent(50, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createRightStick(controller, context),
|
|
||||||
getPercent(55, screen.widthPixels),
|
|
||||||
getPercent(50, screen.heightPixels),
|
|
||||||
getPercent(40, screen.widthPixels),
|
|
||||||
getPercent(50, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_BACK,
|
|
||||||
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
|
||||||
getPercent(40, screen.widthPixels),
|
|
||||||
getPercent(90, screen.heightPixels),
|
|
||||||
getPercent(10, screen.widthPixels),
|
|
||||||
getPercent(10, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_START,
|
|
||||||
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
|
||||||
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
|
|
||||||
getPercent(90, screen.heightPixels),
|
|
||||||
getPercent(10, screen.widthPixels),
|
|
||||||
getPercent(10, screen.heightPixels)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_LSB,
|
|
||||||
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
|
||||||
getPercent(2, screen.widthPixels),
|
|
||||||
getPercent(80, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_RSB,
|
|
||||||
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
|
||||||
getPercent(89, screen.widthPixels),
|
|
||||||
getPercent(80, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void saveProfile(final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
|
|
||||||
|
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
|
||||||
String prefKey = ""+element.elementId;
|
|
||||||
try {
|
|
||||||
prefEditor.putString(prefKey, element.getConfiguration().toString());
|
|
||||||
} catch (JSONException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prefEditor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
|
||||||
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
|
||||||
|
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
|
||||||
String prefKey = ""+element.elementId;
|
|
||||||
|
|
||||||
String jsonConfig = pref.getString(prefKey, null);
|
|
||||||
if (jsonConfig != null) {
|
|
||||||
try {
|
|
||||||
element.loadConfiguration(new JSONObject(jsonConfig));
|
|
||||||
} catch (JSONException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
|
|
||||||
// Remove the corrupt element from the preferences
|
|
||||||
pref.edit().remove(prefKey).apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-322
@@ -1,322 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public abstract class VirtualControllerElement extends View {
|
|
||||||
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
|
||||||
|
|
||||||
public static final int EID_DPAD = 1;
|
|
||||||
public static final int EID_LT = 2;
|
|
||||||
public static final int EID_RT = 3;
|
|
||||||
public static final int EID_LB = 4;
|
|
||||||
public static final int EID_RB = 5;
|
|
||||||
public static final int EID_A = 6;
|
|
||||||
public static final int EID_B = 7;
|
|
||||||
public static final int EID_X = 8;
|
|
||||||
public static final int EID_Y = 9;
|
|
||||||
public static final int EID_BACK = 10;
|
|
||||||
public static final int EID_START = 11;
|
|
||||||
public static final int EID_LS = 12;
|
|
||||||
public static final int EID_RS = 13;
|
|
||||||
public static final int EID_LSB = 14;
|
|
||||||
public static final int EID_RSB = 15;
|
|
||||||
|
|
||||||
protected VirtualController virtualController;
|
|
||||||
protected final int elementId;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
|
||||||
|
|
||||||
private int normalColor = 0xF0888888;
|
|
||||||
protected int pressedColor = 0xF00000FF;
|
|
||||||
private int configNormalColor = 0xF0FF0000;
|
|
||||||
private int configSelectedColor = 0xF000FF00;
|
|
||||||
|
|
||||||
protected int startSize_x;
|
|
||||||
protected int startSize_y;
|
|
||||||
|
|
||||||
float position_pressed_x = 0;
|
|
||||||
float position_pressed_y = 0;
|
|
||||||
|
|
||||||
private enum Mode {
|
|
||||||
Normal,
|
|
||||||
Resize,
|
|
||||||
Move
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mode currentMode = Mode.Normal;
|
|
||||||
|
|
||||||
protected VirtualControllerElement(VirtualController controller, Context context, int elementId) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
this.virtualController = controller;
|
|
||||||
this.elementId = elementId;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
|
||||||
int newPos_x = (int) getX() + x - pressed_x;
|
|
||||||
int newPos_y = (int) getY() + y - pressed_y;
|
|
||||||
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
|
||||||
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
|
||||||
layoutParams.rightMargin = 0;
|
|
||||||
layoutParams.bottomMargin = 0;
|
|
||||||
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
int newHeight = height + (startSize_y - pressed_y);
|
|
||||||
int newWidth = width + (startSize_x - pressed_x);
|
|
||||||
|
|
||||||
layoutParams.height = newHeight > 20 ? newHeight : 20;
|
|
||||||
layoutParams.width = newWidth > 20 ? newWidth : 20;
|
|
||||||
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDraw(Canvas canvas) {
|
|
||||||
onElementDraw(canvas);
|
|
||||||
|
|
||||||
if (currentMode != Mode.Normal) {
|
|
||||||
paint.setColor(configSelectedColor);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
|
|
||||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
|
||||||
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
|
|
||||||
paint);
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onDraw(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
protected void actionShowNormalColorChooser() {
|
|
||||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
|
||||||
@Override
|
|
||||||
public void onCancel(AmbilWarnaDialog dialog)
|
|
||||||
{}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
|
||||||
normalColor = color;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
colorDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void actionShowPressedColorChooser() {
|
|
||||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
|
||||||
@Override
|
|
||||||
public void onCancel(AmbilWarnaDialog dialog) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
|
||||||
pressedColor = color;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
colorDialog.show();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
protected void actionEnableMove() {
|
|
||||||
currentMode = Mode.Move;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void actionEnableResize() {
|
|
||||||
currentMode = Mode.Resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void actionCancel() {
|
|
||||||
currentMode = Mode.Normal;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getDefaultColor() {
|
|
||||||
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
|
||||||
configNormalColor : normalColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getDefaultStrokeWidth() {
|
|
||||||
DisplayMetrics screen = getResources().getDisplayMetrics();
|
|
||||||
return (int)(screen.heightPixels*0.004f);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void showConfigurationDialog() {
|
|
||||||
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
|
||||||
|
|
||||||
alertBuilder.setTitle("Configuration");
|
|
||||||
|
|
||||||
CharSequence functions[] = new CharSequence[]{
|
|
||||||
"Move",
|
|
||||||
"Resize",
|
|
||||||
/*election
|
|
||||||
"Set n
|
|
||||||
Disable color sormal color",
|
|
||||||
"Set pressed color",
|
|
||||||
*/
|
|
||||||
"Cancel"
|
|
||||||
};
|
|
||||||
|
|
||||||
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
switch (which) {
|
|
||||||
case 0: { // move
|
|
||||||
actionEnableMove();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 1: { // resize
|
|
||||||
actionEnableResize();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
case 2: { // set default color
|
|
||||||
actionShowNormalColorChooser();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3: { // set pressed color
|
|
||||||
actionShowPressedColorChooser();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
default: { // cancel
|
|
||||||
actionCancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
AlertDialog alert = alertBuilder.create();
|
|
||||||
// show menu
|
|
||||||
alert.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
|
||||||
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
|
|
||||||
return onElementTouchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.getActionMasked()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
|
||||||
position_pressed_x = event.getX();
|
|
||||||
position_pressed_y = event.getY();
|
|
||||||
startSize_x = getWidth();
|
|
||||||
startSize_y = getHeight();
|
|
||||||
|
|
||||||
actionEnableMove();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_MOVE: {
|
|
||||||
switch (currentMode) {
|
|
||||||
case Move: {
|
|
||||||
moveElement(
|
|
||||||
(int) position_pressed_x,
|
|
||||||
(int) position_pressed_y,
|
|
||||||
(int) event.getX(),
|
|
||||||
(int) event.getY());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Resize: {
|
|
||||||
resizeElement(
|
|
||||||
(int) position_pressed_x,
|
|
||||||
(int) position_pressed_y,
|
|
||||||
(int) event.getX(),
|
|
||||||
(int) event.getY());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Normal: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_POINTER_UP: {
|
|
||||||
actionCancel();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract protected void onElementDraw(Canvas canvas);
|
|
||||||
|
|
||||||
abstract public boolean onElementTouchEvent(MotionEvent event);
|
|
||||||
|
|
||||||
protected static final void _DBG(String text) {
|
|
||||||
if (_PRINT_DEBUG_INFORMATION) {
|
|
||||||
System.out.println(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColors(int normalColor, int pressedColor) {
|
|
||||||
this.normalColor = normalColor;
|
|
||||||
this.pressedColor = pressedColor;
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final float getPercent(float value, float percent) {
|
|
||||||
return value / 100 * percent;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final int getCorrectWidth() {
|
|
||||||
return getWidth() > getHeight() ? getHeight() : getWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public JSONObject getConfiguration() throws JSONException {
|
|
||||||
JSONObject configuration = new JSONObject();
|
|
||||||
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
configuration.put("LEFT", layoutParams.leftMargin);
|
|
||||||
configuration.put("TOP", layoutParams.topMargin);
|
|
||||||
configuration.put("WIDTH", layoutParams.width);
|
|
||||||
configuration.put("HEIGHT", layoutParams.height);
|
|
||||||
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadConfiguration(JSONObject configuration) throws JSONException {
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
layoutParams.leftMargin = configuration.getInt("LEFT");
|
|
||||||
layoutParams.topMargin = configuration.getInt("TOP");
|
|
||||||
layoutParams.width = configuration.getInt("WIDTH");
|
|
||||||
layoutParams.height = configuration.getInt("HEIGHT");
|
|
||||||
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
public interface CrashListener {
|
|
||||||
void notifyCrash(Exception e);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,624 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ConfigurationInfo;
|
|
||||||
import android.media.MediaCodecInfo;
|
|
||||||
import android.media.MediaCodecList;
|
|
||||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
|
||||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
|
|
||||||
public class MediaCodecHelper {
|
|
||||||
|
|
||||||
private static final List<String> preferredDecoders;
|
|
||||||
|
|
||||||
private static final List<String> blacklistedDecoderPrefixes;
|
|
||||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
|
||||||
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
|
|
||||||
private static final List<String> deprioritizedHevcDecoders;
|
|
||||||
private static final List<String> baselineProfileHackPrefixes;
|
|
||||||
private static final List<String> directSubmitPrefixes;
|
|
||||||
private static final List<String> constrainedHighProfilePrefixes;
|
|
||||||
private static final List<String> whitelistedHevcDecoders;
|
|
||||||
private static final List<String> refFrameInvalidationAvcPrefixes;
|
|
||||||
private static final List<String> refFrameInvalidationHevcPrefixes;
|
|
||||||
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragon = false;
|
|
||||||
private static boolean initialized = false;
|
|
||||||
|
|
||||||
static {
|
|
||||||
directSubmitPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// These decoders have low enough input buffer latency that they
|
|
||||||
// can be directly invoked from the receive thread
|
|
||||||
directSubmitPrefixes.add("omx.qcom");
|
|
||||||
directSubmitPrefixes.add("omx.sec");
|
|
||||||
directSubmitPrefixes.add("omx.exynos");
|
|
||||||
directSubmitPrefixes.add("omx.intel");
|
|
||||||
directSubmitPrefixes.add("omx.brcm");
|
|
||||||
directSubmitPrefixes.add("omx.TI");
|
|
||||||
directSubmitPrefixes.add("omx.arc");
|
|
||||||
directSubmitPrefixes.add("omx.nvidia");
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
refFrameInvalidationAvcPrefixes = new LinkedList<>();
|
|
||||||
refFrameInvalidationHevcPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// Qualcomm and NVIDIA may be added at runtime
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
preferredDecoders = new LinkedList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// Blacklist software decoders that don't support H264 high profile,
|
|
||||||
// but exclude the official AOSP and CrOS emulator from this restriction.
|
|
||||||
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
|
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Never use ffmpeg decoders since they're software decoders
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
|
|
||||||
|
|
||||||
// Force these decoders disabled because:
|
|
||||||
// 1) They are software decoders, so the performance is terrible
|
|
||||||
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
// If a decoder qualifies for reference frame invalidation,
|
|
||||||
// these entries will be ignored for those decoders.
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
|
||||||
|
|
||||||
baselineProfileHackPrefixes = new LinkedList<>();
|
|
||||||
baselineProfileHackPrefixes.add("omx.intel");
|
|
||||||
|
|
||||||
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
|
|
||||||
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
|
|
||||||
// if adaptive playback was enabled so let's avoid it to be safe.
|
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
|
||||||
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
|
|
||||||
// on some Android TV devices with H.265 only.
|
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
|
|
||||||
|
|
||||||
constrainedHighProfilePrefixes = new LinkedList<>();
|
|
||||||
constrainedHighProfilePrefixes.add("omx.intel");
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
whitelistedHevcDecoders = new LinkedList<>();
|
|
||||||
|
|
||||||
// Allow software HEVC decoding in the official AOSP emulator
|
|
||||||
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.google");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exynos seems to be the only HEVC decoder that works reliably
|
|
||||||
whitelistedHevcDecoders.add("omx.exynos");
|
|
||||||
|
|
||||||
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
|
|
||||||
// For some reason, other X1 implementations require bitstream fixups.
|
|
||||||
if (Build.DEVICE.equalsIgnoreCase("darcy")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.nvidia");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
|
||||||
// I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
|
|
||||||
// to actually be tested.
|
|
||||||
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
|
||||||
whitelistedHevcDecoders.add("omx.amlogic");
|
|
||||||
}
|
|
||||||
|
|
||||||
// These theoretically have good HEVC decoding capabilities (potentially better than
|
|
||||||
// their AVC decoders), but haven't been tested enough
|
|
||||||
//whitelistedHevcDecoders.add("omx.amlogic");
|
|
||||||
//whitelistedHevcDecoders.add("omx.rk");
|
|
||||||
|
|
||||||
// Based on GPU attributes queried at runtime, the omx.qcom prefix will be added
|
|
||||||
// during initialization to avoid SoCs with broken HEVC decoders.
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
deprioritizedHevcDecoders = new LinkedList<>();
|
|
||||||
|
|
||||||
// These are decoders that work but aren't used by default for various reasons.
|
|
||||||
|
|
||||||
// Qualcomm is currently the only decoders in this group.
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// We see a bunch of crashes on MediaTek Android TVs running
|
|
||||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
|
||||||
// these devices and hope they fix it in Pie.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
||||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPowerVR(String glRenderer) {
|
|
||||||
return glRenderer.toLowerCase().contains("powervr");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getAdrenoVersionString(String glRenderer) {
|
|
||||||
glRenderer = glRenderer.toLowerCase().trim();
|
|
||||||
|
|
||||||
if (!glRenderer.contains("adreno")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
|
|
||||||
|
|
||||||
Matcher matcher = modelNumberPattern.matcher(glRenderer);
|
|
||||||
if (!matcher.matches()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String modelNumber = matcher.group(2);
|
|
||||||
LimeLog.info("Found Adreno GPU: "+modelNumber);
|
|
||||||
return modelNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
|
|
||||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
|
||||||
if (modelNumber == null) {
|
|
||||||
// Not an Adreno GPU
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
|
|
||||||
return modelNumber.charAt(1) == '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a workaround for some broken devices that report
|
|
||||||
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
|
|
||||||
// An example of such a device is the Huawei Honor 5x with the
|
|
||||||
// Snapdragon 616 SoC (Adreno 405).
|
|
||||||
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
|
|
||||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
|
||||||
if (modelNumber == null) {
|
|
||||||
// Not an Adreno GPU
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapdragon 4xx and higher support GLES 3.1
|
|
||||||
return modelNumber.charAt(0) >= '4';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initialize(Context context, String glRenderer) {
|
|
||||||
if (initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityManager activityManager =
|
|
||||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
|
||||||
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
|
||||||
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
|
||||||
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
|
||||||
|
|
||||||
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
|
|
||||||
|
|
||||||
// Tegra K1 and later can do reference frame invalidation properly
|
|
||||||
if (configInfo.reqGlEsVersion >= 0x30000) {
|
|
||||||
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
|
||||||
|
|
||||||
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
|
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
|
||||||
|
|
||||||
// Prior to M, we were tricking the decoder into using baseline profile, which
|
|
||||||
// won't support RFI properly.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
|
||||||
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
|
||||||
// 3xx - bad
|
|
||||||
// 4xx - good
|
|
||||||
//
|
|
||||||
// The "good" GPUs support GLES 3.1, but we can't just check that directly
|
|
||||||
// (see comment on isGLES31SnapdragonRenderer).
|
|
||||||
//
|
|
||||||
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
|
||||||
// We prefer reference frame invalidation support (which is only doable on AVC on
|
|
||||||
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
|
||||||
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
|
|
||||||
// HEVC anyway.
|
|
||||||
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
|
|
||||||
deprioritizedHevcDecoders.add("omx.qcom");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
|
|
||||||
// PowerVR GPUs have good HEVC support.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) {
|
|
||||||
LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU");
|
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
|
||||||
|
|
||||||
// This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting
|
|
||||||
// required to make it work adds a huge amount of latency.
|
|
||||||
LimeLog.info("Added omx.mtk to RFI list for HEVC");
|
|
||||||
refFrameInvalidationHevcPrefixes.add("omx.mtk");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
|
||||||
if (!initialized) {
|
|
||||||
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String badPrefix : decoderList) {
|
|
||||||
if (decoderName.length() >= badPrefix.length()) {
|
|
||||||
String prefix = decoderName.substring(0, badPrefix.length());
|
|
||||||
if (prefix.equalsIgnoreCase(badPrefix)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long getMonotonicMillis() {
|
|
||||||
return System.nanoTime() / 1000000L;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
|
|
||||||
// Possibly enable adaptive playback on KitKat and above
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
|
|
||||||
LimeLog.info("Decoder blacklisted for adaptive playback");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (decoderInfo.getCapabilitiesForType("video/avc").
|
|
||||||
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
|
||||||
{
|
|
||||||
// This will make getCapabilities() return that adaptive playback is supported
|
|
||||||
LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Tolerate buggy codecs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
|
|
||||||
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderCanDirectSubmit(String decoderName) {
|
|
||||||
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) {
|
|
||||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderNeedsBaselineSpsHack(String decoderName) {
|
|
||||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderBlacklistedFor49Fps(String decoderName) {
|
|
||||||
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
|
||||||
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
|
||||||
if (videoHeight > 720 && isLowEndSnapdragon) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This device seems to crash constantly at 720p, so try disabling
|
|
||||||
// RFI to see if we can get that under control.
|
|
||||||
if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) {
|
|
||||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
|
|
||||||
// TODO: Shield Tablet K1/LTE?
|
|
||||||
//
|
|
||||||
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
|
||||||
// whether the performance is good enough to use for streaming, but they're
|
|
||||||
// using the same omx.nvidia.h265.decode name as the Shield TV which has a
|
|
||||||
// fully accelerated HEVC pipeline. AFAIK, the only K1 device with this
|
|
||||||
// partially accelerated HEVC decoder is the Shield Tablet, so I'll
|
|
||||||
// check for it here.
|
|
||||||
//
|
|
||||||
// TODO: Temporarily disabled with NVIDIA HEVC support
|
|
||||||
/*if (Build.DEVICE.equalsIgnoreCase("shieldtablet")) {
|
|
||||||
return false;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Google didn't have official support for HEVC (or more importantly, a CTS test) until
|
|
||||||
// Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC,
|
|
||||||
// so I'm restricting HEVC usage to Lollipop and higher.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Software decoders are terrible and we never want to use them.
|
|
||||||
// We want to catch decoders like:
|
|
||||||
// OMX.qcom.video.decoder.hevcswvdec
|
|
||||||
// OMX.SEC.hevc.sw.dec
|
|
||||||
//
|
|
||||||
if (decoderName.contains("sw")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some devices have HEVC decoders that we prefer not to use
|
|
||||||
// typically because it can't support reference frame invalidation.
|
|
||||||
// However, we will use it for HDR and for streaming over mobile networks
|
|
||||||
// since it works fine otherwise.
|
|
||||||
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
|
||||||
LimeLog.info("Selected deprioritized decoder");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
|
||||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
|
||||||
Collections.addAll(infoList, mcl.getCodecInfos());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
|
||||||
infoList.add(MediaCodecList.getCodecInfoAt(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return infoList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("RedundantThrows")
|
|
||||||
public static String dumpDecoders() throws Exception {
|
|
||||||
String str = "";
|
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
|
||||||
// Skip encoders
|
|
||||||
if (codecInfo.isEncoder()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
str += "Decoder: "+codecInfo.getName()+"\n";
|
|
||||||
for (String type : codecInfo.getSupportedTypes()) {
|
|
||||||
str += "\t"+type+"\n";
|
|
||||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(type);
|
|
||||||
|
|
||||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
|
||||||
str += "\t\t"+profile.profile+" "+profile.level+"\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaCodecInfo findPreferredDecoder() {
|
|
||||||
// This is a different algorithm than the other findXXXDecoder functions,
|
|
||||||
// because we want to evaluate the decoders in our list's order
|
|
||||||
// rather than MediaCodecList's order
|
|
||||||
|
|
||||||
if (!initialized) {
|
|
||||||
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String preferredDecoder : preferredDecoders) {
|
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
|
||||||
// Skip encoders
|
|
||||||
if (codecInfo.isEncoder()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for preferred decoders
|
|
||||||
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
|
|
||||||
LimeLog.info("Preferred decoder choice is "+codecInfo.getName());
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MediaCodecInfo findFirstDecoder(String mimeType) {
|
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
|
||||||
// Skip encoders
|
|
||||||
if (codecInfo.isEncoder()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for explicitly blacklisted decoders
|
|
||||||
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
|
||||||
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a decoder that supports the specified video format
|
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
|
||||||
if (mime.equalsIgnoreCase(mimeType)) {
|
|
||||||
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) {
|
|
||||||
// First look for a preferred decoder by name
|
|
||||||
MediaCodecInfo info = findPreferredDecoder();
|
|
||||||
if (info != null) {
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now look for decoders we know are safe
|
|
||||||
try {
|
|
||||||
// If this function completes, it will determine if the decoder is safe
|
|
||||||
return findKnownSafeDecoder(mimeType, requiredProfile);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Some buggy devices seem to throw exceptions
|
|
||||||
// from getCapabilitiesForType() so we'll just assume
|
|
||||||
// they're okay and go with the first one we find
|
|
||||||
return findFirstDecoder(mimeType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We declare this method as explicitly throwing Exception
|
|
||||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
|
||||||
// and we want to be sure all callers are handling this possibility
|
|
||||||
@SuppressWarnings("RedundantThrows")
|
|
||||||
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
|
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
|
||||||
// Skip encoders
|
|
||||||
if (codecInfo.isEncoder()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for explicitly blacklisted decoders
|
|
||||||
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
|
||||||
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a decoder that supports the requested video format
|
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
|
||||||
if (mime.equalsIgnoreCase(mimeType)) {
|
|
||||||
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
|
||||||
|
|
||||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
|
||||||
|
|
||||||
if (requiredProfile != -1) {
|
|
||||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
|
||||||
if (profile.profile == requiredProfile) {
|
|
||||||
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
package com.limelight.computers;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteException;
|
|
||||||
|
|
||||||
public class ComputerDatabaseManager {
|
|
||||||
private static final String COMPUTER_DB_NAME = "computers2.db";
|
|
||||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
|
||||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
|
||||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
|
||||||
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
|
|
||||||
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
|
|
||||||
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
|
|
||||||
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
|
||||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
|
||||||
|
|
||||||
public ComputerDatabaseManager(Context c) {
|
|
||||||
try {
|
|
||||||
// Create or open an existing DB
|
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
|
||||||
} catch (SQLiteException e) {
|
|
||||||
// Delete the DB and try again
|
|
||||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
|
||||||
}
|
|
||||||
initializeDb(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
computerDb.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeDb(Context c) {
|
|
||||||
// Add cert column to the table if not present
|
|
||||||
try {
|
|
||||||
computerDb.execSQL(String.format((Locale)null,
|
|
||||||
"ALTER TABLE %s ADD COLUMN %s TEXT",
|
|
||||||
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
|
|
||||||
} catch (SQLiteException e) {}
|
|
||||||
|
|
||||||
|
|
||||||
// Create tables if they aren't already there
|
|
||||||
computerDb.execSQL(String.format((Locale)null,
|
|
||||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
|
|
||||||
COMPUTER_TABLE_NAME,
|
|
||||||
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
|
||||||
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
|
|
||||||
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
|
||||||
|
|
||||||
// Move all computers from the old DB (if any) to the new one
|
|
||||||
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
|
||||||
for (ComputerDetails computer : oldComputers) {
|
|
||||||
updateComputer(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteComputer(String name) {
|
|
||||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
|
||||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
|
||||||
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
|
|
||||||
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
|
||||||
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
|
||||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
|
||||||
try {
|
|
||||||
if (details.serverCert != null) {
|
|
||||||
values.put(SERVER_CERT_COLUMN_NAME, details.serverCert.getEncoded());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
|
||||||
}
|
|
||||||
} catch (CertificateEncodingException e) {
|
|
||||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
|
|
||||||
details.uuid = c.getString(0);
|
|
||||||
details.name = c.getString(1);
|
|
||||||
details.localAddress = c.getString(2);
|
|
||||||
details.remoteAddress = c.getString(3);
|
|
||||||
details.manualAddress = c.getString(4);
|
|
||||||
details.macAddress = c.getString(5);
|
|
||||||
|
|
||||||
try {
|
|
||||||
byte[] derCertData = c.getBlob(6);
|
|
||||||
|
|
||||||
if (derCertData != null) {
|
|
||||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
|
||||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
|
||||||
}
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ComputerDetails> getAllComputers() {
|
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
|
||||||
|
|
||||||
// If a critical field is corrupt or missing, skip the database entry
|
|
||||||
if (details.uuid == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
computerList.add(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
c.close();
|
|
||||||
|
|
||||||
return computerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComputerDetails getComputerByUUID(String uuid) {
|
|
||||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
|
||||||
if (!c.moveToFirst()) {
|
|
||||||
// No matching computer
|
|
||||||
c.close();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
|
||||||
c.close();
|
|
||||||
|
|
||||||
// If a critical field is corrupt or missing, delete the database entry
|
|
||||||
if (details.uuid == null) {
|
|
||||||
deleteComputer(details.name);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.limelight.computers;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
|
|
||||||
public interface ComputerManagerListener {
|
|
||||||
void notifyComputerUpdated(ComputerDetails details);
|
|
||||||
}
|
|
||||||
@@ -1,827 +0,0 @@
|
|||||||
package com.limelight.computers;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.StringReader;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.discovery.DiscoveryService;
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
|
||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
|
||||||
import com.limelight.utils.CacheHelper;
|
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.IBinder;
|
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
public class ComputerManagerService extends Service {
|
|
||||||
private static final int SERVERINFO_POLLING_PERIOD_MS = 1500;
|
|
||||||
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
|
||||||
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
|
||||||
private static final int FAST_POLL_TIMEOUT = 1000;
|
|
||||||
private static final int OFFLINE_POLL_TRIES = 5;
|
|
||||||
private static final int INITIAL_POLL_TRIES = 2;
|
|
||||||
private static final int EMPTY_LIST_THRESHOLD = 3;
|
|
||||||
private static final int POLL_DATA_TTL_MS = 30000;
|
|
||||||
|
|
||||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
|
||||||
|
|
||||||
private ComputerDatabaseManager dbManager;
|
|
||||||
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
|
||||||
|
|
||||||
private IdentityManager idManager;
|
|
||||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<>();
|
|
||||||
private ComputerManagerListener listener = null;
|
|
||||||
private final 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, int offlineCount) throws InterruptedException {
|
|
||||||
if (!getLocalDatabaseReference()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int pollTriesBeforeOffline = details.state == ComputerDetails.State.UNKNOWN ?
|
|
||||||
INITIAL_POLL_TRIES : OFFLINE_POLL_TRIES;
|
|
||||||
|
|
||||||
activePolls.incrementAndGet();
|
|
||||||
|
|
||||||
// Poll the machine
|
|
||||||
try {
|
|
||||||
if (!pollComputer(details)) {
|
|
||||||
if (!newPc && offlineCount < pollTriesBeforeOffline) {
|
|
||||||
// Return without calling the listener
|
|
||||||
releaseLocalDatabaseReference();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
details.state = ComputerDetails.State.OFFLINE;
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
releaseLocalDatabaseReference();
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
activePolls.decrementAndGet();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's online, update our persistent state
|
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
|
||||||
ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
|
|
||||||
|
|
||||||
// Check if it's in the database because it could have been
|
|
||||||
// removed after this was issued
|
|
||||||
if (!newPc && existingComputer == null) {
|
|
||||||
// It's gone
|
|
||||||
releaseLocalDatabaseReference();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we already have an entry for this computer in the DB, we must
|
|
||||||
// combine the existing data with this new data (which may be partially available
|
|
||||||
// due to detecting the PC via mDNS) without the saved external address. If we
|
|
||||||
// write to the DB without doing this first, we can overwrite our existing data.
|
|
||||||
if (existingComputer != null) {
|
|
||||||
existingComputer.update(details);
|
|
||||||
dbManager.updateComputer(existingComputer);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
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 PollingTuple tuple) {
|
|
||||||
Thread t = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
|
|
||||||
int offlineCount = 0;
|
|
||||||
while (!isInterrupted() && pollingActive && tuple.thread == this) {
|
|
||||||
try {
|
|
||||||
// Only allow one request to the machine at a time
|
|
||||||
synchronized (tuple.networkLock) {
|
|
||||||
// Check if this poll has modified the details
|
|
||||||
if (!runPoll(tuple.computer, false, offlineCount)) {
|
|
||||||
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
|
||||||
offlineCount++;
|
|
||||||
} else {
|
|
||||||
tuple.lastSuccessfulPollMs = System.currentTimeMillis();
|
|
||||||
offlineCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait until the next polling interval
|
|
||||||
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
t.setName("Polling thread for " + tuple.computer.name);
|
|
||||||
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) {
|
|
||||||
// Enforce the poll data TTL
|
|
||||||
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
|
||||||
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
|
||||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report this computer initially
|
|
||||||
listener.notifyComputerUpdated(tuple.computer);
|
|
||||||
|
|
||||||
// This polling thread might already be there
|
|
||||||
if (tuple.thread == null) {
|
|
||||||
tuple.thread = createPollingThread(tuple);
|
|
||||||
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(String addr, boolean manuallyAdded) {
|
|
||||||
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
|
||||||
ComputerManagerService.this.removeComputer(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopPolling() {
|
|
||||||
// Just call the unbind handler to cleanup
|
|
||||||
ComputerManagerService.this.onUnbind(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApplistPoller createAppListPoller(ComputerDetails computer) {
|
|
||||||
return new ApplistPoller(computer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUniqueId() {
|
|
||||||
return idManager.getUniqueId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComputerDetails getComputer(String uuid) {
|
|
||||||
synchronized (pollingTuples) {
|
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
|
||||||
if (uuid.equals(tuple.computer.uuid)) {
|
|
||||||
return tuple.computer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void invalidateStateForComputer(String uuid) {
|
|
||||||
synchronized (pollingTuples) {
|
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
|
||||||
if (uuid.equals(tuple.computer.uuid)) {
|
|
||||||
// We need the network lock to prevent a concurrent poll
|
|
||||||
// from wiping this change out
|
|
||||||
synchronized (tuple.networkLock) {
|
|
||||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onUnbind(Intent intent) {
|
|
||||||
if (discoveryBinder != null) {
|
|
||||||
// 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
|
|
||||||
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
|
||||||
// Nothing to do here
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyDiscoveryFailure(Exception e) {
|
|
||||||
LimeLog.severe("mDNS discovery failed");
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addTuple(ComputerDetails details) {
|
|
||||||
synchronized (pollingTuples) {
|
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
|
||||||
// Check if this is the same computer
|
|
||||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
|
||||||
// Update the saved computer with potentially new details
|
|
||||||
tuple.computer.update(details);
|
|
||||||
|
|
||||||
// Start a polling thread if polling is active
|
|
||||||
if (pollingActive && tuple.thread == null) {
|
|
||||||
tuple.thread = createPollingThread(tuple);
|
|
||||||
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, null);
|
|
||||||
if (pollingActive) {
|
|
||||||
tuple.thread = createPollingThread(tuple);
|
|
||||||
}
|
|
||||||
pollingTuples.add(tuple);
|
|
||||||
if (tuple.thread != null) {
|
|
||||||
tuple.thread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
|
||||||
// Setup a placeholder
|
|
||||||
ComputerDetails fakeDetails = new ComputerDetails();
|
|
||||||
|
|
||||||
if (manuallyAdded) {
|
|
||||||
// Add PC UI
|
|
||||||
fakeDetails.manualAddress = addr;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// mDNS
|
|
||||||
fakeDetails.localAddress = addr;
|
|
||||||
|
|
||||||
// Since we're on the same network, we can use STUN to find
|
|
||||||
// our WAN address, which is also very likely the WAN address
|
|
||||||
// of the PC. We can use this later to connect remotely.
|
|
||||||
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block while we try to fill the details
|
|
||||||
try {
|
|
||||||
// We cannot use runPoll() here because it will attempt to persist the state of the machine
|
|
||||||
// in the database, which would be bad because we don't have our pinned cert loaded yet.
|
|
||||||
if (pollComputer(fakeDetails)) {
|
|
||||||
// See if we have record of this PC to pull its pinned cert
|
|
||||||
synchronized (pollingTuples) {
|
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
|
||||||
if (tuple.computer.uuid.equals(fakeDetails.uuid)) {
|
|
||||||
fakeDetails.serverCert = tuple.computer.serverCert;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poll again, possibly with the pinned cert, to get accurate pairing information.
|
|
||||||
// This will insert the host into the database too.
|
|
||||||
runPoll(fakeDetails, true, 0);
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the machine is reachable, it was successful
|
|
||||||
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
|
||||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
|
||||||
|
|
||||||
// Start a polling thread for this machine
|
|
||||||
addTuple(fakeDetails);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (!manuallyAdded) {
|
|
||||||
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
tuple.thread = null;
|
|
||||||
}
|
|
||||||
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, String address) {
|
|
||||||
// Fast poll this address first to determine if we can connect at the TCP layer
|
|
||||||
if (!fastPollIp(address)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
|
||||||
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
|
||||||
|
|
||||||
ComputerDetails newDetails = http.getComputerDetails();
|
|
||||||
|
|
||||||
// Check if this is the PC we expected
|
|
||||||
if (newDetails.uuid == null) {
|
|
||||||
LimeLog.severe("Polling returned no UUID!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// details.uuid can be null on initial PC add
|
|
||||||
else if (details.uuid != null && !details.uuid.equals(newDetails.uuid)) {
|
|
||||||
// We got the wrong PC!
|
|
||||||
LimeLog.info("Polling returned the wrong PC!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the new active address
|
|
||||||
newDetails.activeAddress = address;
|
|
||||||
|
|
||||||
return newDetails;
|
|
||||||
} catch (XmlPullParserException | IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just try to establish a TCP connection to speculatively detect a running
|
|
||||||
// GFE server
|
|
||||||
private boolean fastPollIp(String address) {
|
|
||||||
if (address == null) {
|
|
||||||
// Don't bother if our address is null
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Socket s = new Socket();
|
|
||||||
try {
|
|
||||||
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
|
||||||
s.close();
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startFastPollThread(final String address, final boolean[] info) {
|
|
||||||
Thread t = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean pollRes = fastPollIp(address);
|
|
||||||
|
|
||||||
synchronized (info) {
|
|
||||||
info[0] = true; // Done
|
|
||||||
info[1] = pollRes; // Polling result
|
|
||||||
|
|
||||||
info.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
t.setName("Fast Poll - "+address);
|
|
||||||
t.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException {
|
|
||||||
final boolean[] remoteInfo = new boolean[2];
|
|
||||||
final boolean[] localInfo = new boolean[2];
|
|
||||||
final boolean[] manualInfo = new boolean[2];
|
|
||||||
|
|
||||||
startFastPollThread(localAddress, localInfo);
|
|
||||||
startFastPollThread(remoteAddress, remoteInfo);
|
|
||||||
startFastPollThread(manualAddress, manualInfo);
|
|
||||||
|
|
||||||
// Check local first
|
|
||||||
synchronized (localInfo) {
|
|
||||||
while (!localInfo[0]) {
|
|
||||||
localInfo.wait(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localInfo[1]) {
|
|
||||||
return localAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now manual
|
|
||||||
synchronized (manualInfo) {
|
|
||||||
while (!manualInfo[0]) {
|
|
||||||
manualInfo.wait(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manualInfo[1]) {
|
|
||||||
return manualAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// And finally, remote
|
|
||||||
synchronized (remoteInfo) {
|
|
||||||
while (!remoteInfo[0]) {
|
|
||||||
remoteInfo.wait(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteInfo[1]) {
|
|
||||||
return remoteAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
|
||||||
ComputerDetails polledDetails;
|
|
||||||
|
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening.
|
|
||||||
// Do not write this address to details.activeAddress because:
|
|
||||||
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
|
|
||||||
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
|
|
||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")");
|
|
||||||
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
|
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
|
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
|
||||||
if (candidateAddress == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try using the active address from fast-poll
|
|
||||||
polledDetails = tryPollIp(details, candidateAddress);
|
|
||||||
if (polledDetails == null) {
|
|
||||||
// If that failed, try all unique addresses except what we've
|
|
||||||
// already tried
|
|
||||||
HashSet<String> uniqueAddresses = new HashSet<>();
|
|
||||||
uniqueAddresses.add(details.localAddress);
|
|
||||||
uniqueAddresses.add(details.remoteAddress);
|
|
||||||
uniqueAddresses.add(details.manualAddress);
|
|
||||||
for (String addr : uniqueAddresses) {
|
|
||||||
if (addr == null || addr.equals(candidateAddress)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
polledDetails = tryPollIp(details, addr);
|
|
||||||
if (polledDetails != null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polledDetails != null) {
|
|
||||||
details.update(polledDetails);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ApplistPoller {
|
|
||||||
private Thread thread;
|
|
||||||
private final ComputerDetails computer;
|
|
||||||
private final Object pollEvent = new Object();
|
|
||||||
private boolean receivedAppList = false;
|
|
||||||
|
|
||||||
public ApplistPoller(ComputerDetails computer) {
|
|
||||||
this.computer = computer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void pollNow() {
|
|
||||||
synchronized (pollEvent) {
|
|
||||||
pollEvent.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean waitPollingDelay() {
|
|
||||||
try {
|
|
||||||
synchronized (pollEvent) {
|
|
||||||
if (receivedAppList) {
|
|
||||||
// If we've already reported an app list successfully,
|
|
||||||
// wait the full polling period
|
|
||||||
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// If we've failed to get an app list so far, retry much earlier
|
|
||||||
pollEvent.wait(APPLIST_FAILED_POLLING_RETRY_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return thread != null && !thread.isInterrupted();
|
|
||||||
}
|
|
||||||
|
|
||||||
private PollingTuple getPollingTuple(ComputerDetails details) {
|
|
||||||
synchronized (pollingTuples) {
|
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
|
||||||
if (details.uuid.equals(tuple.computer.uuid)) {
|
|
||||||
return tuple;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
thread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
int emptyAppListResponses = 0;
|
|
||||||
do {
|
|
||||||
// Can't poll if it's not online or paired
|
|
||||||
if (computer.state != ComputerDetails.State.ONLINE ||
|
|
||||||
computer.pairState != PairingManager.PairState.PAIRED) {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.notifyComputerUpdated(computer);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can't poll if there's no UUID yet
|
|
||||||
if (computer.uuid == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
PollingTuple tuple = getPollingTuple(computer);
|
|
||||||
|
|
||||||
try {
|
|
||||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
|
||||||
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
|
||||||
|
|
||||||
String appList;
|
|
||||||
if (tuple != null) {
|
|
||||||
// If we're polling this machine too, grab the network lock
|
|
||||||
// while doing the app list request to prevent other requests
|
|
||||||
// from being issued in the meantime.
|
|
||||||
synchronized (tuple.networkLock) {
|
|
||||||
appList = http.getAppListRaw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// No polling is happening now, so we just call it directly
|
|
||||||
appList = http.getAppListRaw();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
|
||||||
if (list.isEmpty()) {
|
|
||||||
LimeLog.warning("Empty app list received from "+computer.uuid);
|
|
||||||
|
|
||||||
// The app list might actually be empty, so if we get an empty response a few times
|
|
||||||
// in a row, we'll go ahead and believe it.
|
|
||||||
emptyAppListResponses++;
|
|
||||||
}
|
|
||||||
if (!appList.isEmpty() &&
|
|
||||||
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
|
||||||
// Open the cache file
|
|
||||||
OutputStream cacheOut = null;
|
|
||||||
try {
|
|
||||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
|
|
||||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (cacheOut != null) {
|
|
||||||
cacheOut.close();
|
|
||||||
}
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset empty count if it wasn't empty this time
|
|
||||||
if (!list.isEmpty()) {
|
|
||||||
emptyAppListResponses = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the computer
|
|
||||||
computer.rawAppList = appList;
|
|
||||||
receivedAppList = true;
|
|
||||||
|
|
||||||
// Notify that the app list has been updated
|
|
||||||
// and ensure that the thread is still active
|
|
||||||
if (listener != null && thread != null) {
|
|
||||||
listener.notifyComputerUpdated(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (appList.isEmpty()) {
|
|
||||||
LimeLog.warning("Null app list received from "+computer.uuid);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (XmlPullParserException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} while (waitPollingDelay());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
thread.setName("App list polling thread for " + computer.name);
|
|
||||||
thread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (thread != null) {
|
|
||||||
thread.interrupt();
|
|
||||||
|
|
||||||
// Don't join here because we might be blocked on network I/O
|
|
||||||
|
|
||||||
thread = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PollingTuple {
|
|
||||||
public Thread thread;
|
|
||||||
public final ComputerDetails computer;
|
|
||||||
public final Object networkLock;
|
|
||||||
public long lastSuccessfulPollMs;
|
|
||||||
|
|
||||||
public PollingTuple(ComputerDetails computer, Thread thread) {
|
|
||||||
this.computer = computer;
|
|
||||||
this.thread = thread;
|
|
||||||
this.networkLock = new Object();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReachabilityTuple {
|
|
||||||
public final String reachableAddress;
|
|
||||||
public final ComputerDetails computer;
|
|
||||||
|
|
||||||
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
|
|
||||||
this.computer = computer;
|
|
||||||
this.reachableAddress = reachableAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package com.limelight.computers;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public class IdentityManager {
|
|
||||||
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
|
||||||
private static final int UID_SIZE_IN_BYTES = 8;
|
|
||||||
|
|
||||||
private String uniqueId;
|
|
||||||
|
|
||||||
public IdentityManager(Context c) {
|
|
||||||
uniqueId = loadUniqueId(c);
|
|
||||||
if (uniqueId == null) {
|
|
||||||
uniqueId = generateNewUniqueId(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("UID is now: "+uniqueId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUniqueId() {
|
|
||||||
return uniqueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String loadUniqueId(Context c) {
|
|
||||||
// 2 Hex digits per byte
|
|
||||||
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
|
||||||
InputStreamReader reader = null;
|
|
||||||
LimeLog.info("Reading UID from disk");
|
|
||||||
try {
|
|
||||||
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
|
||||||
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
|
||||||
{
|
|
||||||
LimeLog.severe("UID file data is truncated");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new String(uid);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
LimeLog.info("No UID file found");
|
|
||||||
return null;
|
|
||||||
} catch (IOException e) {
|
|
||||||
LimeLog.severe("Error while reading UID file");
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
if (reader != null) {
|
|
||||||
try {
|
|
||||||
reader.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String generateNewUniqueId(Context c) {
|
|
||||||
// Generate a new UID hex string
|
|
||||||
LimeLog.info("Generating new UID");
|
|
||||||
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
|
||||||
|
|
||||||
OutputStreamWriter writer = null;
|
|
||||||
try {
|
|
||||||
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
|
||||||
writer.write(uidStr);
|
|
||||||
LimeLog.info("UID written to disk");
|
|
||||||
} catch (IOException e) {
|
|
||||||
LimeLog.severe("Error while writing UID file");
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (writer != null) {
|
|
||||||
try {
|
|
||||||
writer.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can return a UID even if I/O fails
|
|
||||||
return uidStr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package com.limelight.computers;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteException;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class LegacyDatabaseReader {
|
|
||||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
|
||||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
|
||||||
|
|
||||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
|
||||||
|
|
||||||
private static ComputerDetails getComputerFromCursor(Cursor c) {
|
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
|
|
||||||
details.name = c.getString(0);
|
|
||||||
details.uuid = c.getString(1);
|
|
||||||
|
|
||||||
// An earlier schema defined addresses as byte blobs. We'll
|
|
||||||
// gracefully migrate those to strings so we can store DNS names
|
|
||||||
// too. To disambiguate, we'll need to prefix them with a string
|
|
||||||
// greater than the allowable IP address length.
|
|
||||||
try {
|
|
||||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
|
||||||
LimeLog.warning("DB: Legacy local address for " + details.name);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// This is probably a hostname/address with the prefix string
|
|
||||||
String stringData = c.getString(2);
|
|
||||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
|
||||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
|
||||||
} else {
|
|
||||||
LimeLog.severe("DB: Corrupted local address for " + details.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
|
||||||
LimeLog.warning("DB: Legacy remote address for " + details.name);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// This is probably a hostname/address with the prefix string
|
|
||||||
String stringData = c.getString(3);
|
|
||||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
|
||||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
|
||||||
} else {
|
|
||||||
LimeLog.severe("DB: Corrupted remote address for " + details.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On older versions of Moonlight, this is typically where manual addresses got stored,
|
|
||||||
// so let's initialize it just to be safe.
|
|
||||||
details.manualAddress = details.remoteAddress;
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<ComputerDetails> getAllComputers(SQLiteDatabase db) {
|
|
||||||
Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null);
|
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
|
||||||
|
|
||||||
// If a critical field is corrupt or missing, skip the database entry
|
|
||||||
if (details.uuid == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
computerList.add(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
c.close();
|
|
||||||
|
|
||||||
return computerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
|
||||||
SQLiteDatabase computerDb = null;
|
|
||||||
try {
|
|
||||||
// Open the existing database
|
|
||||||
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
|
||||||
return getAllComputers(computerDb);
|
|
||||||
} catch (SQLiteException e) {
|
|
||||||
return new LinkedList<ComputerDetails>();
|
|
||||||
} finally {
|
|
||||||
// Close and delete the old DB
|
|
||||||
if (computerDb != null) {
|
|
||||||
computerDb.close();
|
|
||||||
}
|
|
||||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package com.limelight.discovery;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
|
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.wifi.WifiManager;
|
|
||||||
import android.net.wifi.WifiManager.MulticastLock;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.IBinder;
|
|
||||||
|
|
||||||
public class DiscoveryService extends Service {
|
|
||||||
|
|
||||||
private MdnsDiscoveryAgent discoveryAgent;
|
|
||||||
private MdnsDiscoveryListener boundListener;
|
|
||||||
private MulticastLock multicastLock;
|
|
||||||
|
|
||||||
public class DiscoveryBinder extends Binder {
|
|
||||||
public void setListener(MdnsDiscoveryListener listener) {
|
|
||||||
boundListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startDiscovery(int queryIntervalMs) {
|
|
||||||
multicastLock.acquire();
|
|
||||||
discoveryAgent.startDiscovery(queryIntervalMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopDiscovery() {
|
|
||||||
discoveryAgent.stopDiscovery();
|
|
||||||
multicastLock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MdnsComputer> getComputerSet() {
|
|
||||||
return discoveryAgent.getComputerSet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
|
||||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
|
||||||
multicastLock.setReferenceCounted(false);
|
|
||||||
|
|
||||||
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
|
||||||
@Override
|
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
|
||||||
if (boundListener != null) {
|
|
||||||
boundListener.notifyComputerAdded(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
|
||||||
if (boundListener != null) {
|
|
||||||
boundListener.notifyComputerRemoved(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyDiscoveryFailure(Exception e) {
|
|
||||||
if (boundListener != null) {
|
|
||||||
boundListener.notifyDiscoveryFailure(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private final DiscoveryBinder binder = new DiscoveryBinder();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return binder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onUnbind(Intent intent) {
|
|
||||||
// Stop any discovery session
|
|
||||||
discoveryAgent.stopDiscovery();
|
|
||||||
multicastLock.release();
|
|
||||||
|
|
||||||
// Unbind the listener
|
|
||||||
boundListener = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
package com.limelight.grid;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.limelight.AppView;
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.grid.assets.CachedAppAssetLoader;
|
|
||||||
import com.limelight.grid.assets.DiskAssetLoader;
|
|
||||||
import com.limelight.grid.assets.MemoryAssetLoader;
|
|
||||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|
||||||
private static final int ART_WIDTH_PX = 300;
|
|
||||||
private static final int SMALL_WIDTH_DP = 100;
|
|
||||||
private static final int LARGE_WIDTH_DP = 150;
|
|
||||||
|
|
||||||
private final CachedAppAssetLoader loader;
|
|
||||||
|
|
||||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
|
||||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
|
|
||||||
|
|
||||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
|
||||||
int dp;
|
|
||||||
|
|
||||||
if (small) {
|
|
||||||
dp = SMALL_WIDTH_DP;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dp = LARGE_WIDTH_DP;
|
|
||||||
}
|
|
||||||
|
|
||||||
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
|
|
||||||
if (scalingDivisor < 1.0) {
|
|
||||||
// We don't want to make them bigger before draw-time
|
|
||||||
scalingDivisor = 1.0;
|
|
||||||
}
|
|
||||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
|
||||||
|
|
||||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
|
||||||
new NetworkAssetLoader(context, uniqueId),
|
|
||||||
new MemoryAssetLoader(),
|
|
||||||
new DiskAssetLoader(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
|
||||||
loader.cancelForegroundLoads();
|
|
||||||
loader.cancelBackgroundLoads();
|
|
||||||
loader.freeCacheMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sortList() {
|
|
||||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
|
||||||
@Override
|
|
||||||
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
|
||||||
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addApp(AppView.AppObject app) {
|
|
||||||
// Queue a request to fetch this bitmap into cache
|
|
||||||
loader.queueCacheLoad(app.app);
|
|
||||||
|
|
||||||
// Add the app to our sorted list
|
|
||||||
itemList.add(app);
|
|
||||||
sortList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeApp(AppView.AppObject app) {
|
|
||||||
itemList.remove(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
|
||||||
// Let the cached asset loader handle it
|
|
||||||
loader.populateImageView(obj.app, imgView, prgView);
|
|
||||||
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.isRunning) {
|
|
||||||
// Show the play button overlay
|
|
||||||
overlayView.setImageResource(R.drawable.ic_play);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No overlay
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
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.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|
||||||
protected final Context context;
|
|
||||||
protected final int layoutId;
|
|
||||||
protected final ArrayList<T> itemList = new ArrayList<>();
|
|
||||||
protected final LayoutInflater inflater;
|
|
||||||
|
|
||||||
public GenericGridAdapter(Context context, int layoutId) {
|
|
||||||
this.context = context;
|
|
||||||
this.layoutId = layoutId;
|
|
||||||
|
|
||||||
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, ProgressBar prgView, 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 = convertView.findViewById(R.id.grid_image);
|
|
||||||
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
|
|
||||||
TextView txtView = convertView.findViewById(R.id.grid_text);
|
|
||||||
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
|
||||||
|
|
||||||
if (imgView != null) {
|
|
||||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
|
||||||
imgView.setImageBitmap(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!populateTextView(txtView, itemList.get(i))) {
|
|
||||||
txtView.setText(itemList.get(i).toString());
|
|
||||||
}
|
|
||||||
if (overlayView != null) {
|
|
||||||
if (!populateOverlayView(overlayView, itemList.get(i))) {
|
|
||||||
overlayView.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
overlayView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package com.limelight.grid;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.limelight.PcView;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|
||||||
|
|
||||||
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
|
||||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addComputer(PcView.ComputerObject computer) {
|
|
||||||
itemList.add(computer);
|
|
||||||
sortList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sortList() {
|
|
||||||
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
|
||||||
@Override
|
|
||||||
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
|
||||||
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean removeComputer(PcView.ComputerObject computer) {
|
|
||||||
return itemList.remove(computer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
|
||||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
|
||||||
imgView.setAlpha(1.0f);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
imgView.setAlpha(0.4f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
|
|
||||||
prgView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
prgView.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
imgView.setImageResource(R.drawable.ic_computer);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
|
||||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
|
||||||
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.state == ComputerDetails.State.OFFLINE) {
|
|
||||||
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
|
||||||
overlayView.setAlpha(0.4f);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// We must check if the status is exactly online and unpaired
|
|
||||||
// to avoid colliding with the loading spinner when status is unknown
|
|
||||||
else if (obj.details.state == ComputerDetails.State.ONLINE &&
|
|
||||||
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
|
|
||||||
overlayView.setImageResource(R.drawable.ic_lock);
|
|
||||||
overlayView.setAlpha(1.0f);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class CachedAppAssetLoader {
|
|
||||||
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
|
|
||||||
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
|
|
||||||
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
|
|
||||||
|
|
||||||
private static final int MAX_PENDING_CACHE_LOADS = 100;
|
|
||||||
private static final int MAX_PENDING_NETWORK_LOADS = 40;
|
|
||||||
private static final int MAX_PENDING_DISK_LOADS = 40;
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ComputerDetails computer;
|
|
||||||
private final double scalingDivider;
|
|
||||||
private final NetworkAssetLoader networkLoader;
|
|
||||||
private final MemoryAssetLoader memoryLoader;
|
|
||||||
private final DiskAssetLoader diskLoader;
|
|
||||||
private final Bitmap placeholderBitmap;
|
|
||||||
|
|
||||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
|
||||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
|
||||||
DiskAssetLoader diskLoader) {
|
|
||||||
this.computer = computer;
|
|
||||||
this.scalingDivider = scalingDivider;
|
|
||||||
this.networkLoader = networkLoader;
|
|
||||||
this.memoryLoader = memoryLoader;
|
|
||||||
this.diskLoader = diskLoader;
|
|
||||||
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelBackgroundLoads() {
|
|
||||||
Runnable r;
|
|
||||||
while ((r = cacheExecutor.getQueue().poll()) != null) {
|
|
||||||
cacheExecutor.remove(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelForegroundLoads() {
|
|
||||||
Runnable r;
|
|
||||||
|
|
||||||
while ((r = foregroundExecutor.getQueue().poll()) != null) {
|
|
||||||
foregroundExecutor.remove(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
while ((r = networkExecutor.getQueue().poll()) != null) {
|
|
||||||
networkExecutor.remove(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void freeCacheMemory() {
|
|
||||||
memoryLoader.clearCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
|
||||||
// Try 3 times
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
// Check again whether we've been cancelled or the image view is gone
|
|
||||||
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputStream in = networkLoader.getBitmapStream(tuple);
|
|
||||||
if (in != null) {
|
|
||||||
// Write the stream straight to disk
|
|
||||||
diskLoader.populateCacheWithStream(tuple, in);
|
|
||||||
|
|
||||||
// Close the network input stream
|
|
||||||
try {
|
|
||||||
in.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
|
|
||||||
// If there's a task associated with this load, we should return the bitmap
|
|
||||||
if (task != null) {
|
|
||||||
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
|
||||||
if (bmp != null) {
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Otherwise it's a background load and we return nothing
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait 1 second with a bit of fuzz
|
|
||||||
try {
|
|
||||||
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
|
||||||
private final WeakReference<ImageView> imageViewRef;
|
|
||||||
private final WeakReference<ProgressBar> progressViewRef;
|
|
||||||
private final boolean diskOnly;
|
|
||||||
|
|
||||||
private LoaderTuple tuple;
|
|
||||||
|
|
||||||
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
|
||||||
this.imageViewRef = new WeakReference<>(imageView);
|
|
||||||
this.progressViewRef = new WeakReference<>(prgView);
|
|
||||||
this.diskOnly = diskOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
|
||||||
tuple = params[0];
|
|
||||||
|
|
||||||
// Check whether it has been cancelled or the views are gone
|
|
||||||
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
|
||||||
if (bmp == null) {
|
|
||||||
if (!diskOnly) {
|
|
||||||
// Try to load the asset from the network
|
|
||||||
bmp = doNetworkAssetLoad(tuple, this);
|
|
||||||
} else {
|
|
||||||
// Report progress to display the placeholder and spin
|
|
||||||
// off the network-capable task
|
|
||||||
publishProgress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the bitmap
|
|
||||||
if (bmp != null) {
|
|
||||||
memoryLoader.populateCache(tuple, bmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onProgressUpdate(Void... nothing) {
|
|
||||||
// Do nothing if cancelled
|
|
||||||
if (isCancelled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current loader task for this view isn't us, do nothing
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
|
||||||
final ProgressBar prgView = progressViewRef.get();
|
|
||||||
if (getLoaderTask(imageView) == this) {
|
|
||||||
// Now display the progress bar since we have to hit the network
|
|
||||||
if (prgView != null) {
|
|
||||||
prgView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set off another loader task on the network executor
|
|
||||||
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
|
||||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
|
||||||
imageView.setVisibility(View.VISIBLE);
|
|
||||||
imageView.setImageDrawable(asyncDrawable);
|
|
||||||
task.executeOnExecutor(networkExecutor, tuple);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Bitmap bitmap) {
|
|
||||||
// Do nothing if cancelled
|
|
||||||
if (isCancelled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
|
||||||
final ProgressBar prgView = progressViewRef.get();
|
|
||||||
if (getLoaderTask(imageView) == this) {
|
|
||||||
// Set the bitmap
|
|
||||||
if (bitmap != null) {
|
|
||||||
imageView.setImageBitmap(bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the progress bar
|
|
||||||
if (prgView != null) {
|
|
||||||
prgView.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the view
|
|
||||||
imageView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class AsyncDrawable extends BitmapDrawable {
|
|
||||||
private final WeakReference<LoaderTask> loaderTaskReference;
|
|
||||||
|
|
||||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
|
||||||
LoaderTask loaderTask) {
|
|
||||||
super(res, bitmap);
|
|
||||||
loaderTaskReference = new WeakReference<>(loaderTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoaderTask getLoaderTask() {
|
|
||||||
return loaderTaskReference.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LoaderTask getLoaderTask(ImageView imageView) {
|
|
||||||
if (imageView == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Drawable drawable = imageView.getDrawable();
|
|
||||||
|
|
||||||
// If our drawable is in play, get the loader task
|
|
||||||
if (drawable instanceof AsyncDrawable) {
|
|
||||||
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
|
||||||
return asyncDrawable.getLoaderTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
|
|
||||||
final LoaderTask loaderTask = getLoaderTask(imageView);
|
|
||||||
|
|
||||||
// Check if any task was pending for this image view
|
|
||||||
if (loaderTask != null && !loaderTask.isCancelled()) {
|
|
||||||
final LoaderTuple taskTuple = loaderTask.tuple;
|
|
||||||
|
|
||||||
// Cancel the task if it's not already loading the same data
|
|
||||||
if (taskTuple == null || !taskTuple.equals(tuple)) {
|
|
||||||
loaderTask.cancel(true);
|
|
||||||
} else {
|
|
||||||
// It's already loading what we want
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the load to proceed
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queueCacheLoad(NvApp app) {
|
|
||||||
final LoaderTuple tuple = new LoaderTuple(computer, app);
|
|
||||||
|
|
||||||
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
|
|
||||||
// It's in memory which means it must also be on disk
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue a fetch in the cache executor
|
|
||||||
cacheExecutor.execute(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Check if the image is cached on disk
|
|
||||||
if (diskLoader.checkCacheExists(tuple)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load the asset from the network and cache result on disk
|
|
||||||
doNetworkAssetLoad(tuple, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
|
|
||||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
|
||||||
|
|
||||||
// If there's already a task in progress for this view,
|
|
||||||
// cancel it. If the task is already loading the same image,
|
|
||||||
// we return and let that load finish.
|
|
||||||
if (!cancelPendingLoad(tuple, imgView)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the progress bar always on initial load
|
|
||||||
prgView.setVisibility(View.INVISIBLE);
|
|
||||||
|
|
||||||
// First, try the memory cache in the current context
|
|
||||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
|
||||||
if (bmp != null) {
|
|
||||||
// Show the bitmap immediately
|
|
||||||
imgView.setVisibility(View.VISIBLE);
|
|
||||||
imgView.setImageBitmap(bmp);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's not in memory, create an async task to load it. This task will be attached
|
|
||||||
// via AsyncDrawable to this view.
|
|
||||||
final LoaderTask task = new LoaderTask(imgView, prgView, true);
|
|
||||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
|
||||||
imgView.setVisibility(View.INVISIBLE);
|
|
||||||
imgView.setImageDrawable(asyncDrawable);
|
|
||||||
|
|
||||||
// Run the task on our foreground executor
|
|
||||||
task.executeOnExecutor(foregroundExecutor, tuple);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LoaderTuple {
|
|
||||||
public final ComputerDetails computer;
|
|
||||||
public final NvApp app;
|
|
||||||
|
|
||||||
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
|
||||||
this.computer = computer;
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (!(o instanceof LoaderTuple)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoaderTuple other = (LoaderTuple) o;
|
|
||||||
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "("+computer.uuid+", "+app.getAppId()+")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.ImageDecoder;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.utils.CacheHelper;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class DiskAssetLoader {
|
|
||||||
// 5 MB
|
|
||||||
private static final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
|
||||||
|
|
||||||
// Standard box art is 300x400
|
|
||||||
private static final int STANDARD_ASSET_WIDTH = 300;
|
|
||||||
private static final int STANDARD_ASSET_HEIGHT = 400;
|
|
||||||
|
|
||||||
private final boolean isLowRamDevice;
|
|
||||||
private final File cacheDir;
|
|
||||||
|
|
||||||
public DiskAssetLoader(Context context) {
|
|
||||||
this.cacheDir = context.getCacheDir();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
this.isLowRamDevice =
|
|
||||||
((ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)).isLowRamDevice();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Use conservative low RAM behavior on very old devices
|
|
||||||
this.isLowRamDevice = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
|
||||||
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
|
||||||
// Raw height and width of image
|
|
||||||
final int height = options.outHeight;
|
|
||||||
final int width = options.outWidth;
|
|
||||||
int inSampleSize = 1;
|
|
||||||
|
|
||||||
if (height > reqHeight || width > reqWidth) {
|
|
||||||
|
|
||||||
final int halfHeight = height / 2;
|
|
||||||
final int halfWidth = width / 2;
|
|
||||||
|
|
||||||
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
|
|
||||||
// height and width larger than the requested height and width.
|
|
||||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
|
||||||
inSampleSize *= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inSampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
|
||||||
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
|
||||||
|
|
||||||
// Don't bother with anything if it doesn't exist
|
|
||||||
if (!file.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the cached asset doesn't exceed the maximum size
|
|
||||||
if (file.length() > MAX_ASSET_SIZE) {
|
|
||||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
|
||||||
file.delete();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bitmap bmp;
|
|
||||||
|
|
||||||
// For OSes prior to P, we have to use the ugly BitmapFactory API
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
||||||
// Lookup bounds of the downloaded image
|
|
||||||
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
|
|
||||||
decodeOnlyOptions.inJustDecodeBounds = true;
|
|
||||||
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
|
|
||||||
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
|
|
||||||
// Dimensions set to -1 on error. Return value always null.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
|
|
||||||
|
|
||||||
// Load the image scaled to the appropriate size
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
|
|
||||||
STANDARD_ASSET_WIDTH / sampleSize,
|
|
||||||
STANDARD_ASSET_HEIGHT / sampleSize);
|
|
||||||
if (isLowRamDevice) {
|
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
|
||||||
options.inDither = true;
|
|
||||||
}
|
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
options.inPreferredConfig = Bitmap.Config.HARDWARE;
|
|
||||||
}
|
|
||||||
|
|
||||||
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
|
||||||
if (bmp != null) {
|
|
||||||
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// On P, we can get a bitmap back in one step with ImageDecoder
|
|
||||||
try {
|
|
||||||
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
|
||||||
@Override
|
|
||||||
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
|
||||||
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
|
||||||
if (isLowRamDevice) {
|
|
||||||
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
|
||||||
OutputStream out = null;
|
|
||||||
boolean success = false;
|
|
||||||
try {
|
|
||||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
|
||||||
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
|
||||||
success = true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (out != null) {
|
|
||||||
try {
|
|
||||||
out.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
|
||||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.util.LruCache;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
|
|
||||||
public class MemoryAssetLoader {
|
|
||||||
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
|
||||||
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
|
||||||
@Override
|
|
||||||
protected int sizeOf(String key, Bitmap bitmap) {
|
|
||||||
// Sizeof returns kilobytes
|
|
||||||
return bitmap.getByteCount() / 1024;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
|
||||||
if (bmp != null) {
|
|
||||||
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
|
||||||
}
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
|
||||||
memoryCache.put(constructKey(tuple), bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearCache() {
|
|
||||||
memoryCache.evictAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class NetworkAssetLoader {
|
|
||||||
private final Context context;
|
|
||||||
private final String uniqueId;
|
|
||||||
|
|
||||||
public NetworkAssetLoader(Context context, String uniqueId) {
|
|
||||||
this.context = context;
|
|
||||||
this.uniqueId = uniqueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
InputStream in = null;
|
|
||||||
try {
|
|
||||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
|
|
||||||
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
|
|
||||||
in = http.getBoxArt(tuple.app);
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
|
|
||||||
if (in != null) {
|
|
||||||
LimeLog.info("Network asset load complete: " + tuple);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.info("Network asset load failed: " + tuple);
|
|
||||||
}
|
|
||||||
|
|
||||||
return in;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InterfaceAddress;
|
|
||||||
import java.net.NetworkInterface;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.utils.Dialog;
|
|
||||||
import com.limelight.utils.SpinnerDialog;
|
|
||||||
import com.limelight.utils.UiHelper;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.inputmethod.EditorInfo;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
public class AddComputerManually extends Activity {
|
|
||||||
private TextView hostText;
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
|
||||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
|
|
||||||
private Thread addThread;
|
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
|
||||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
|
||||||
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
|
||||||
startAddThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onServiceDisconnected(ComponentName className) {
|
|
||||||
joinAddThread();
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private boolean isWrongSubnetSiteLocalAddress(String address) {
|
|
||||||
try {
|
|
||||||
InetAddress targetAddress = InetAddress.getByName(address);
|
|
||||||
if (!(targetAddress instanceof Inet4Address) || !targetAddress.isSiteLocalAddress()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a site-local address. Look for a matching local interface.
|
|
||||||
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
|
||||||
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
|
|
||||||
if (!(addr.getAddress() instanceof Inet4Address) || !addr.getAddress().isSiteLocalAddress()) {
|
|
||||||
// Skip non-site-local or non-IPv4 addresses
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] targetAddrBytes = targetAddress.getAddress();
|
|
||||||
byte[] ifaceAddrBytes = addr.getAddress().getAddress();
|
|
||||||
|
|
||||||
// Compare prefix to ensure it's the same
|
|
||||||
boolean addressMatches = true;
|
|
||||||
for (int i = 0; i < addr.getNetworkPrefixLength(); i++) {
|
|
||||||
if ((ifaceAddrBytes[i / 8] & (1 << (i % 8))) != (targetAddrBytes[i / 8] & (1 << (i % 8)))) {
|
|
||||||
addressMatches = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addressMatches) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couldn't find a matching interface
|
|
||||||
return true;
|
|
||||||
} catch (SocketException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doAddPc(String host) {
|
|
||||||
boolean wrongSiteLocal = false;
|
|
||||||
boolean success;
|
|
||||||
|
|
||||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
|
||||||
getResources().getString(R.string.msg_add_pc), false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
success = managerBinder.addComputerBlocking(host, true);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
|
||||||
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
|
||||||
e.printStackTrace();
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
if (!success){
|
|
||||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.dismiss();
|
|
||||||
|
|
||||||
if (wrongSiteLocal) {
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
|
||||||
}
|
|
||||||
else if (!success) {
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_success), Toast.LENGTH_LONG).show();
|
|
||||||
|
|
||||||
if (!isFinishing()) {
|
|
||||||
// Close the activity
|
|
||||||
AddComputerManually.this.finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startAddThread() {
|
|
||||||
addThread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
while (!isInterrupted()) {
|
|
||||||
String computer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
computer = computersToAdd.take();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
doAddPc(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
addThread.setName("UI - AddComputerManually");
|
|
||||||
addThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void joinAddThread() {
|
|
||||||
if (addThread != null) {
|
|
||||||
addThread.interrupt();
|
|
||||||
|
|
||||||
try {
|
|
||||||
addThread.join();
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
|
|
||||||
addThread = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
SpinnerDialog.closeDialogs(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
joinAddThread();
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_add_computer_manually);
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
|
||||||
|
|
||||||
this.hostText = 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)) {
|
|
||||||
return handleDoneEvent();
|
|
||||||
}
|
|
||||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
|
||||||
// This is how the Fire TV dismisses the keyboard
|
|
||||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
findViewById(R.id.addPcButton).setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
handleDoneEvent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bind to the ComputerManager service
|
|
||||||
bindService(new Intent(AddComputerManually.this,
|
|
||||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if the event should be eaten
|
|
||||||
private boolean handleDoneEvent() {
|
|
||||||
String hostAddress = hostText.getText().toString().trim();
|
|
||||||
|
|
||||||
if (hostAddress.length() == 0) {
|
|
||||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
computersToAdd.add(hostAddress);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.DialogPreference;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
|
|
||||||
import static com.limelight.binding.input.virtual_controller.VirtualControllerConfigurationLoader.OSC_PREFERENCE;
|
|
||||||
|
|
||||||
public class ConfirmDeleteOscPreference extends DialogPreference {
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public ConfirmDeleteOscPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
|
||||||
getContext().getSharedPreferences(OSC_PREFERENCE, Context.MODE_PRIVATE).edit().clear().apply();
|
|
||||||
Toast.makeText(getContext(), R.string.toast_reset_osc_success, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
public class GlPreferences {
|
|
||||||
private static final String PREF_NAME = "GlPreferences";
|
|
||||||
|
|
||||||
private static final String FINGERPRINT_PREF_STRING = "Fingerprint";
|
|
||||||
private static final String GL_RENDERER_PREF_STRING = "Renderer";
|
|
||||||
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
public String glRenderer;
|
|
||||||
public String savedFingerprint;
|
|
||||||
|
|
||||||
private GlPreferences(SharedPreferences prefs) {
|
|
||||||
this.prefs = prefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GlPreferences readPreferences(Context context) {
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, 0);
|
|
||||||
GlPreferences glPrefs = new GlPreferences(prefs);
|
|
||||||
|
|
||||||
glPrefs.glRenderer = prefs.getString(GL_RENDERER_PREF_STRING, "");
|
|
||||||
glPrefs.savedFingerprint = prefs.getString(FINGERPRINT_PREF_STRING, "");
|
|
||||||
|
|
||||||
return glPrefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean writePreferences() {
|
|
||||||
return prefs.edit()
|
|
||||||
.putString(GL_RENDERER_PREF_STRING, glRenderer)
|
|
||||||
.putString(FINGERPRINT_PREF_STRING, savedFingerprint)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
|
||||||
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
|
||||||
|
|
||||||
|
|
||||||
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
|
||||||
static final String FPS_PREF_STRING = "list_fps";
|
|
||||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
|
||||||
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
|
||||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
|
||||||
private static final String 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 String LANGUAGE_PREF_STRING = "list_languages";
|
|
||||||
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
|
||||||
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
|
||||||
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
|
||||||
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
|
||||||
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
|
||||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
|
||||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
|
||||||
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
|
||||||
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
|
||||||
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
|
||||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
|
||||||
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
|
||||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
|
||||||
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
|
|
||||||
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
|
||||||
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
|
||||||
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
|
||||||
|
|
||||||
static final String DEFAULT_RESOLUTION = "720p";
|
|
||||||
static final String DEFAULT_FPS = "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 String DEFAULT_LANGUAGE = "default";
|
|
||||||
private static final boolean DEFAULT_LIST_MODE = false;
|
|
||||||
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
|
||||||
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
|
|
||||||
private static final boolean DEFAULT_USB_DRIVER = true;
|
|
||||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
|
||||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
|
||||||
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
|
||||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
|
||||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
|
||||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
|
||||||
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
|
||||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
|
||||||
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
|
||||||
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
|
||||||
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
|
||||||
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
|
||||||
|
|
||||||
public static final int FORCE_H265_ON = -1;
|
|
||||||
public static final int AUTOSELECT_H265 = 0;
|
|
||||||
public static final int FORCE_H265_OFF = 1;
|
|
||||||
|
|
||||||
public int width, height, fps;
|
|
||||||
public int bitrate;
|
|
||||||
public int videoFormat;
|
|
||||||
public int deadzonePercentage;
|
|
||||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
|
||||||
public String language;
|
|
||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
|
||||||
public boolean onscreenController;
|
|
||||||
public boolean onlyL3R3;
|
|
||||||
public boolean disableFrameDrop;
|
|
||||||
public boolean enableHdr;
|
|
||||||
public boolean enablePip;
|
|
||||||
public boolean bindAllUsb;
|
|
||||||
public boolean mouseEmulation;
|
|
||||||
public boolean mouseNavButtons;
|
|
||||||
public boolean unlockFps;
|
|
||||||
public boolean vibrateOsc;
|
|
||||||
public boolean vibrateFallbackToDevice;
|
|
||||||
|
|
||||||
private static int getHeightFromResolutionString(String resString) {
|
|
||||||
if (resString.equalsIgnoreCase("360p")) {
|
|
||||||
return 360;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("480p")) {
|
|
||||||
return 480;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("720p")) {
|
|
||||||
return 720;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("1080p")) {
|
|
||||||
return 1080;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("1440p")) {
|
|
||||||
return 1440;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("4K")) {
|
|
||||||
return 2160;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should be unreachable
|
|
||||||
return 720;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getWidthFromResolutionString(String resString) {
|
|
||||||
int height = getHeightFromResolutionString(resString);
|
|
||||||
if (height == 480) {
|
|
||||||
// This isn't an exact 16:9 resolution
|
|
||||||
return 854;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (height * 16) / 9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getResolutionString(int width, int height) {
|
|
||||||
switch (height) {
|
|
||||||
case 360:
|
|
||||||
return "360p";
|
|
||||||
case 480:
|
|
||||||
return "480p";
|
|
||||||
default:
|
|
||||||
case 720:
|
|
||||||
return "720p";
|
|
||||||
case 1080:
|
|
||||||
return "1080p";
|
|
||||||
case 1440:
|
|
||||||
return "1440p";
|
|
||||||
case 2160:
|
|
||||||
return "4K";
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resString, String fpsString) {
|
|
||||||
int width = getWidthFromResolutionString(resString);
|
|
||||||
int height = getHeightFromResolutionString(resString);
|
|
||||||
int fps = Integer.parseInt(fpsString);
|
|
||||||
|
|
||||||
// This table prefers 16:10 resolutions because they are
|
|
||||||
// only slightly more pixels than the 16:9 equivalents, so
|
|
||||||
// we don't want to bump those 16:10 resolutions up to the
|
|
||||||
// next 16:9 slot.
|
|
||||||
//
|
|
||||||
// This logic is shamelessly stolen from Moonlight Qt:
|
|
||||||
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
|
|
||||||
|
|
||||||
if (width * height <= 640 * 360) {
|
|
||||||
return (int)(1000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
else if (width * height <= 854 * 480) {
|
|
||||||
return (int)(1500 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
// This covers 1280x720 and 1280x800 too
|
|
||||||
else if (width * height <= 1366 * 768) {
|
|
||||||
return (int)(5000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
else if (width * height <= 1920 * 1200) {
|
|
||||||
return (int)(10000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
else if (width * height <= 2560 * 1600) {
|
|
||||||
return (int)(20000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
else /* if (width * height <= 3840 * 2160) */ {
|
|
||||||
return (int)(40000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean getDefaultSmallMode(Context context) {
|
|
||||||
PackageManager manager = context.getPackageManager();
|
|
||||||
if (manager != null) {
|
|
||||||
// TVs shouldn't use small mode by default
|
|
||||||
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 21 uses LEANBACK instead of TELEVISION
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
||||||
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use small mode on anything smaller than a 7" tablet
|
|
||||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
return getDefaultBitrate(
|
|
||||||
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
|
|
||||||
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getVideoFormatValue(Context context) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
|
|
||||||
String str = prefs.getString(VIDEO_FORMAT_PREF_STRING, DEFAULT_VIDEO_FORMAT);
|
|
||||||
if (str.equals("auto")) {
|
|
||||||
return AUTOSELECT_H265;
|
|
||||||
}
|
|
||||||
else if (str.equals("forceh265")) {
|
|
||||||
return FORCE_H265_ON;
|
|
||||||
}
|
|
||||||
else if (str.equals("neverh265")) {
|
|
||||||
return FORCE_H265_OFF;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should never get here
|
|
||||||
return AUTOSELECT_H265;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void resetStreamingSettings(Context context) {
|
|
||||||
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
prefs.edit()
|
|
||||||
.remove(BITRATE_PREF_STRING)
|
|
||||||
.remove(BITRATE_PREF_OLD_STRING)
|
|
||||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
|
||||||
.remove(RESOLUTION_PREF_STRING)
|
|
||||||
.remove(FPS_PREF_STRING)
|
|
||||||
.remove(VIDEO_FORMAT_PREF_STRING)
|
|
||||||
.remove(ENABLE_HDR_PREF_STRING)
|
|
||||||
.remove(UNLOCK_FPS_STRING)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PreferenceConfiguration readPreferences(Context context) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
|
||||||
|
|
||||||
// Migrate legacy preferences to the new locations
|
|
||||||
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
|
||||||
if (str != null) {
|
|
||||||
if (str.equals("360p30")) {
|
|
||||||
config.width = 640;
|
|
||||||
config.height = 360;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("360p60")) {
|
|
||||||
config.width = 640;
|
|
||||||
config.height = 360;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("720p30")) {
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("720p60")) {
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p30")) {
|
|
||||||
config.width = 1920;
|
|
||||||
config.height = 1080;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p60")) {
|
|
||||||
config.width = 1920;
|
|
||||||
config.height = 1080;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("4K30")) {
|
|
||||||
config.width = 3840;
|
|
||||||
config.height = 2160;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("4K60")) {
|
|
||||||
config.width = 3840;
|
|
||||||
config.height = 2160;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should never get here
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit()
|
|
||||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
|
||||||
.putString(RESOLUTION_PREF_STRING, getResolutionString(config.width, config.height))
|
|
||||||
.putString(FPS_PREF_STRING, ""+config.fps)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Use the new preference location
|
|
||||||
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
|
||||||
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
|
||||||
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
|
||||||
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This must happen after the preferences migration to ensure the preferences are populated
|
|
||||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
|
||||||
if (config.bitrate == 0) {
|
|
||||||
config.bitrate = getDefaultBitrate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.videoFormat = getVideoFormatValue(context);
|
|
||||||
|
|
||||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
|
||||||
|
|
||||||
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
|
||||||
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
|
||||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
|
||||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
|
||||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
|
||||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
|
||||||
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
|
||||||
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
|
||||||
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
|
||||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
|
||||||
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
|
||||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
|
||||||
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
|
||||||
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
|
||||||
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
|
||||||
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
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 ANDROID_SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
|
||||||
private static final String SEEKBAR_SCHEMA_URL = "http://schemas.moonlight-stream.com/apk/res/seekbar";
|
|
||||||
|
|
||||||
private SeekBar seekBar;
|
|
||||||
private TextView valueText;
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
private final String dialogMessage;
|
|
||||||
private final String suffix;
|
|
||||||
private final int defaultValue;
|
|
||||||
private final int maxValue;
|
|
||||||
private final int minValue;
|
|
||||||
private final int stepSize;
|
|
||||||
private int currentValue;
|
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
// Read the message from XML
|
|
||||||
int dialogMessageId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "dialogMessage", 0);
|
|
||||||
if (dialogMessageId == 0) {
|
|
||||||
dialogMessage = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "dialogMessage");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dialogMessage = context.getString(dialogMessageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the suffix for the number displayed in the dialog
|
|
||||||
int suffixId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "text", 0);
|
|
||||||
if (suffixId == 0) {
|
|
||||||
suffix = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "text");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
suffix = context.getString(suffixId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get default, min, and max seekbar values
|
|
||||||
defaultValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
|
||||||
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
|
||||||
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
|
||||||
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
int roundedValue = ((value + (stepSize - 1))/stepSize)*stepSize;
|
|
||||||
if (roundedValue != value) {
|
|
||||||
seekBar.setProgress(roundedValue);
|
|
||||||
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 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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.preference.CheckBoxPreference;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
public class SmallIconCheckboxPreference extends CheckBoxPreference {
|
|
||||||
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Object onGetDefaultValue(TypedArray a, int index) {
|
|
||||||
return PreferenceConfiguration.getDefaultSmallMode(getContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.media.MediaCodecInfo;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceCategory;
|
|
||||||
import android.preference.PreferenceFragment;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.util.Range;
|
|
||||||
import android.view.Display;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.PcView;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
|
||||||
import com.limelight.utils.UiHelper;
|
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
|
||||||
private PreferenceConfiguration previousPrefs;
|
|
||||||
|
|
||||||
void reloadSettings() {
|
|
||||||
getFragmentManager().beginTransaction().replace(
|
|
||||||
R.id.stream_settings, new SettingsFragment()
|
|
||||||
).commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
previousPrefs = PreferenceConfiguration.readPreferences(this);
|
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_stream_settings);
|
|
||||||
reloadSettings();
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
finish();
|
|
||||||
|
|
||||||
// Check for changes that require a UI reload to take effect
|
|
||||||
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
|
|
||||||
if (newPrefs.listMode != previousPrefs.listMode ||
|
|
||||||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
|
|
||||||
!newPrefs.language.equals(previousPrefs.language)) {
|
|
||||||
// Restart the PC view to apply UI changes
|
|
||||||
Intent intent = new Intent(this, PcView.class);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
startActivity(intent, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
|
||||||
|
|
||||||
private void setValue(String preferenceKey, String value) {
|
|
||||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
|
||||||
|
|
||||||
pref.setValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
|
||||||
int matchingCount = 0;
|
|
||||||
|
|
||||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
|
||||||
|
|
||||||
// Count the number of matching entries we'll be removing
|
|
||||||
for (CharSequence seq : pref.getEntryValues()) {
|
|
||||||
if (seq.toString().equalsIgnoreCase(value)) {
|
|
||||||
matchingCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the new arrays
|
|
||||||
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
|
|
||||||
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
|
||||||
int outIndex = 0;
|
|
||||||
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
|
||||||
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
|
||||||
// Skip matching values
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries[outIndex] = pref.getEntries()[i];
|
|
||||||
entryValues[outIndex] = pref.getEntryValues()[i];
|
|
||||||
outIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pref.getValue().equalsIgnoreCase(value)) {
|
|
||||||
onMatched.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the preference with the new list
|
|
||||||
pref.setEntries(entries);
|
|
||||||
pref.setEntryValues(entryValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
|
||||||
if (res == null) {
|
|
||||||
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
|
||||||
}
|
|
||||||
if (fps == null) {
|
|
||||||
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit()
|
|
||||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
|
||||||
PreferenceConfiguration.getDefaultBitrate(res, fps))
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
|
||||||
PreferenceScreen screen = getPreferenceScreen();
|
|
||||||
|
|
||||||
// hide on-screen controls category on non touch screen devices
|
|
||||||
if (!getActivity().getPackageManager().
|
|
||||||
hasSystemFeature("android.hardware.touchscreen")) {
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_onscreen_controls");
|
|
||||||
screen.removePreference(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove PiP mode on devices pre-Oreo
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
|
||||||
}
|
|
||||||
|
|
||||||
int maxSupportedFps = 0;
|
|
||||||
|
|
||||||
// Hide non-supported resolution/FPS combinations
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
|
||||||
|
|
||||||
int maxSupportedResW = 0;
|
|
||||||
|
|
||||||
// Always allow resolutions that are smaller or equal to the active
|
|
||||||
// display resolution because decoders can report total non-sense to us.
|
|
||||||
// For example, a p201 device reports:
|
|
||||||
// AVC Decoder: OMX.amlogic.avc.decoder.awesome
|
|
||||||
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
|
|
||||||
// AVC supported width range: 64 - 384
|
|
||||||
// HEVC supported width range: 64 - 544
|
|
||||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
|
||||||
// Some devices report their dimensions in the portrait orientation
|
|
||||||
// where height > width. Normalize these to the conventional width > height
|
|
||||||
// arrangement before we process them.
|
|
||||||
|
|
||||||
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
|
||||||
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
|
||||||
|
|
||||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
|
|
||||||
maxSupportedResW = 2560;
|
|
||||||
}
|
|
||||||
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.getRefreshRate() > maxSupportedFps) {
|
|
||||||
maxSupportedFps = (int)candidate.getRefreshRate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This must be called to do runtime initialization before calling functions that evaluate
|
|
||||||
// decoder lists.
|
|
||||||
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
|
|
||||||
|
|
||||||
MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1);
|
|
||||||
MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
|
||||||
|
|
||||||
if (avcDecoder != null) {
|
|
||||||
Range<Integer> avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
|
||||||
|
|
||||||
LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper());
|
|
||||||
|
|
||||||
// If 720p is not reported as supported, ignore all results from this API
|
|
||||||
if (avcWidthRange.contains(1280)) {
|
|
||||||
if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
else if (maxSupportedResW < 1280) {
|
|
||||||
maxSupportedResW = 1280;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hevcDecoder != null) {
|
|
||||||
Range<Integer> hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
|
||||||
|
|
||||||
LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper());
|
|
||||||
|
|
||||||
// If 720p is not reported as supported, ignore all results from this API
|
|
||||||
if (hevcWidthRange.contains(1280)) {
|
|
||||||
if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
else if (maxSupportedResW < 1280) {
|
|
||||||
maxSupportedResW = 1280;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
|
||||||
|
|
||||||
if (maxSupportedResW != 0) {
|
|
||||||
if (maxSupportedResW < 3840) {
|
|
||||||
// 4K is unsupported
|
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maxSupportedResW < 2560) {
|
|
||||||
// 1440p is unsupported
|
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maxSupportedResW < 1920) {
|
|
||||||
// 1080p is unsupported
|
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Never remove 720p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
|
||||||
// We give some extra room in case the FPS is rounded down
|
|
||||||
if (maxSupportedFps < 118) {
|
|
||||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "120", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "90");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maxSupportedFps < 88) {
|
|
||||||
// 1080p is unsupported
|
|
||||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "90", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "60");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Never remove 30 FPS or 60 FPS
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
|
|
||||||
// that the unlock FPS option relies on to not massively increase latency.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_unlock_fps"));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
// HACK: We need to let the preference change succeed before reinitializing to ensure
|
|
||||||
// it's reflected in the new layout.
|
|
||||||
final Handler h = new Handler();
|
|
||||||
h.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Ensure the activity is still open when this timeout expires
|
|
||||||
StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity();
|
|
||||||
if (settingsActivity != null) {
|
|
||||||
settingsActivity.reloadSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Allow the original preference change to take place
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove HDR preference for devices below Nougat
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
||||||
LimeLog.info("Excluding HDR toggle based on OS");
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_hdr"));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
|
||||||
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
|
|
||||||
|
|
||||||
// We must now ensure our display is compatible with HDR10
|
|
||||||
boolean foundHdr10 = false;
|
|
||||||
if (hdrCaps != null) {
|
|
||||||
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
|
||||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
|
||||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
|
||||||
foundHdr10 = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundHdr10) {
|
|
||||||
LimeLog.info("Excluding HDR toggle based on display capabilities");
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_hdr"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a listener to the FPS and resolution preference
|
|
||||||
// so the bitrate can be auto-adjusted
|
|
||||||
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
String valueStr = (String) newValue;
|
|
||||||
|
|
||||||
// Write the new bitrate value
|
|
||||||
resetBitrateToDefault(prefs, valueStr, null);
|
|
||||||
|
|
||||||
// Allow the original preference change to take place
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
findPreference(PreferenceConfiguration.FPS_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
String valueStr = (String) newValue;
|
|
||||||
|
|
||||||
// Write the new bitrate value
|
|
||||||
resetBitrateToDefault(prefs, null, valueStr);
|
|
||||||
|
|
||||||
// Allow the original preference change to take place
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.limelight.ui;
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
|
|
||||||
public class AdapterFragment extends Fragment {
|
|
||||||
private AdapterFragmentCallbacks callbacks;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Activity activity) {
|
|
||||||
super.onAttach(activity);
|
|
||||||
|
|
||||||
callbacks = (AdapterFragmentCallbacks) activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityCreated(Bundle savedInstanceState) {
|
|
||||||
super.onActivityCreated(savedInstanceState);
|
|
||||||
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.limelight.ui;
|
|
||||||
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
|
|
||||||
public interface AdapterFragmentCallbacks {
|
|
||||||
int getAdapterFragmentLayoutId();
|
|
||||||
void receiveAbsListView(AbsListView gridView);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.limelight.ui;
|
|
||||||
|
|
||||||
public interface GameGestures {
|
|
||||||
void showKeyboard();
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user