Compare commits
352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44871626cf | |||
| f661522b5d | |||
| a454b0ab78 | |||
| 75bf84d0d9 | |||
| c248994ed4 | |||
| a7a34ec629 | |||
| 8d469c5d0a | |||
| e6979d50b5 | |||
| 6e25b135a3 | |||
| 04e093a2c2 | |||
| 813f2edd95 | |||
| 337d753a33 | |||
| 1137c74f76 | |||
| 0c1451f757 | |||
| 5ab9ea48fd | |||
| ffcb623040 | |||
| bfe6929642 | |||
| 50d45011a8 | |||
| 2f7087d6d3 | |||
| 92b71588d0 | |||
| 4f3d018764 | |||
| a22e33eeb9 | |||
| 6a939e7495 | |||
| f8ba7cf190 | |||
| d1e135db4d | |||
| 61a17afe69 | |||
| 47fd691884 | |||
| 0d171c6b28 | |||
| f0c69d08b8 | |||
| 629bf5766d | |||
| 233bceeece | |||
| 6660ea7d91 | |||
| 4864b2ca45 | |||
| 92097b318d | |||
| 997898c99d | |||
| 1174e03885 | |||
| ff0f54d541 | |||
| 814964a100 | |||
| 7e154292a9 | |||
| 0f9cba1053 | |||
| a4e134589d | |||
| cd80a94f28 | |||
| 57c645a291 | |||
| 0cba200207 | |||
| 81582d7343 | |||
| 04e561fd54 | |||
| 5efbb5229d | |||
| 541e43eb18 | |||
| 7e679ff4c6 | |||
| 486b4b4c4c | |||
| 7d76bf7868 | |||
| db49077b9b | |||
| 16b845ab84 | |||
| 5c175fecf6 | |||
| 773976b265 | |||
| 80070bbdbe | |||
| 4c8d433b6c | |||
| 404f096d11 | |||
| d2ac927cec | |||
| 5e3d59d3d7 | |||
| 9cd2ce1309 | |||
| 9ed49730d4 | |||
| 39ebb48f58 | |||
| 1c29c70fba | |||
| 6993051529 | |||
| 4930087c4d | |||
| 795f0a013b | |||
| 213414778e | |||
| 7eac0ccaf8 | |||
| 6adc9dcb2d | |||
| be620908f9 | |||
| e4edfdb043 | |||
| 3b5028d1a4 | |||
| bc8c45bd59 | |||
| 63eb346a70 | |||
| 27ad691d23 | |||
| 747e920061 | |||
| 8d09f56a0e | |||
| 113a0e2c45 | |||
| 977215a098 | |||
| a7e65b47f9 | |||
| 7126055ad6 | |||
| 3de9765eaa | |||
| d4072eb295 | |||
| cac2bdbb81 | |||
| 66f0aee3f8 | |||
| b690dc5474 | |||
| c2fbe6ad91 | |||
| cf07c02398 | |||
| 42dc928ad5 | |||
| 11597f0aa7 | |||
| cdcd4d48f2 | |||
| a9af4e54a9 | |||
| 7eac609219 | |||
| fa761debc4 | |||
| 62e175f069 | |||
| d7d8c40565 | |||
| 64de13ab50 | |||
| 2f02939638 | |||
| 1d7c8697e9 | |||
| 7dea322bbd | |||
| 349ecb16ab | |||
| a3867735c1 | |||
| 5b087e9f70 | |||
| eed18223eb | |||
| 30d4d2a918 | |||
| 30f666c70e | |||
| 209fead0e8 | |||
| 5c6889bf6d | |||
| 7d24900756 | |||
| 79a75b9d19 | |||
| 29b64992bd | |||
| c9b14540f2 | |||
| 546843a26c | |||
| d03d260535 | |||
| 6946e3c7a2 | |||
| b79d328961 | |||
| c313797d93 | |||
| c8cb8e1346 | |||
| 6a9f8da14e | |||
| ff9260a0fd | |||
| 62bedb1609 | |||
| a519723d44 | |||
| 36191781ed | |||
| 61b6a49669 | |||
| e97845e46e | |||
| 6bba68207d | |||
| 0e17cccc06 | |||
| 918e922e40 | |||
| a08854ddfd | |||
| eb6f15c2b7 | |||
| 2cd9e31684 | |||
| 791d6624e2 | |||
| af41021271 | |||
| d726d939f4 | |||
| 748085e7bb | |||
| d57d19174b | |||
| efebe1828a | |||
| 06007e0597 | |||
| 3a868045d7 | |||
| e0a7ff1880 | |||
| 88d43bbd40 | |||
| 30ff319b13 | |||
| 9a0f48b799 | |||
| b52c8a1a8f | |||
| 3fde115670 | |||
| b6f4d8ff1e | |||
| a7d85a7dd5 | |||
| 9b238ab6c3 | |||
| f82ee97c05 | |||
| 35fb96f9f4 | |||
| 37371906d5 | |||
| 83a9539f4b | |||
| b214fe5301 | |||
| 57779b4e89 | |||
| 547932f8b2 | |||
| 762fa0fe2f | |||
| 9cedc57df2 | |||
| ba81f8096a | |||
| c4fa654166 | |||
| 8ac440b68b | |||
| 165386b941 | |||
| 3a7398f321 | |||
| ebb1d0dfa2 | |||
| 1ca1ed5d20 | |||
| b416bafb78 | |||
| 3a301b74a6 | |||
| 71d463f063 | |||
| 1fae816223 | |||
| 989d6fc169 | |||
| 381509b3a6 | |||
| d8ae40376e | |||
| 4ea93f5e68 | |||
| cd84c8f30e | |||
| 8d4cdca7c3 | |||
| c0239c36fd | |||
| 9d9f729e42 | |||
| 6c5fe18b6e | |||
| 1994bf6522 | |||
| 31381e5664 | |||
| fac1b1d7e5 | |||
| 40c406051c | |||
| 8bac873e67 | |||
| a170e1efd7 | |||
| 17bffa8d78 | |||
| 289222749b | |||
| 81d84600d4 | |||
| 0b15fd582d | |||
| cbe4a1cde6 | |||
| 89ef16c02e | |||
| 58b6ed8d00 | |||
| 7d01e1a7a4 | |||
| ab769a1606 | |||
| 3ac9abbab1 | |||
| 288efd0726 | |||
| d2d0ed65d6 | |||
| e697ed72db | |||
| b657c746be | |||
| 947f8db2d5 | |||
| 15857efd36 | |||
| 3fd0f20e10 | |||
| a2e64fd7df | |||
| a620dc7d0c | |||
| 9d7a28e408 | |||
| 3244344fc7 | |||
| 75057f2d39 | |||
| bbec3402d9 | |||
| dcf4dac8dd | |||
| d98f484aaf | |||
| 0218a9ce14 | |||
| 0ec6dcd67e | |||
| 88f9b68db7 | |||
| 3c2fd32d1e | |||
| 6557cba307 | |||
| ae6f797436 | |||
| 3442a64f4d | |||
| 37ddccde0c | |||
| ffc59c6bd6 | |||
| 88f84a0c12 | |||
| 03ecf3e5ac | |||
| 617c8582b4 | |||
| ef3b28295b | |||
| 3bcd2ee068 | |||
| d4ff58b3ad | |||
| c797318ece | |||
| 82387d23f8 | |||
| 766e629be5 | |||
| b93aa42c0c | |||
| 36f132942f | |||
| e4c251e7ee | |||
| fb54bd5c78 | |||
| 8d4c86e113 | |||
| 7fafb8e0ff | |||
| fbcbe09255 | |||
| e336a4446a | |||
| ffb35b2cdd | |||
| 2d0af6281c | |||
| 472a7f6c8a | |||
| cd06559c66 | |||
| d833933aaa | |||
| dc3495d59b | |||
| e3a2e40043 | |||
| 31e1fb743e | |||
| bc59f11096 | |||
| 6d97775aa9 | |||
| 3fff34e08a | |||
| 15e856dccb | |||
| 07d04171c3 | |||
| 42bd93cb3a | |||
| 7d289f1134 | |||
| 214461e123 | |||
| b0144a3256 | |||
| 3171256c6e | |||
| 5c69f6716c | |||
| 6264781539 | |||
| 0225f534d0 | |||
| 284a31737e | |||
| b37a2dea57 | |||
| 5c865e7f36 | |||
| 04d9aea8c8 | |||
| b6f52db9c3 | |||
| 99d2e40683 | |||
| 02c4ed2724 | |||
| 5f4aab8f94 | |||
| ec65901003 | |||
| 915acee88d | |||
| 300d444f71 | |||
| f37ab40c2f | |||
| 16e285d926 | |||
| f2d122a275 | |||
| bfa5a6349e | |||
| a56689aea3 | |||
| 3a5ba820cb | |||
| ec69fef36f | |||
| ff38074f55 | |||
| 85d0ce0c40 | |||
| 777129ca90 | |||
| 06156c4d68 | |||
| 1c725b9dac | |||
| f761ee52db | |||
| 05e8cfcc0a | |||
| 912925ef2c | |||
| 4deb881ec8 | |||
| f55d6308ce | |||
| 44a3a141c0 | |||
| 37b5ba004c | |||
| b774b47213 | |||
| 74dc00445e | |||
| 3b4563d5ea | |||
| 38669817b4 | |||
| 8f1d3ae04e | |||
| 74ed95871b | |||
| cc5d67616c | |||
| eed7f09e6f | |||
| e3c1d23744 | |||
| c4b1200b43 | |||
| dff09f33a3 | |||
| 1f6b1dc2fe | |||
| 3f118dae93 | |||
| 91a30ff6fe | |||
| 5102669b06 | |||
| 2e2f09be00 | |||
| c402103fe3 | |||
| 5e5df8abc8 | |||
| d125eb7b16 | |||
| a116858493 | |||
| 5f3b333e98 | |||
| 80a37855c7 | |||
| 5db1ec8ec0 | |||
| 8911c58e50 | |||
| 780a64694d | |||
| 3c5ea9c8c3 | |||
| 40d1436ce3 | |||
| dbb02acd37 | |||
| 20c4eac4ef | |||
| b9f1142af7 | |||
| 38a6a2b74a | |||
| fd2421618a | |||
| 79a9ea7179 | |||
| 34a11c9262 | |||
| 84a9845c1d | |||
| 5b05220008 | |||
| b2bd7257e1 | |||
| 46a998c113 | |||
| 60cd951774 | |||
| d4f8d8f689 | |||
| 608a0ebb5b | |||
| f01a15d182 | |||
| 0268b4f958 | |||
| d71cf0eb98 | |||
| 10ab40f823 | |||
| 427edfa021 | |||
| 6f18831d5c | |||
| a3db09f422 | |||
| d185a05b1d | |||
| 78e575504a | |||
| 0a0be19b69 | |||
| 0792157e9d | |||
| cdd0ecf0b7 | |||
| 1ac721a35b | |||
| e49b1c92a2 | |||
| db4295bf83 | |||
| 824c37f9d5 | |||
| acf4426952 | |||
| e8c50342ab | |||
| 598995de3b | |||
| 01cf0cc649 | |||
| fa560f462f | |||
| f6e40118a9 | |||
| fe7148dbd4 | |||
| 60de065836 | |||
| 6f82f82abb |
@@ -0,0 +1,8 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||||
|
|
||||||
|
daysUntilStale: 90
|
||||||
|
daysUntilClose: 7
|
||||||
|
exemptLabels:
|
||||||
|
- accepted
|
||||||
|
- bug
|
||||||
|
- enhancement
|
||||||
|
- meta
|
||||||
|
staleLabel: stale
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs.
|
||||||
|
closeComment: false
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
#built application files
|
# built application files
|
||||||
*.apk
|
*.apk
|
||||||
*.ap_
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
output.json
|
||||||
|
out/
|
||||||
|
|
||||||
# files for the dex VM
|
# files for the dex VM
|
||||||
*.dex
|
*.dex
|
||||||
@@ -30,7 +33,7 @@ Thumbs.db
|
|||||||
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
app/app.iml
|
*.iml
|
||||||
|
|
||||||
# Compiled JNI libraries folder
|
# Compiled JNI libraries folder
|
||||||
**/jniLibs
|
**/jniLibs
|
||||||
|
|||||||
@@ -1,44 +1,42 @@
|
|||||||
#Moonlight
|
# Moonlight Android
|
||||||
|
|
||||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight](https://moonlight-stream.org) 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,
|
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
whether in your own home or over the internet.
|
whether in your own home or over the internet.
|
||||||
|
|
||||||
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
|
||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
##Features
|
## Features
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
* Streams any of your games from your PC to your Android device
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* Automatically finds GameStream-compatible PCs on your network
|
||||||
|
|
||||||
##Installation
|
## Installation
|
||||||
|
|
||||||
* Download and install Moonlight for Android from
|
* Download and install Moonlight for Android from
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||||
* 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 GTX 600/700 series GPU
|
* [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)
|
||||||
* Android device running 4.1 (Jelly Bean) or higher
|
* Android device running 4.1 (Jelly Bean) or higher
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
* High-end wireless router (802.11n dual-band recommended)
|
||||||
|
|
||||||
##Usage
|
## Usage
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC
|
* Accept the pairing confirmation on your PC and add the PIN if needed
|
||||||
* Tap your PC again to view the list of apps to stream
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
|
|
||||||
##Contribute
|
## Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
|
|
||||||
@@ -46,13 +44,13 @@ 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
|
## Building
|
||||||
* Install Android Studio and the Android NDK
|
* Install Android Studio and the Android NDK
|
||||||
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
* 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.
|
* 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
|
* 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)
|
||||||
|
|||||||
@@ -1,37 +1,44 @@
|
|||||||
import com.android.builder.model.ProductFlavor
|
|
||||||
import org.apache.tools.ant.taskdefs.condition.Os
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 26
|
buildToolsVersion '28.0.3'
|
||||||
buildToolsVersion '26.0.0'
|
compileSdkVersion 28
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 26
|
targetSdkVersion 28
|
||||||
|
|
||||||
versionName "5.1"
|
versionName "7.2"
|
||||||
versionCode = 128
|
versionCode = 191
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "root"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
root {
|
root {
|
||||||
// Android O has native mouse capture, so don't show the rooted
|
// Android O has native mouse capture, so don't show the rooted
|
||||||
// version to devices running O on the Play Store.
|
// version to devices running O on the Play Store.
|
||||||
maxSdkVersion 25
|
maxSdkVersion 25
|
||||||
|
|
||||||
applicationId "com.limelight.root"
|
externalNativeBuild {
|
||||||
ndk {
|
ndkBuild {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
arguments "PRODUCT_FLAVOR=root"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applicationId "com.limelight.root"
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
applicationId "com.limelight"
|
externalNativeBuild {
|
||||||
ndk {
|
ndkBuild {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
arguments "PRODUCT_FLAVOR=nonRoot"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applicationId "com.limelight"
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +46,63 @@ android {
|
|||||||
disable 'MissingTranslation'
|
disable 'MissingTranslation'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bundle {
|
||||||
|
language {
|
||||||
|
// Avoid splitting by language, since we allow users
|
||||||
|
// to manually switch language in settings.
|
||||||
|
enableSplit = false
|
||||||
|
}
|
||||||
|
density {
|
||||||
|
// FIXME: This should not be neccessary but we get
|
||||||
|
// weird crashes due to missing drawable resources
|
||||||
|
// when this split is enabled.
|
||||||
|
enableSplit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
|
||||||
|
minifyEnabled true
|
||||||
|
useProguard false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
// To whomever is releasing/using an APK in release mode with
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
// 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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +114,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||||
compile 'org.bouncycastle:bcpkix-jdk15on:1.52'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
||||||
compile files('libs/jcodec-0.1.9-patched.jar')
|
implementation 'org.jcodec:jcodec:0.2.3'
|
||||||
|
|
||||||
debugCompile project(path:':moonlight-common', configuration:'debug')
|
implementation project(':moonlight-common')
|
||||||
releaseCompile project(path:':moonlight-common', configuration:'release')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 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.**
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_label" translatable="false">Moonlight (Debug)</string>
|
||||||
|
<string name="app_label_root" translatable="false">Moonlight (Root Debug)</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
@@ -24,12 +25,20 @@
|
|||||||
android:name="android.software.leanback"
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<!-- Disable legacy input emulation on ChromeOS -->
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.type.pc"
|
||||||
|
android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:isGame="true"
|
android:isGame="true"
|
||||||
android:banner="@drawable/atv_banner"
|
android:banner="@drawable/atv_banner"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:appCategory="game"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<!-- Samsung multi-window support -->
|
<!-- Samsung multi-window support -->
|
||||||
@@ -59,8 +68,9 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppViewShortcutTrampoline"
|
android:name=".ShortcutTrampoline"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
|
android:exported="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -91,12 +101,16 @@
|
|||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".Game"
|
android:name=".Game"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="userLandscape"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
android:theme="@style/StreamTheme">
|
android:theme="@style/StreamTheme">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 58 KiB |
@@ -1,9 +1,8 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -27,7 +26,9 @@ import android.app.Service;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
@@ -38,10 +39,13 @@ import android.view.ContextMenu.ContextMenuInfo;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private AppGridAdapter appGridAdapter;
|
private AppGridAdapter appGridAdapter;
|
||||||
private String uuidString;
|
private String uuidString;
|
||||||
@@ -58,7 +62,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private final static int START_OR_RESUME_ID = 1;
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
private final static int CANCEL_ID = 3;
|
private final static int CANCEL_ID = 3;
|
||||||
private final static int START_WTIH_QUIT = 4;
|
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 NAME_EXTRA = "Name";
|
||||||
public final static String UUID_EXTRA = "UUID";
|
public final static String UUID_EXTRA = "UUID";
|
||||||
@@ -76,11 +82,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Wait for the binder to be ready
|
// Wait for the binder to be ready
|
||||||
localBinder.waitForReady();
|
localBinder.waitForReady();
|
||||||
|
|
||||||
// Now make the binder visible
|
|
||||||
managerBinder = localBinder;
|
|
||||||
|
|
||||||
// Get the computer object
|
// Get the computer object
|
||||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
computer = localBinder.getComputer(uuidString);
|
||||||
if (computer == null) {
|
if (computer == null) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
@@ -90,13 +93,18 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||||
computer, managerBinder.getUniqueId());
|
computer, localBinder.getUniqueId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now make the binder visible. We must do this after appGridAdapter
|
||||||
|
// is set to prevent us from reaching updateUiWithServerinfo() and
|
||||||
|
// touching the appGridAdapter prior to initialization.
|
||||||
|
managerBinder = localBinder;
|
||||||
|
|
||||||
// Load the app grid with cached data (if possible).
|
// Load the app grid with cached data (if possible).
|
||||||
// This must be done _before_ startComputerUpdates()
|
// This must be done _before_ startComputerUpdates()
|
||||||
// so the initial serverinfo response can update the running
|
// so the initial serverinfo response can update the running
|
||||||
@@ -149,7 +157,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't care about other computers
|
// Don't care about other computers
|
||||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +181,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Disable shortcuts referencing this PC for now
|
// Disable shortcuts referencing this PC for now
|
||||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
shortcutHelper.disableShortcut(details.uuid,
|
||||||
getResources().getString(R.string.scut_not_paired));
|
getResources().getString(R.string.scut_not_paired));
|
||||||
|
|
||||||
// Display a toast to the user and quit the activity
|
// Display a toast to the user and quit the activity
|
||||||
@@ -209,7 +217,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
blockingLoadSpinner.dismiss();
|
blockingLoadSpinner.dismiss();
|
||||||
blockingLoadSpinner = null;
|
blockingLoadSpinner = null;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,6 +247,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Assume we're in the foreground when created to avoid a race
|
||||||
|
// between binding to CMS and onResume()
|
||||||
|
inForeground = true;
|
||||||
|
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
@@ -249,10 +263,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
|
||||||
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
TextView label = findViewById(R.id.appListText);
|
||||||
TextView label = (TextView) findViewById(R.id.appListText);
|
setTitle(computerName);
|
||||||
setTitle(labelText);
|
label.setText(computerName);
|
||||||
label.setText(labelText);
|
|
||||||
|
|
||||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||||
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
||||||
@@ -270,7 +283,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
||||||
updateUiWithAppList(applist);
|
updateUiWithAppList(applist);
|
||||||
LimeLog.info("Loaded applist from cache");
|
LimeLog.info("Loaded applist from cache");
|
||||||
} catch (Exception e) {
|
} catch (IOException | XmlPullParserException e) {
|
||||||
if (lastRawApplist != null) {
|
if (lastRawApplist != null) {
|
||||||
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -302,6 +315,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
|
// Display a decoder crash notification if we've returned after a crash
|
||||||
|
UiHelper.showDecoderCrashDialog(this);
|
||||||
|
|
||||||
inForeground = true;
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
@@ -326,10 +342,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
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, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// Only add an option to create shortcut if box art is loaded
|
||||||
|
// and when we're in grid-mode (not list-mode).
|
||||||
|
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
||||||
|
if (appImageView != null) {
|
||||||
|
// We have a grid ImageView, so we must be in grid-mode
|
||||||
|
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
||||||
|
if (drawable != null && drawable.getBitmap() != null) {
|
||||||
|
// We have a bitmap loaded too
|
||||||
|
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -341,7 +372,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case START_WTIH_QUIT:
|
case START_WITH_QUIT:
|
||||||
// Display a confirmation dialog first
|
// Display a confirmation dialog first
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -362,8 +393,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
suspendGridUpdates = true;
|
suspendGridUpdates = true;
|
||||||
ServerHelper.doQuit(AppView.this,
|
ServerHelper.doQuit(AppView.this, computer,
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer),
|
|
||||||
app.app, managerBinder, new Runnable() {
|
app.app, managerBinder, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -381,6 +411,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
case CANCEL_ID:
|
case CANCEL_ID:
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case VIEW_DETAILS_ID:
|
||||||
|
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
|
||||||
|
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case CREATE_SHORTCUT_ID:
|
||||||
|
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
||||||
|
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
|
||||||
|
if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
|
||||||
|
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,164 +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.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 AppViewShortcutTrampoline extends Activity {
|
|
||||||
private String uuidString;
|
|
||||||
|
|
||||||
private ComputerDetails computer;
|
|
||||||
private SpinnerDialog blockingLoadSpinner;
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Now make the binder visible
|
|
||||||
managerBinder = localBinder;
|
|
||||||
|
|
||||||
// Get the computer object
|
|
||||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
|
||||||
|
|
||||||
// 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.toString().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 (details.state == ComputerDetails.State.ONLINE) {
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
|
|
||||||
// Create a new activity stack for this launch
|
|
||||||
ArrayList<Intent> intentStack = new ArrayList<>();
|
|
||||||
Intent i;
|
|
||||||
|
|
||||||
// Add the PC view at the back (and clear the task)
|
|
||||||
i = new Intent(AppViewShortcutTrampoline.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(AppViewShortcutTrampoline.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(AppViewShortcutTrampoline.this,
|
|
||||||
new NvApp("app", details.runningGameId), 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(AppViewShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.error_pc_offline),
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
|
||||||
|
|
||||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
|
||||||
|
|
||||||
// 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 onPause() {
|
|
||||||
super.onPause();
|
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
|
||||||
blockingLoadSpinner.dismiss();
|
|
||||||
blockingLoadSpinner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
managerBinder.stopPolling();
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,14 +51,8 @@ public class HelpActivity extends Activity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
if (url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
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())) {
|
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
|
||||||
// Allow navigation to Moonlight docs
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package com.limelight;
|
|||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
@@ -18,6 +16,7 @@ import com.limelight.nvstream.http.PairingManager;
|
|||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
import com.limelight.nvstream.wol.WakeOnLanSender;
|
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||||
import com.limelight.preferences.AddComputerManually;
|
import com.limelight.preferences.AddComputerManually;
|
||||||
|
import com.limelight.preferences.GlPreferences;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.preferences.StreamSettings;
|
import com.limelight.preferences.StreamSettings;
|
||||||
import com.limelight.ui.AdapterFragment;
|
import com.limelight.ui.AdapterFragment;
|
||||||
@@ -29,11 +28,14 @@ import com.limelight.utils.ShortcutHelper;
|
|||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.ActivityManager;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.opengl.GLSurfaceView;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
@@ -51,12 +53,17 @@ import android.widget.RelativeLayout;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
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 {
|
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private RelativeLayout noPcFoundLayout;
|
private RelativeLayout noPcFoundLayout;
|
||||||
private PcGridAdapter pcGridAdapter;
|
private PcGridAdapter pcGridAdapter;
|
||||||
private ShortcutHelper shortcutHelper;
|
private ShortcutHelper shortcutHelper;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling, inForeground;
|
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
@@ -86,11 +93,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
|
|
||||||
// Reinitialize views just in case orientation changed
|
// Only reinitialize views if completeOnCreate() was called
|
||||||
initializeViews();
|
// 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 APP_LIST_ID = 1;
|
||||||
@@ -100,6 +115,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private final static int DELETE_ID = 5;
|
private final static int DELETE_ID = 5;
|
||||||
private final static int RESUME_ID = 6;
|
private final static int RESUME_ID = 6;
|
||||||
private final static int QUIT_ID = 7;
|
private final static int QUIT_ID = 7;
|
||||||
|
private final static int VIEW_DETAILS_ID = 8;
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
@@ -110,9 +126,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
||||||
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
||||||
ImageButton helpButton = (ImageButton) findViewById(R.id.helpButton);
|
ImageButton helpButton = findViewById(R.id.helpButton);
|
||||||
|
|
||||||
settingsButton.setOnClickListener(new OnClickListener() {
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -138,7 +154,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
|
|
||||||
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
|
noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
|
||||||
if (pcGridAdapter.getCount() == 0) {
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
@@ -152,6 +168,52 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Assume we're in the foreground when created to avoid a race
|
||||||
|
// 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);
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
@@ -220,6 +282,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
|
// Display a decoder crash notification if we've returned after a crash
|
||||||
|
UiHelper.showDecoderCrashDialog(this);
|
||||||
|
|
||||||
inForeground = true;
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
@@ -250,8 +315,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
|
|
||||||
// Inflate the context menu
|
// Inflate the context menu
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
|
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
||||||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
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, 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));
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
@@ -271,6 +336,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// it with delete which actually work
|
// 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, 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
|
@Override
|
||||||
@@ -282,7 +348,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
private void doPair(final ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
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();
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -306,23 +373,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Stop updates and wait while pairing
|
// Stop updates and wait while pairing
|
||||||
stopComputerUpdates(true);
|
stopComputerUpdates(true);
|
||||||
|
|
||||||
InetAddress addr;
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
|
||||||
addr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.warning("Unknown reachability - using local IP");
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
computer.serverCert,
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
if (httpConn.getPairState() == PairState.PAIRED) {
|
||||||
// Don't display any toast, but open the app list
|
// Don't display any toast, but open the app list
|
||||||
message = null;
|
message = null;
|
||||||
success = true;
|
success = true;
|
||||||
@@ -334,21 +389,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||||
|
|
||||||
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
PairingManager pm = httpConn.getPairingManager();
|
||||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
|
||||||
|
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
|
||||||
|
if (pairState == PairState.PIN_WRONG) {
|
||||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||||
}
|
}
|
||||||
else if (pairState == PairingManager.PairState.FAILED) {
|
else if (pairState == PairState.FAILED) {
|
||||||
message = getResources().getString(R.string.pair_fail);
|
message = getResources().getString(R.string.pair_fail);
|
||||||
}
|
}
|
||||||
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) {
|
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
|
||||||
message = getResources().getString(R.string.pair_already_in_progress);
|
message = getResources().getString(R.string.pair_already_in_progress);
|
||||||
}
|
}
|
||||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
else if (pairState == PairState.PAIRED) {
|
||||||
// Just navigate to the app view without displaying a toast
|
// Just navigate to the app view without displaying a toast
|
||||||
message = null;
|
message = null;
|
||||||
success = true;
|
success = true;
|
||||||
|
|
||||||
|
// Pin this certificate for later HTTPS use
|
||||||
|
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
|
||||||
|
|
||||||
// Invalidate reachability information after pairing to force
|
// Invalidate reachability information after pairing to force
|
||||||
// a refresh before reading pair state again
|
// a refresh before reading pair state again
|
||||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||||
@@ -362,7 +422,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
message = getResources().getString(R.string.error_unknown_host);
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = getResources().getString(R.string.error_404);
|
message = getResources().getString(R.string.error_404);
|
||||||
} catch (Exception e) {
|
} catch (XmlPullParserException | IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
@@ -403,7 +463,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -427,7 +486,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doUnpair(final ComputerDetails computer) {
|
private void doUnpair(final ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
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();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -443,21 +503,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
InetAddress addr;
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
|
||||||
addr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.warning("Unknown reachability - using local IP");
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
computer.serverCert,
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
httpConn.unpair();
|
httpConn.unpair();
|
||||||
@@ -475,8 +523,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
message = getResources().getString(R.string.error_unknown_host);
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = getResources().getString(R.string.error_404);
|
message = getResources().getString(R.string.error_404);
|
||||||
} catch (Exception e) {
|
} catch (XmlPullParserException | IOException e) {
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String toastMessage = message;
|
final String toastMessage = message;
|
||||||
@@ -491,7 +540,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doAppList(ComputerDetails computer) {
|
private void doAppList(ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.state == ComputerDetails.State.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -502,7 +551,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
Intent i = new Intent(this, AppView.class);
|
Intent i = new Intent(this, AppView.class);
|
||||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
|
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,12 +573,21 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
case DELETE_ID:
|
case DELETE_ID:
|
||||||
if (managerBinder == null) {
|
if (ActivityManager.isUserAMonkey()) {
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
LimeLog.info("Ignoring delete PC request from monkey");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
managerBinder.removeComputer(computer.details.name);
|
UiHelper.displayDeletePcConfirmationDialog(this, computer.details, new Runnable() {
|
||||||
removeComputer(computer.details);
|
@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;
|
return true;
|
||||||
|
|
||||||
case APP_LIST_ID:
|
case APP_LIST_ID:
|
||||||
@@ -542,7 +600,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
|
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case QUIT_ID:
|
case QUIT_ID:
|
||||||
@@ -555,13 +613,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
ServerHelper.doQuit(PcView.this,
|
ServerHelper.doQuit(PcView.this, computer.details,
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
new NvApp("app", 0, false), managerBinder, null);
|
||||||
new NvApp("app", 0), managerBinder, null);
|
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case VIEW_DETAILS_ID:
|
||||||
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -573,7 +634,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
if (details.equals(computer.details)) {
|
if (details.equals(computer.details)) {
|
||||||
// Disable or delete shortcuts referencing this PC
|
// Disable or delete shortcuts referencing this PC
|
||||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
shortcutHelper.disableShortcut(details.uuid,
|
||||||
getResources().getString(R.string.scut_deleted_pc));
|
getResources().getString(R.string.scut_deleted_pc));
|
||||||
|
|
||||||
pcGridAdapter.removeComputer(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
@@ -604,7 +665,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
// Add a launcher shortcut for this PC
|
// Add a launcher shortcut for this PC
|
||||||
if (details.pairState == PairState.PAIRED) {
|
if (details.pairState == PairState.PAIRED) {
|
||||||
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false);
|
shortcutHelper.createAppViewShortcut(details.uuid, details, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
@@ -638,8 +699,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
long id) {
|
long id) {
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
if (computer.details.state == ComputerDetails.State.UNKNOWN ||
|
||||||
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
computer.details.state == ComputerDetails.State.OFFLINE) {
|
||||||
// Open the context menu if a PC is offline or refreshing
|
// Open the context menu if a PC is offline or refreshing
|
||||||
openContextMenu(arg1);
|
openContextMenu(arg1);
|
||||||
} else if (computer.details.pairState != PairState.PAIRED) {
|
} else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
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,8 +1,10 @@
|
|||||||
package com.limelight.binding.audio;
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
|
import android.media.AudioAttributes;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
@@ -12,10 +14,58 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
|
|
||||||
private AudioTrack track;
|
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
|
@Override
|
||||||
public int setup(int audioConfiguration) {
|
public int setup(int audioConfiguration) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bufferSize;
|
|
||||||
int bytesPerFrame;
|
int bytesPerFrame;
|
||||||
|
|
||||||
switch (audioConfiguration)
|
switch (audioConfiguration)
|
||||||
@@ -38,44 +88,82 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
// do this on many devices and it lowers audio latency.
|
// do this on many devices and it lowers audio latency.
|
||||||
// We'll try the small buffer size first and if it fails,
|
// We'll try the small buffer size first and if it fails,
|
||||||
// use the recommended larger buffer size.
|
// use the recommended larger buffer size.
|
||||||
try {
|
|
||||||
// Buffer two frames of audio if possible
|
|
||||||
bufferSize = bytesPerFrame * 2;
|
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
for (int i = 0; i < 4; i++) {
|
||||||
48000,
|
boolean lowLatency;
|
||||||
channelConfig,
|
int bufferSize;
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
|
||||||
track.play();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Try to release the AudioTrack if we got far enough
|
|
||||||
try {
|
|
||||||
if (track != null) {
|
|
||||||
track.release();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
// Now try the larger buffer size
|
// We will try:
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
// 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,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
bytesPerFrame * 2);
|
bytesPerFrame * 2);
|
||||||
|
|
||||||
// Round to next frame
|
// Round to next frame
|
||||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unreachable
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
// Skip low latency options if hardware sample rate isn't 48000Hz
|
||||||
48000,
|
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
||||||
channelConfig,
|
continue;
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
}
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
try {
|
||||||
track.play();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Audio track buffer size: "+bufferSize);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +176,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
public void start() {}
|
public void start() {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {}
|
||||||
// Immediately drop all pending data
|
|
||||||
track.pause();
|
|
||||||
track.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
|
// Immediately drop all pending data
|
||||||
|
track.pause();
|
||||||
|
track.flush();
|
||||||
|
|
||||||
track.release();
|
track.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import java.security.KeyFactory;
|
|||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.NoSuchProviderException;
|
import java.security.Provider;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@@ -54,10 +53,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
|
|
||||||
private static final Object globalCryptoLock = new Object();
|
private static final Object globalCryptoLock = new Object();
|
||||||
|
|
||||||
static {
|
private static final Provider bcProvider = new BouncyCastleProvider();
|
||||||
// Install the Bouncy Castle provider
|
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
|
||||||
}
|
|
||||||
|
|
||||||
public AndroidCryptoProvider(Context c) {
|
public AndroidCryptoProvider(Context c) {
|
||||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||||
@@ -96,10 +92,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
pemCertBytes = certBytes;
|
pemCertBytes = certBytes;
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
|
||||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
// May happen if the cert is corrupt
|
// May happen if the cert is corrupt
|
||||||
@@ -113,10 +109,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
// May happen if the key is corrupt
|
// May happen if the key is corrupt
|
||||||
LimeLog.warning("Corrupted key");
|
LimeLog.warning("Corrupted key");
|
||||||
return false;
|
return false;
|
||||||
} catch (NoSuchProviderException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -129,17 +121,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
|
|
||||||
KeyPair keyPair;
|
KeyPair keyPair;
|
||||||
try {
|
try {
|
||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
||||||
keyPairGenerator.initialize(2048);
|
keyPairGenerator.initialize(2048);
|
||||||
keyPair = keyPairGenerator.generateKeyPair();
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
} catch (NoSuchProviderException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
@@ -160,13 +148,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
||||||
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
||||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Nothing should go wrong here
|
// Nothing should go wrong here
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return false;
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Generated a new key pair");
|
LimeLog.info("Generated a new key pair");
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbManager;
|
||||||
|
import android.media.AudioAttributes;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.os.VibrationEffect;
|
||||||
|
import android.os.Vibrator;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
@@ -11,13 +17,17 @@ import android.view.MotionEvent;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.input.driver.AbstractController;
|
||||||
import com.limelight.binding.input.driver.UsbDriverListener;
|
import com.limelight.binding.input.driver.UsbDriverListener;
|
||||||
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
import com.limelight.utils.Vector2d;
|
import com.limelight.utils.Vector2d;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
@@ -45,20 +55,22 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private final double stickDeadzone;
|
private final double stickDeadzone;
|
||||||
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
||||||
private final GameGestures gestures;
|
private final GameGestures gestures;
|
||||||
|
private final Vibrator deviceVibrator;
|
||||||
private boolean hasGameController;
|
private boolean hasGameController;
|
||||||
|
|
||||||
private final boolean multiControllerEnabled;
|
private final PreferenceConfiguration prefConfig;
|
||||||
private short currentControllers;
|
private short currentControllers, initialControllers;
|
||||||
|
|
||||||
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
|
||||||
this.activityContext = activityContext;
|
this.activityContext = activityContext;
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.gestures = gestures;
|
this.gestures = gestures;
|
||||||
this.multiControllerEnabled = multiControllerEnabled;
|
this.prefConfig = prefConfig;
|
||||||
|
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
|
||||||
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
// HACK: For now we're hardcoding a 7% deadzone. Some deadzone
|
||||||
// is required for controller batching support to work.
|
// is required for controller batching support to work.
|
||||||
deadzonePercentage = 10;
|
int deadzonePercentage = 7;
|
||||||
|
|
||||||
int[] ids = InputDevice.getDeviceIds();
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
@@ -96,6 +108,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
defaultContext.controllerNumber = (short) 0;
|
defaultContext.controllerNumber = (short) 0;
|
||||||
defaultContext.assignedControllerNumber = true;
|
defaultContext.assignedControllerNumber = true;
|
||||||
|
|
||||||
|
// Some devices (GPD XD) have a back button which sends input events
|
||||||
|
// with device ID == 0. This hits the default context which would normally
|
||||||
|
// consume these. Instead, let's ignore them since that's probably the
|
||||||
|
// most likely case.
|
||||||
|
defaultContext.ignoreBack = true;
|
||||||
|
|
||||||
|
// Get the initially attached set of gamepads. As each gamepad receives
|
||||||
|
// its initial InputEvent, we will move these from this set onto the
|
||||||
|
// currentControllers set which will allow them to properly unplug
|
||||||
|
// if they are removed.
|
||||||
|
initialControllers = getAttachedControllerMask(activityContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||||
@@ -133,8 +157,92 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
onInputDeviceAdded(deviceId);
|
onInputDeviceAdded(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||||
|
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
||||||
|
|
||||||
|
if (deviceContext.vibrator != null) {
|
||||||
|
deviceContext.vibrator.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceVibrator.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasJoystickAxes(InputDevice device) {
|
||||||
|
return (device.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
|
||||||
|
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_X) != null &&
|
||||||
|
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_Y) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasGamepadButtons(InputDevice device) {
|
||||||
|
return (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGameControllerDevice(InputDevice device) {
|
||||||
|
if (device == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasJoystickAxes(device) || hasGamepadButtons(device)) {
|
||||||
|
// Has real joystick axes or gamepad buttons
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'll try anything that claims to be a non-alphabetic keyboard
|
||||||
|
return device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static short getAttachedControllerMask(Context context) {
|
||||||
|
int count = 0;
|
||||||
|
short mask = 0;
|
||||||
|
|
||||||
|
// Count all input devices that are gamepads
|
||||||
|
InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
|
||||||
|
for (int id : im.getInputDeviceIds()) {
|
||||||
|
InputDevice dev = im.getInputDevice(id);
|
||||||
|
if (dev == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasJoystickAxes(dev)) {
|
||||||
|
LimeLog.info("Counting InputDevice: "+dev.getName());
|
||||||
|
mask |= 1 << count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count all USB devices that match our drivers
|
||||||
|
if (PreferenceConfiguration.readPreferences(context).usbDriver) {
|
||||||
|
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
||||||
|
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||||
|
// We explicitly ask not to claim devices that appear as InputDevices
|
||||||
|
// otherwise we will double count them.
|
||||||
|
if (UsbDriverService.shouldClaimDevice(dev, false)) {
|
||||||
|
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
|
||||||
|
mask |= 1 << count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PreferenceConfiguration.readPreferences(context).onscreenController) {
|
||||||
|
LimeLog.info("Counting OSC gamepad");
|
||||||
|
mask |= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Enumerated "+count+" gamepads");
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
private void releaseControllerNumber(GenericControllerContext context) {
|
private void releaseControllerNumber(GenericControllerContext context) {
|
||||||
// If this device sent data as a gamepad, zero the values before removing
|
// If we reserved a controller number, remove that reservation
|
||||||
|
if (context.reservedControllerNumber) {
|
||||||
|
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
|
||||||
|
currentControllers &= ~(1 << context.controllerNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this device sent data as a gamepad, zero the values before removing.
|
||||||
|
// We must do this after clearing the currentControllers entry so this
|
||||||
|
// causes the device to be removed on the server PC.
|
||||||
if (context.assignedControllerNumber) {
|
if (context.assignedControllerNumber) {
|
||||||
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
|
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
|
||||||
(short) 0,
|
(short) 0,
|
||||||
@@ -142,12 +250,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
(short) 0, (short) 0,
|
(short) 0, (short) 0,
|
||||||
(short) 0, (short) 0);
|
(short) 0, (short) 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we reserved a controller number, remove that reservation
|
|
||||||
if (context.reservedControllerNumber) {
|
|
||||||
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
|
|
||||||
currentControllers &= ~(1 << context.controllerNumber);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called before sending input but after we've determined that this
|
// Called before sending input but after we've determined that this
|
||||||
@@ -167,7 +269,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
}
|
}
|
||||||
else if (multiControllerEnabled && devContext.hasJoystickAxes) {
|
else if (prefConfig.multiController && devContext.hasJoystickAxes) {
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
|
|
||||||
LimeLog.info("Reserving the next available controller number");
|
LimeLog.info("Reserving the next available controller number");
|
||||||
@@ -175,6 +277,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if ((currentControllers & (1 << i)) == 0) {
|
if ((currentControllers & (1 << i)) == 0) {
|
||||||
// Found an unused controller value
|
// Found an unused controller value
|
||||||
currentControllers |= (1 << i);
|
currentControllers |= (1 << i);
|
||||||
|
|
||||||
|
// Take this value out of the initial gamepad set
|
||||||
|
initialControllers &= ~(1 << i);
|
||||||
|
|
||||||
context.controllerNumber = i;
|
context.controllerNumber = i;
|
||||||
context.reservedControllerNumber = true;
|
context.reservedControllerNumber = true;
|
||||||
break;
|
break;
|
||||||
@@ -187,7 +293,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (multiControllerEnabled) {
|
if (prefConfig.multiController) {
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
|
|
||||||
LimeLog.info("Reserving the next available controller number");
|
LimeLog.info("Reserving the next available controller number");
|
||||||
@@ -195,6 +301,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if ((currentControllers & (1 << i)) == 0) {
|
if ((currentControllers & (1 << i)) == 0) {
|
||||||
// Found an unused controller value
|
// Found an unused controller value
|
||||||
currentControllers |= (1 << i);
|
currentControllers |= (1 << i);
|
||||||
|
|
||||||
|
// Take this value out of the initial gamepad set
|
||||||
|
initialControllers &= ~(1 << i);
|
||||||
|
|
||||||
context.controllerNumber = i;
|
context.controllerNumber = i;
|
||||||
context.reservedControllerNumber = true;
|
context.reservedControllerNumber = true;
|
||||||
break;
|
break;
|
||||||
@@ -211,10 +321,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.assignedControllerNumber = true;
|
context.assignedControllerNumber = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private UsbDeviceContext createUsbDeviceContextForDevice(int deviceId) {
|
private UsbDeviceContext createUsbDeviceContextForDevice(AbstractController device) {
|
||||||
UsbDeviceContext context = new UsbDeviceContext();
|
UsbDeviceContext context = new UsbDeviceContext();
|
||||||
|
|
||||||
context.id = deviceId;
|
context.id = device.getControllerId();
|
||||||
|
context.device = device;
|
||||||
|
|
||||||
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
@@ -223,6 +334,79 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isExternal(InputDevice dev) {
|
||||||
|
try {
|
||||||
|
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
|
||||||
|
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Answer true if we don't know
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldIgnoreBack(InputDevice dev) {
|
||||||
|
String devName = dev.getName();
|
||||||
|
|
||||||
|
// The Serval has a Select button but the framework doesn't
|
||||||
|
// know about that because it uses a non-standard scancode.
|
||||||
|
if (devName.contains("Razer Serval")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classify this device as a remote by name if it has no joystick axes
|
||||||
|
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) == null &&
|
||||||
|
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) == null &&
|
||||||
|
devName.toLowerCase().contains("remote")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, dynamically try to determine whether we should allow this
|
||||||
|
// back button to function for navigation.
|
||||||
|
//
|
||||||
|
// First, check if this is an internal device we're being called on.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !isExternal(dev)) {
|
||||||
|
InputManager im = (InputManager) activityContext.getSystemService(Context.INPUT_SERVICE);
|
||||||
|
|
||||||
|
boolean foundInternalGamepad = false;
|
||||||
|
boolean foundInternalSelect = false;
|
||||||
|
for (int id : im.getInputDeviceIds()) {
|
||||||
|
InputDevice currentDev = im.getInputDevice(id);
|
||||||
|
|
||||||
|
// Ignore external devices
|
||||||
|
if (currentDev == null || isExternal(currentDev)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that we are explicitly NOT excluding the current device we're examining here,
|
||||||
|
// since the other gamepad buttons may be on our current device and that's fine.
|
||||||
|
boolean[] keys = currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_A);
|
||||||
|
if (keys[0]) {
|
||||||
|
foundInternalSelect = true;
|
||||||
|
}
|
||||||
|
if (keys[1]) {
|
||||||
|
foundInternalGamepad = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the back button to function for navigation if we either:
|
||||||
|
// a) have no internal gamepad (most phones)
|
||||||
|
// b) have an internal gamepad but also have an internal select button (GPD XD)
|
||||||
|
// but not:
|
||||||
|
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
|
||||||
|
return !foundInternalGamepad || foundInternalSelect;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
|
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
|
||||||
InputDeviceContext context = new InputDeviceContext();
|
InputDeviceContext context = new InputDeviceContext();
|
||||||
String devName = dev.getName();
|
String devName = dev.getName();
|
||||||
@@ -233,6 +417,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.name = devName;
|
context.name = devName;
|
||||||
context.id = dev.getId();
|
context.id = dev.getId();
|
||||||
|
|
||||||
|
if (dev.getVibrator().hasVibrator()) {
|
||||||
|
context.vibrator = dev.getVibrator();
|
||||||
|
}
|
||||||
|
|
||||||
context.leftStickXAxis = MotionEvent.AXIS_X;
|
context.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
|
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
|
||||||
@@ -355,6 +543,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.ignoreBack = shouldIgnoreBack(dev);
|
||||||
|
|
||||||
if (devName != null) {
|
if (devName != null) {
|
||||||
// For the Nexus Player (and probably other ATV devices), we should
|
// For the Nexus Player (and probably other ATV devices), we should
|
||||||
// use the back button as start since it doesn't have a start/menu button
|
// use the back button as start since it doesn't have a start/menu button
|
||||||
@@ -374,28 +564,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// so we increase the deadzone on them to minimize this
|
// so we increase the deadzone on them to minimize this
|
||||||
context.triggerDeadzone = 0.30f;
|
context.triggerDeadzone = 0.30f;
|
||||||
}
|
}
|
||||||
// Classify this device as a remote by name
|
|
||||||
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
|
|
||||||
// It's only a remote if it doesn't any sticks
|
|
||||||
if (!context.hasJoystickAxes) {
|
|
||||||
context.ignoreBack = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SHIELD controllers will use small stick deadzones
|
// SHIELD controllers will use small stick deadzones
|
||||||
else if (devName.contains("SHIELD")) {
|
else if (devName.contains("SHIELD")) {
|
||||||
context.leftStickDeadzoneRadius = 0.07f;
|
context.leftStickDeadzoneRadius = 0.07f;
|
||||||
context.rightStickDeadzoneRadius = 0.07f;
|
context.rightStickDeadzoneRadius = 0.07f;
|
||||||
}
|
}
|
||||||
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
|
|
||||||
// back presses on this device
|
|
||||||
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey")) {
|
|
||||||
context.ignoreBack = true;
|
|
||||||
}
|
|
||||||
// The Serval has a couple of unknown buttons that are start and select. It also has
|
// The Serval has a couple of unknown buttons that are start and select. It also has
|
||||||
// a back button which we want to ignore since there's already a select button.
|
// a back button which we want to ignore since there's already a select button.
|
||||||
else if (devName.contains("Razer Serval")) {
|
else if (devName.contains("Razer Serval")) {
|
||||||
context.isServal = true;
|
context.isServal = true;
|
||||||
context.ignoreBack = true;
|
|
||||||
}
|
}
|
||||||
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
|
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
|
||||||
// However, Microsoft released a firmware update with no change to VID/PID
|
// However, Microsoft released a firmware update with no change to VID/PID
|
||||||
@@ -464,8 +641,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
private short getActiveControllerMask() {
|
private short getActiveControllerMask() {
|
||||||
if (multiControllerEnabled) {
|
if (prefConfig.multiController) {
|
||||||
return currentControllers;
|
return (short)(currentControllers | initialControllers | (prefConfig.onscreenController ? 1 : 0));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Only Player 1 is active with multi-controller disabled
|
// Only Player 1 is active with multi-controller disabled
|
||||||
@@ -551,6 +728,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ((changedMask & ControllerPacket.UP_FLAG) != 0) {
|
||||||
|
if ((inputMap & ControllerPacket.UP_FLAG) != 0) {
|
||||||
|
conn.sendMouseScroll((byte) 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((changedMask & ControllerPacket.DOWN_FLAG) != 0) {
|
||||||
|
if ((inputMap & ControllerPacket.DOWN_FLAG) != 0) {
|
||||||
|
conn.sendMouseScroll((byte) -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
|
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
|
||||||
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
|
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
|
||||||
@@ -762,16 +949,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// to normal behavior, so ignore triggersIdleNegative for each trigger until
|
// to normal behavior, so ignore triggersIdleNegative for each trigger until
|
||||||
// first touch.
|
// first touch.
|
||||||
if (lt != 0) {
|
if (lt != 0) {
|
||||||
context.leftTriggerUsed = true;
|
context.leftTriggerAxisUsed = true;
|
||||||
}
|
}
|
||||||
if (rt != 0) {
|
if (rt != 0) {
|
||||||
context.rightTriggerUsed = true;
|
context.rightTriggerAxisUsed = true;
|
||||||
}
|
}
|
||||||
if (context.triggersIdleNegative) {
|
if (context.triggersIdleNegative) {
|
||||||
if (context.leftTriggerUsed) {
|
if (context.leftTriggerAxisUsed) {
|
||||||
lt = (lt + 1) / 2;
|
lt = (lt + 1) / 2;
|
||||||
}
|
}
|
||||||
if (context.rightTriggerUsed) {
|
if (context.rightTriggerAxisUsed) {
|
||||||
rt = (rt + 1) / 2;
|
rt = (rt + 1) / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -884,6 +1071,94 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void rumbleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) {
|
||||||
|
// Since we can only use a single amplitude value, compute the desired amplitude
|
||||||
|
// by taking 80% of the big motor and 33% of the small motor, then capping to 255.
|
||||||
|
// NB: This value is now 0-255 as required by VibrationEffect.
|
||||||
|
short lowFreqMotorMSB = (short)((lowFreqMotor >> 8) & 0xFF);
|
||||||
|
short highFreqMotorMSB = (short)((highFreqMotor >> 8) & 0xFF);
|
||||||
|
int simulatedAmplitude = Math.min(255, (int)((lowFreqMotorMSB * 0.80) + (highFreqMotorMSB * 0.33)));
|
||||||
|
|
||||||
|
if (simulatedAmplitude == 0) {
|
||||||
|
// This case is easy - just cancel the current effect and get out.
|
||||||
|
// NB: We cannot simply check lowFreqMotor == highFreqMotor == 0
|
||||||
|
// because our simulatedAmplitude could be 0 even though our inputs
|
||||||
|
// are not (ex: lowFreqMotor == 0 && highFreqMotor == 1).
|
||||||
|
vibrator.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to use amplitude-based control if we're on Oreo and the device
|
||||||
|
// supports amplitude-based vibration control.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
if (vibrator.hasAmplitudeControl()) {
|
||||||
|
VibrationEffect effect = VibrationEffect.createOneShot(60000, simulatedAmplitude);
|
||||||
|
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_GAME)
|
||||||
|
.build();
|
||||||
|
vibrator.vibrate(effect, audioAttributes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach this point, we don't have amplitude controls available, so
|
||||||
|
// we must emulate it by PWMing the vibration. Ick.
|
||||||
|
long pwmPeriod = 20;
|
||||||
|
long onTime = (long)((simulatedAmplitude / 255.0) * pwmPeriod);
|
||||||
|
long offTime = pwmPeriod - onTime;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_GAME)
|
||||||
|
.build();
|
||||||
|
vibrator.vibrate(new long[]{0, onTime, offTime}, 0, audioAttributes);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
vibrator.vibrate(new long[]{0, onTime, offTime}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleRumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
|
||||||
|
boolean foundMatchingDevice = false;
|
||||||
|
boolean vibrated = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||||
|
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
||||||
|
|
||||||
|
if (deviceContext.controllerNumber == controllerNumber) {
|
||||||
|
foundMatchingDevice = true;
|
||||||
|
|
||||||
|
if (deviceContext.vibrator != null) {
|
||||||
|
vibrated = true;
|
||||||
|
rumbleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < usbDeviceContexts.size(); i++) {
|
||||||
|
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
|
||||||
|
|
||||||
|
if (deviceContext.controllerNumber == controllerNumber) {
|
||||||
|
foundMatchingDevice = vibrated = true;
|
||||||
|
deviceContext.device.rumble((short)lowFreqMotor, (short)highFreqMotor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may decide to rumble the device for player 1
|
||||||
|
if (controllerNumber == 0) {
|
||||||
|
// If we didn't find a matching device, it must be the on-screen
|
||||||
|
// controls that triggered the rumble. Vibrate the device if
|
||||||
|
// the user has requested that behavior.
|
||||||
|
if (!foundMatchingDevice && prefConfig.onscreenController && !prefConfig.onlyL3R3 && prefConfig.vibrateOsc) {
|
||||||
|
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
||||||
|
}
|
||||||
|
else if (foundMatchingDevice && !vibrated && prefConfig.vibrateFallbackToDevice) {
|
||||||
|
// We found a device to vibrate but it didn't have rumble support. The user
|
||||||
|
// has requested us to vibrate the device in this case.
|
||||||
|
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean handleButtonUp(KeyEvent event) {
|
public boolean handleButtonUp(KeyEvent event) {
|
||||||
InputDeviceContext context = getContextForEvent(event);
|
InputDeviceContext context = getContextForEvent(event);
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
@@ -917,7 +1192,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// Make sure it's real by checking that the key is actually down before taking
|
// Make sure it's real by checking that the key is actually down before taking
|
||||||
// any action.
|
// any action.
|
||||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||||
SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS) {
|
SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS &&
|
||||||
|
prefConfig.mouseEmulation) {
|
||||||
toggleMouseEmulation(context);
|
toggleMouseEmulation(context);
|
||||||
}
|
}
|
||||||
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||||
@@ -966,9 +1242,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
|
if (context.leftTriggerAxisUsed) {
|
||||||
|
// Suppress this digital event if an analog trigger is active
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.leftTrigger = 0;
|
context.leftTrigger = 0;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
|
if (context.rightTriggerAxisUsed) {
|
||||||
|
// Suppress this digital event if an analog trigger is active
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.rightTrigger = 0;
|
context.rightTrigger = 0;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -1078,9 +1362,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
|
if (context.leftTriggerAxisUsed) {
|
||||||
|
// Suppress this digital event if an analog trigger is active
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.leftTrigger = (byte)0xFF;
|
context.leftTrigger = (byte)0xFF;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
|
if (context.rightTriggerAxisUsed) {
|
||||||
|
// Suppress this digital event if an analog trigger is active
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.rightTrigger = (byte)0xFF;
|
context.rightTrigger = (byte)0xFF;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -1118,12 +1410,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reportOscState(short buttonFlags,
|
||||||
|
short leftStickX, short leftStickY,
|
||||||
|
short rightStickX, short rightStickY,
|
||||||
|
byte leftTrigger, byte rightTrigger) {
|
||||||
|
defaultContext.leftStickX = leftStickX;
|
||||||
|
defaultContext.leftStickY = leftStickY;
|
||||||
|
|
||||||
|
defaultContext.rightStickX = rightStickX;
|
||||||
|
defaultContext.rightStickY = rightStickY;
|
||||||
|
|
||||||
|
defaultContext.leftTrigger = leftTrigger;
|
||||||
|
defaultContext.rightTrigger = rightTrigger;
|
||||||
|
|
||||||
|
defaultContext.inputMap = buttonFlags;
|
||||||
|
|
||||||
|
sendControllerInputPacket(defaultContext);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reportControllerState(int controllerId, short buttonFlags,
|
public void reportControllerState(int controllerId, short buttonFlags,
|
||||||
float leftStickX, float leftStickY,
|
float leftStickX, float leftStickY,
|
||||||
float rightStickX, float rightStickY,
|
float rightStickX, float rightStickY,
|
||||||
float leftTrigger, float rightTrigger) {
|
float leftTrigger, float rightTrigger) {
|
||||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
GenericControllerContext context = usbDeviceContexts.get(controllerId);
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1158,19 +1468,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deviceRemoved(int controllerId) {
|
public void deviceRemoved(AbstractController controller) {
|
||||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
UsbDeviceContext context = usbDeviceContexts.get(controller.getControllerId());
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
LimeLog.info("Removed controller: "+controllerId);
|
LimeLog.info("Removed controller: "+controller.getControllerId());
|
||||||
releaseControllerNumber(context);
|
releaseControllerNumber(context);
|
||||||
usbDeviceContexts.remove(controllerId);
|
usbDeviceContexts.remove(controller.getControllerId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deviceAdded(int controllerId) {
|
public void deviceAdded(AbstractController controller) {
|
||||||
UsbDeviceContext context = createUsbDeviceContextForDevice(controllerId);
|
UsbDeviceContext context = createUsbDeviceContextForDevice(controller);
|
||||||
usbDeviceContexts.put(controllerId, context);
|
usbDeviceContexts.put(controller.getControllerId(), context);
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenericControllerContext {
|
class GenericControllerContext {
|
||||||
@@ -1199,6 +1509,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
class InputDeviceContext extends GenericControllerContext {
|
class InputDeviceContext extends GenericControllerContext {
|
||||||
public String name;
|
public String name;
|
||||||
|
public Vibrator vibrator;
|
||||||
|
|
||||||
public int leftStickXAxis = -1;
|
public int leftStickXAxis = -1;
|
||||||
public int leftStickYAxis = -1;
|
public int leftStickYAxis = -1;
|
||||||
@@ -1209,7 +1520,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public int leftTriggerAxis = -1;
|
public int leftTriggerAxis = -1;
|
||||||
public int rightTriggerAxis = -1;
|
public int rightTriggerAxis = -1;
|
||||||
public boolean triggersIdleNegative;
|
public boolean triggersIdleNegative;
|
||||||
public boolean leftTriggerUsed, rightTriggerUsed;
|
public boolean leftTriggerAxisUsed, rightTriggerAxisUsed;
|
||||||
|
|
||||||
public int hatXAxis = -1;
|
public int hatXAxis = -1;
|
||||||
public int hatYAxis = -1;
|
public int hatYAxis = -1;
|
||||||
@@ -1236,5 +1547,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public long startDownTime = 0;
|
public long startDownTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UsbDeviceContext extends GenericControllerContext {}
|
class UsbDeviceContext extends GenericControllerContext {
|
||||||
|
public AbstractController device;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to translate a Android key code into the codes GFE is expecting
|
* Class to translate a Android key code into the codes GFE is expecting
|
||||||
* @author Diego Waxemberg
|
* @author Diego Waxemberg
|
||||||
@@ -20,22 +18,15 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_9 = 57;
|
public static final int VK_9 = 57;
|
||||||
public static final int VK_A = 65;
|
public static final int VK_A = 65;
|
||||||
public static final int VK_Z = 90;
|
public static final int VK_Z = 90;
|
||||||
public static final int VK_ALT = 18;
|
|
||||||
public static final int VK_NUMPAD0 = 96;
|
public static final int VK_NUMPAD0 = 96;
|
||||||
public static final int VK_BACK_SLASH = 92;
|
public static final int VK_BACK_SLASH = 92;
|
||||||
public static final int VK_CAPS_LOCK = 20;
|
public static final int VK_CAPS_LOCK = 20;
|
||||||
public static final int VK_CLEAR = 12;
|
public static final int VK_CLEAR = 12;
|
||||||
public static final int VK_COMMA = 44;
|
public static final int VK_COMMA = 44;
|
||||||
public static final int VK_CONTROL = 17;
|
|
||||||
public static final int VK_BACK_SPACE = 8;
|
public static final int VK_BACK_SPACE = 8;
|
||||||
public static final int VK_EQUALS = 61;
|
public static final int VK_EQUALS = 61;
|
||||||
public static final int VK_ESCAPE = 27;
|
public static final int VK_ESCAPE = 27;
|
||||||
public static final int VK_F1 = 112;
|
public static final int VK_F1 = 112;
|
||||||
public static final int VK_PERIOD = 46;
|
|
||||||
public static final int VK_INSERT = 155;
|
|
||||||
public static final int VK_OPEN_BRACKET = 91;
|
|
||||||
public static final int VK_WINDOWS = 524;
|
|
||||||
public static final int VK_MINUS = 45;
|
|
||||||
public static final int VK_END = 35;
|
public static final int VK_END = 35;
|
||||||
public static final int VK_HOME = 36;
|
public static final int VK_HOME = 36;
|
||||||
public static final int VK_NUM_LOCK = 144;
|
public static final int VK_NUM_LOCK = 144;
|
||||||
@@ -45,7 +36,6 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_CLOSE_BRACKET = 93;
|
public static final int VK_CLOSE_BRACKET = 93;
|
||||||
public static final int VK_SCROLL_LOCK = 145;
|
public static final int VK_SCROLL_LOCK = 145;
|
||||||
public static final int VK_SEMICOLON = 59;
|
public static final int VK_SEMICOLON = 59;
|
||||||
public static final int VK_SHIFT = 16;
|
|
||||||
public static final int VK_SLASH = 47;
|
public static final int VK_SLASH = 47;
|
||||||
public static final int VK_SPACE = 32;
|
public static final int VK_SPACE = 32;
|
||||||
public static final int VK_PRINTSCREEN = 154;
|
public static final int VK_PRINTSCREEN = 154;
|
||||||
@@ -57,7 +47,21 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_BACK_QUOTE = 192;
|
public static final int VK_BACK_QUOTE = 192;
|
||||||
public static final int VK_QUOTE = 222;
|
public static final int VK_QUOTE = 222;
|
||||||
public static final int VK_PAUSE = 19;
|
public static final int VK_PAUSE = 19;
|
||||||
|
|
||||||
|
public static boolean needsShift(int keycode) {
|
||||||
|
switch (keycode)
|
||||||
|
{
|
||||||
|
case KeyEvent.KEYCODE_AT:
|
||||||
|
case KeyEvent.KEYCODE_POUND:
|
||||||
|
case KeyEvent.KEYCODE_PLUS:
|
||||||
|
case KeyEvent.KEYCODE_STAR:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates the given keycode and returns the GFE keycode
|
* Translates the given keycode and returns the GFE keycode
|
||||||
* @param keycode the code to be translated
|
* @param keycode the code to be translated
|
||||||
@@ -66,10 +70,9 @@ public class KeyboardTranslator {
|
|||||||
public static short translate(int keycode) {
|
public static short translate(int keycode) {
|
||||||
int translated;
|
int translated;
|
||||||
|
|
||||||
/* There seems to be no clean mapping between Android key codes
|
// This is a poor man's mapping between Android key codes
|
||||||
* and what Nvidia sends over the wire. If someone finds one,
|
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||||
* I'll happily delete this code :)
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||||
*/
|
|
||||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||||
keycode <= KeyEvent.KEYCODE_9) {
|
keycode <= KeyEvent.KEYCODE_9) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||||
@@ -89,8 +92,11 @@ public class KeyboardTranslator {
|
|||||||
else {
|
else {
|
||||||
switch (keycode) {
|
switch (keycode) {
|
||||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||||
|
translated = 0xA4;
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||||
translated = VK_ALT;
|
translated = 0xA5;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_BACKSLASH:
|
case KeyEvent.KEYCODE_BACKSLASH:
|
||||||
@@ -110,8 +116,11 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||||
|
translated = 0xA2;
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||||
translated = VK_CONTROL;
|
translated = 0xA3;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DEL:
|
case KeyEvent.KEYCODE_DEL:
|
||||||
@@ -121,7 +130,8 @@ public class KeyboardTranslator {
|
|||||||
case KeyEvent.KEYCODE_ENTER:
|
case KeyEvent.KEYCODE_ENTER:
|
||||||
translated = 0x0d;
|
translated = 0x0d;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_PLUS:
|
||||||
case KeyEvent.KEYCODE_EQUALS:
|
case KeyEvent.KEYCODE_EQUALS:
|
||||||
translated = 0xbb;
|
translated = 0xbb;
|
||||||
break;
|
break;
|
||||||
@@ -131,23 +141,25 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||||
// Nvidia maps period to delete
|
translated = 0x2e;
|
||||||
translated = VK_PERIOD;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_INSERT:
|
case KeyEvent.KEYCODE_INSERT:
|
||||||
translated = -1;
|
translated = 0x2d;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||||
translated = 0xdb;
|
translated = 0xdb;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_META_LEFT:
|
case KeyEvent.KEYCODE_META_LEFT:
|
||||||
case KeyEvent.KEYCODE_META_RIGHT:
|
translated = 0x5b;
|
||||||
translated = VK_WINDOWS;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_META_RIGHT:
|
||||||
|
translated = 0x5c;
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_MINUS:
|
case KeyEvent.KEYCODE_MINUS:
|
||||||
translated = 0xbd;
|
translated = 0xbd;
|
||||||
break;
|
break;
|
||||||
@@ -189,8 +201,11 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||||
|
translated = 0xA0;
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||||
translated = VK_SHIFT;
|
translated = 0xA1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SLASH:
|
case KeyEvent.KEYCODE_SLASH:
|
||||||
@@ -237,7 +252,39 @@ public class KeyboardTranslator {
|
|||||||
case KeyEvent.KEYCODE_BREAK:
|
case KeyEvent.KEYCODE_BREAK:
|
||||||
translated = VK_PAUSE;
|
translated = VK_PAUSE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||||
|
translated = 0x6F;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||||
|
translated = 0x6A;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||||
|
translated = 0x6D;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||||
|
translated = 0x6B;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||||
|
translated = 0x6E;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_AT:
|
||||||
|
translated = 2 + VK_0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_POUND:
|
||||||
|
translated = 3 + VK_0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_STAR:
|
||||||
|
translated = 8 + VK_0;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
System.out.println("No key for "+keycode);
|
System.out.println("No key for "+keycode);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -43,11 +43,19 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
return event.getX();
|
float x = event.getX();
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
x += event.getHistoricalX(i);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
return event.getY();
|
float y = event.getY();
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
y += event.getHistoricalY(i);
|
||||||
|
}
|
||||||
|
return y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package com.limelight.binding.input.capture;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
|
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
|
||||||
public class InputCaptureManager {
|
public class InputCaptureManager {
|
||||||
@@ -13,13 +14,15 @@ public class InputCaptureManager {
|
|||||||
LimeLog.info("Using Android O+ native mouse capture");
|
LimeLog.info("Using Android O+ native mouse capture");
|
||||||
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
||||||
}
|
}
|
||||||
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
// 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");
|
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||||
return new ShieldCaptureProvider(activity);
|
return new ShieldCaptureProvider(activity);
|
||||||
}
|
}
|
||||||
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
|
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
|
||||||
LimeLog.info("Using Evdev mouse capture");
|
LimeLog.info("Using Evdev mouse capture");
|
||||||
return new EvdevCaptureProvider(activity, rootListener);
|
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
|
||||||
}
|
}
|
||||||
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
||||||
// Android N's native capture can't capture over system UI elements
|
// Android N's native capture can't capture over system UI elements
|
||||||
|
|||||||
@@ -37,11 +37,13 @@ public abstract class AbstractController {
|
|||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
||||||
|
|
||||||
protected void notifyDeviceRemoved() {
|
protected void notifyDeviceRemoved() {
|
||||||
listener.deviceRemoved(deviceId);
|
listener.deviceRemoved(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyDeviceAdded() {
|
protected void notifyDeviceAdded() {
|
||||||
listener.deviceAdded(deviceId);
|
listener.deviceAdded(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ public abstract class AbstractXboxController extends AbstractController {
|
|||||||
|
|
||||||
stopped = true;
|
stopped = true;
|
||||||
|
|
||||||
|
// Cancel any rumble effects
|
||||||
|
rumble((short)0, (short)0);
|
||||||
|
|
||||||
// Stop the input thread
|
// Stop the input thread
|
||||||
if (inputThread != null) {
|
if (inputThread != null) {
|
||||||
inputThread.interrupt();
|
inputThread.interrupt();
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ public interface UsbDriverListener {
|
|||||||
float rightStickX, float rightStickY,
|
float rightStickX, float rightStickY,
|
||||||
float leftTrigger, float rightTrigger);
|
float leftTrigger, float rightTrigger);
|
||||||
|
|
||||||
void deviceRemoved(int controllerId);
|
void deviceRemoved(AbstractController controller);
|
||||||
void deviceAdded(int controllerId);
|
void deviceAdded(AbstractController controller);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ import android.hardware.usb.UsbDeviceConnection;
|
|||||||
import android.hardware.usb.UsbManager;
|
import android.hardware.usb.UsbManager;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@@ -24,6 +28,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
"com.limelight.USB_PERMISSION";
|
"com.limelight.USB_PERMISSION";
|
||||||
|
|
||||||
private UsbManager usbManager;
|
private UsbManager usbManager;
|
||||||
|
private PreferenceConfiguration prefConfig;
|
||||||
|
|
||||||
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
||||||
private final UsbDriverBinder binder = new UsbDriverBinder();
|
private final UsbDriverBinder binder = new UsbDriverBinder();
|
||||||
@@ -42,26 +47,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deviceRemoved(int controllerId) {
|
public void deviceRemoved(AbstractController controller) {
|
||||||
// Remove the the controller from our list (if not removed already)
|
// Remove the the controller from our list (if not removed already)
|
||||||
for (AbstractController controller : controllers) {
|
controllers.remove(controller);
|
||||||
if (controller.getControllerId() == controllerId) {
|
|
||||||
controllers.remove(controller);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call through to the client's listener
|
// Call through to the client's listener
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.deviceRemoved(controllerId);
|
listener.deviceRemoved(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deviceAdded(int controllerId) {
|
public void deviceAdded(AbstractController controller) {
|
||||||
// Call through to the client's listener
|
// Call through to the client's listener
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.deviceAdded(controllerId);
|
listener.deviceAdded(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,10 +72,22 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Initial attachment broadcast
|
// Initial attachment broadcast
|
||||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||||
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
// Continue the state machine
|
// shouldClaimDevice() looks at the kernel's enumerated input
|
||||||
handleUsbDeviceState(device);
|
// 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
|
// Subsequent permission dialog completion intent
|
||||||
else if (action.equals(ACTION_USB_PERMISSION)) {
|
else if (action.equals(ACTION_USB_PERMISSION)) {
|
||||||
@@ -96,7 +108,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
// Report all controllerMap that already exist
|
// Report all controllerMap that already exist
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
for (AbstractController controller : controllers) {
|
for (AbstractController controller : controllers) {
|
||||||
listener.deviceAdded(controller.getControllerId());
|
listener.deviceAdded(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,11 +116,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
private void handleUsbDeviceState(UsbDevice device) {
|
private void handleUsbDeviceState(UsbDevice device) {
|
||||||
// Are we able to operate it?
|
// Are we able to operate it?
|
||||||
if (shouldClaimDevice(device)) {
|
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
|
||||||
// Do we have permission yet?
|
// Do we have permission yet?
|
||||||
if (!usbManager.hasPermission(device)) {
|
if (!usbManager.hasPermission(device)) {
|
||||||
// Let's ask for permission
|
// Let's ask for permission
|
||||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,13 +166,17 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRecognizedInputDevice(UsbDevice device) {
|
private static boolean isRecognizedInputDevice(UsbDevice device) {
|
||||||
// On KitKat and later, we can determine if this VID and PID combo
|
// 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
|
// 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.
|
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
for (int id : InputDevice.getDeviceIds()) {
|
for (int id : InputDevice.getDeviceIds()) {
|
||||||
InputDevice inputDev = InputDevice.getDevice(id);
|
InputDevice inputDev = InputDevice.getDevice(id);
|
||||||
|
if (inputDev == null) {
|
||||||
|
// Device was removed while looping
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (inputDev.getVendorId() == device.getVendorId() &&
|
if (inputDev.getVendorId() == device.getVendorId() &&
|
||||||
inputDev.getProductId() == device.getProductId()) {
|
inputDev.getProductId() == device.getProductId()) {
|
||||||
@@ -165,16 +191,17 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldClaimDevice(UsbDevice device) {
|
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
||||||
// We always bind to XB1 controllers but only bind to XB360 controllers
|
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||||
// if we know the kernel isn't already driving this device.
|
// if we know the kernel isn't already driving this device.
|
||||||
return XboxOneController.canClaimDevice(device) ||
|
return XboxOneController.canClaimDevice(device) ||
|
||||||
(!isRecognizedInputDevice(device) && Xbox360Controller.canClaimDevice(device));
|
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||||
|
this.prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
|
|
||||||
// Register for USB attach broadcasts and permission completions
|
// Register for USB attach broadcasts and permission completions
|
||||||
IntentFilter filter = new IntentFilter();
|
IntentFilter filter = new IntentFilter();
|
||||||
@@ -184,7 +211,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Enumerate existing devices
|
// Enumerate existing devices
|
||||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||||
if (shouldClaimDevice(dev)) {
|
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
|
||||||
// Start the process of claiming this device
|
// Start the process of claiming this device
|
||||||
handleUsbDeviceState(dev);
|
handleUsbDeviceState(dev);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,21 +14,27 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||||
|
|
||||||
private static final int[] SUPPORTED_VENDORS = {
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x0079, // GPD Win 2
|
||||||
0x044f, // Thrustmaster
|
0x044f, // Thrustmaster
|
||||||
0x045e, // Microsoft
|
0x045e, // Microsoft
|
||||||
0x046d, // Logitech
|
0x046d, // Logitech
|
||||||
|
0x056e, // Elecom
|
||||||
|
0x06a3, // Saitek
|
||||||
0x0738, // Mad Catz
|
0x0738, // Mad Catz
|
||||||
|
0x07ff, // Mad Catz
|
||||||
0x0e6f, // Unknown
|
0x0e6f, // Unknown
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1038, // SteelSeries
|
||||||
|
0x11c9, // Nacon
|
||||||
0x12ab, // Unknown
|
0x12ab, // Unknown
|
||||||
0x1430, // RedOctane
|
0x1430, // RedOctane
|
||||||
0x146b, // BigBen
|
0x146b, // BigBen
|
||||||
0x1bad, // Harmonix
|
|
||||||
0x0f0d, // Hori
|
|
||||||
0x1689, // Razer Onza
|
|
||||||
0x24c6, // PowerA
|
|
||||||
0x1532, // Razer Sabertooth
|
0x1532, // Razer Sabertooth
|
||||||
0x15e4, // Numark
|
0x15e4, // Numark
|
||||||
0x162e, // Joytech
|
0x162e, // Joytech
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x24c6, // PowerA
|
||||||
};
|
};
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
@@ -133,4 +139,17 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
// No need to fail init if the LED command fails
|
// No need to fail init if the LED command fails
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
|
|||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class XboxOneController extends AbstractXboxController {
|
public class XboxOneController extends AbstractXboxController {
|
||||||
|
|
||||||
@@ -19,11 +20,35 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
0x0738, // Mad Catz
|
0x0738, // Mad Catz
|
||||||
0x0e6f, // Unknown
|
0x0e6f, // Unknown
|
||||||
0x0f0d, // Hori
|
0x0f0d, // Hori
|
||||||
|
0x1532, // Razer Wildcat
|
||||||
0x24c6, // PowerA
|
0x24c6, // PowerA
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: odata_serial
|
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||||
private static final byte[] XB1_INIT_DATA = {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) {
|
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
super(device, connection, deviceId, listener);
|
super(device, connection, deviceId, listener);
|
||||||
@@ -110,13 +135,55 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doInit() {
|
protected boolean doInit() {
|
||||||
// Send the initialization packet
|
// Send all applicable init packets
|
||||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
for (InitPacket pkt : INIT_PKTS) {
|
||||||
if (res != XB1_INIT_DATA.length) {
|
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
|
||||||
LimeLog.warning("Initialization transfer failed: "+res);
|
continue;
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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,12 +1,14 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
public interface EvdevListener {
|
public interface EvdevListener {
|
||||||
public static final int BUTTON_LEFT = 1;
|
int BUTTON_LEFT = 1;
|
||||||
public static final int BUTTON_MIDDLE = 2;
|
int BUTTON_MIDDLE = 2;
|
||||||
public static final int BUTTON_RIGHT = 3;
|
int BUTTON_RIGHT = 3;
|
||||||
|
int BUTTON_X1 = 4;
|
||||||
|
int BUTTON_X2 = 5;
|
||||||
|
|
||||||
public void mouseMove(int deltaX, int deltaY);
|
void mouseMove(int deltaX, int deltaY);
|
||||||
public void mouseButtonEvent(int buttonId, boolean down);
|
void mouseButtonEvent(int buttonId, boolean down);
|
||||||
public void mouseScroll(byte amount);
|
void mouseScroll(byte amount);
|
||||||
public void keyboardEvent(boolean buttonDown, short keyCode);
|
void keyboardEvent(boolean buttonDown, short keyCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public AnalogStick(VirtualController controller, Context context) {
|
public AnalogStick(VirtualController controller, Context context, int elementId) {
|
||||||
super(controller, context);
|
super(controller, context, elementId);
|
||||||
// reset stick position
|
// reset stick position
|
||||||
position_stick_x = getWidth() / 2;
|
position_stick_x = getWidth() / 2;
|
||||||
position_stick_y = getHeight() / 2;
|
position_stick_y = getHeight() / 2;
|
||||||
@@ -210,7 +210,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
// calculate new radius sizes depending
|
// calculate new radius sizes depending
|
||||||
radius_complete = getPercent(getCorrectWidth() / 2, 90);
|
radius_complete = getPercent(getCorrectWidth() / 2, 100);
|
||||||
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||||
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||||
|
|
||||||
@@ -338,6 +338,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
} else {
|
} else {
|
||||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||||
notifyOnRevoke();
|
notifyOnRevoke();
|
||||||
|
|
||||||
// not longer pressed reset analog stick
|
// not longer pressed reset analog stick
|
||||||
notifyOnMovement(0, 0);
|
notifyOnMovement(0, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ public class DigitalButton extends VirtualControllerElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DigitalButton(VirtualController controller, int layer, Context context) {
|
public DigitalButton(VirtualController controller, int elementId, int layer, Context context) {
|
||||||
super(controller, context);
|
super(controller, context, elementId);
|
||||||
this.layer = layer;
|
this.layer = layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class DigitalPad extends VirtualControllerElement {
|
|||||||
private final Paint paint = new Paint();
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
public DigitalPad(VirtualController controller, Context context) {
|
public DigitalPad(VirtualController controller, Context context) {
|
||||||
super(controller, context);
|
super(controller, context, EID_DPAD);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addDigitalPadListener(DigitalPadListener listener) {
|
public void addDigitalPadListener(DigitalPadListener listener) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.limelight.nvstream.input.ControllerPacket;
|
|||||||
|
|
||||||
public class LeftAnalogStick extends AnalogStick {
|
public class LeftAnalogStick extends AnalogStick {
|
||||||
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
||||||
super(controller, context);
|
super(controller, context, EID_LS);
|
||||||
|
|
||||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import android.content.Context;
|
|||||||
|
|
||||||
public class LeftTrigger extends DigitalButton {
|
public class LeftTrigger extends DigitalButton {
|
||||||
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
super(controller, layer, context);
|
super(controller, EID_LT, layer, context);
|
||||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.limelight.nvstream.input.ControllerPacket;
|
|||||||
|
|
||||||
public class RightAnalogStick extends AnalogStick {
|
public class RightAnalogStick extends AnalogStick {
|
||||||
public RightAnalogStick(final VirtualController controller, final Context context) {
|
public RightAnalogStick(final VirtualController controller, final Context context) {
|
||||||
super(controller, context);
|
super(controller, context, EID_RS);
|
||||||
|
|
||||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import android.content.Context;
|
|||||||
|
|
||||||
public class RightTrigger extends DigitalButton {
|
public class RightTrigger extends DigitalButton {
|
||||||
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
super(controller, layer, context);
|
super(controller, EID_RT, layer, context);
|
||||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import android.widget.RelativeLayout;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class VirtualController {
|
public class VirtualController {
|
||||||
public class ControllerInputContext {
|
public class ControllerInputContext {
|
||||||
@@ -36,22 +39,23 @@ public class VirtualController {
|
|||||||
|
|
||||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
private NvConnection connection = null;
|
private ControllerHandler controllerHandler;
|
||||||
private Context context = null;
|
private Context context = null;
|
||||||
|
|
||||||
private FrameLayout frame_layout = null;
|
private FrameLayout frame_layout = null;
|
||||||
private RelativeLayout relative_layout = null;
|
private RelativeLayout relative_layout = null;
|
||||||
|
|
||||||
|
private Timer retransmitTimer;
|
||||||
|
|
||||||
ControllerMode currentMode = ControllerMode.Active;
|
ControllerMode currentMode = ControllerMode.Active;
|
||||||
ControllerInputContext inputContext = new ControllerInputContext();
|
ControllerInputContext inputContext = new ControllerInputContext();
|
||||||
|
|
||||||
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
|
||||||
private Button buttonConfigure = null;
|
private Button buttonConfigure = null;
|
||||||
|
|
||||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||||
|
|
||||||
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
|
||||||
this.connection = conn;
|
this.controllerHandler = controllerHandler;
|
||||||
this.frame_layout = layout;
|
this.frame_layout = layout;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
@@ -60,6 +64,8 @@ public class VirtualController {
|
|||||||
frame_layout.addView(relative_layout);
|
frame_layout.addView(relative_layout);
|
||||||
|
|
||||||
buttonConfigure = new Button(context);
|
buttonConfigure = new Button(context);
|
||||||
|
buttonConfigure.setAlpha(0.25f);
|
||||||
|
buttonConfigure.setFocusable(false);
|
||||||
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -68,6 +74,7 @@ public class VirtualController {
|
|||||||
|
|
||||||
if (currentMode == ControllerMode.Configuration) {
|
if (currentMode == ControllerMode.Configuration) {
|
||||||
currentMode = ControllerMode.Active;
|
currentMode = ControllerMode.Active;
|
||||||
|
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
||||||
message = "Exiting configuration mode";
|
message = "Exiting configuration mode";
|
||||||
} else {
|
} else {
|
||||||
currentMode = ControllerMode.Configuration;
|
currentMode = ControllerMode.Configuration;
|
||||||
@@ -85,6 +92,27 @@ public class VirtualController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
public void removeElements() {
|
||||||
for (VirtualControllerElement element : elements) {
|
for (VirtualControllerElement element : elements) {
|
||||||
relative_layout.removeView(element);
|
relative_layout.removeView(element);
|
||||||
@@ -116,11 +144,19 @@ public class VirtualController {
|
|||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
int buttonSize = (int)(screen.heightPixels*0.05f);
|
int buttonSize = (int)(screen.heightPixels*0.06f);
|
||||||
layoutParamsButtonConfigure = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
||||||
relative_layout.addView(buttonConfigure, layoutParamsButtonConfigure);
|
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);
|
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
||||||
|
|
||||||
|
// Apply user preferences onto the default layout
|
||||||
|
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ControllerMode getControllerMode() {
|
public ControllerMode getControllerMode() {
|
||||||
@@ -131,32 +167,23 @@ public class VirtualController {
|
|||||||
return inputContext;
|
return inputContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendControllerInputContext() {
|
void sendControllerInputContext() {
|
||||||
sendControllerInputPacket();
|
_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);
|
||||||
|
|
||||||
private void sendControllerInputPacket() {
|
if (controllerHandler != null) {
|
||||||
try {
|
controllerHandler.reportOscState(
|
||||||
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
inputContext.inputMap,
|
||||||
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
inputContext.leftStickX,
|
||||||
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
inputContext.leftStickY,
|
||||||
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
inputContext.rightStickX,
|
||||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
inputContext.rightStickY,
|
||||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
inputContext.leftTrigger,
|
||||||
|
inputContext.rightTrigger
|
||||||
if (connection != null) {
|
);
|
||||||
connection.sendControllerInput(
|
|
||||||
inputContext.inputMap,
|
|
||||||
inputContext.leftTrigger,
|
|
||||||
inputContext.rightTrigger,
|
|
||||||
inputContext.leftStickX,
|
|
||||||
inputContext.leftStickY,
|
|
||||||
inputContext.rightStickX,
|
|
||||||
inputContext.rightStickY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,19 @@
|
|||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class VirtualControllerConfigurationLoader {
|
public class VirtualControllerConfigurationLoader {
|
||||||
private static final String PROFILE_PATH = "profiles";
|
public static final String OSC_PREFERENCE = "OSC";
|
||||||
|
|
||||||
private static int getPercent(
|
private static int getPercent(
|
||||||
int percent,
|
int percent,
|
||||||
@@ -58,6 +64,7 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static DigitalButton createDigitalButton(
|
private static DigitalButton createDigitalButton(
|
||||||
|
final int elementId,
|
||||||
final int keyShort,
|
final int keyShort,
|
||||||
final int keyLong,
|
final int keyLong,
|
||||||
final int layer,
|
final int layer,
|
||||||
@@ -65,7 +72,7 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
final int icon,
|
final int icon,
|
||||||
final VirtualController controller,
|
final VirtualController controller,
|
||||||
final Context context) {
|
final Context context) {
|
||||||
DigitalButton button = new DigitalButton(controller, layer, context);
|
DigitalButton button = new DigitalButton(controller, elementId, layer, context);
|
||||||
button.setText(text);
|
button.setText(text);
|
||||||
button.setIcon(icon);
|
button.setIcon(icon);
|
||||||
|
|
||||||
@@ -146,140 +153,176 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
||||||
|
|
||||||
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
// 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.
|
// into a single call. Due to floating point rounding, this isn't actually possible.
|
||||||
|
|
||||||
controller.addElement(createDigitalPad(controller, context),
|
if (!config.onlyL3R3)
|
||||||
getPercent(5, screen.widthPixels),
|
{
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
controller.addElement(createDigitalPad(controller, context),
|
||||||
getPercent(30, screen.widthPixels),
|
getPercent(5, screen.widthPixels),
|
||||||
getPercent(40, screen.heightPixels)
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
);
|
getPercent(30, screen.widthPixels),
|
||||||
|
getPercent(40, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
VirtualControllerElement.EID_A,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
);
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
VirtualControllerElement.EID_B,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
);
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
VirtualControllerElement.EID_X,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
);
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
VirtualControllerElement.EID_Y,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
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(createLeftTrigger(
|
controller.addElement(createLeftTrigger(
|
||||||
0, "LT", -1, controller, context),
|
0, "LT", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createRightTrigger(
|
controller.addElement(createRightTrigger(
|
||||||
0, "RT", -1, controller, context),
|
0, "RT", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
VirtualControllerElement.EID_LB,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
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_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
);
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
VirtualControllerElement.EID_RB,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
);
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
controller.addElement(createLeftStick(controller, context),
|
controller.addElement(createLeftStick(controller, context),
|
||||||
getPercent(5, screen.widthPixels),
|
getPercent(5, screen.widthPixels),
|
||||||
getPercent(50, screen.heightPixels),
|
getPercent(50, screen.heightPixels),
|
||||||
getPercent(40, screen.widthPixels),
|
getPercent(40, screen.widthPixels),
|
||||||
getPercent(50, screen.heightPixels)
|
getPercent(50, screen.heightPixels)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createRightStick(controller, context),
|
controller.addElement(createRightStick(controller, context),
|
||||||
getPercent(55, screen.widthPixels),
|
getPercent(55, screen.widthPixels),
|
||||||
getPercent(50, screen.heightPixels),
|
getPercent(50, screen.heightPixels),
|
||||||
getPercent(40, screen.widthPixels),
|
getPercent(40, screen.widthPixels),
|
||||||
getPercent(50, screen.heightPixels)
|
getPercent(50, screen.heightPixels)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
VirtualControllerElement.EID_BACK,
|
||||||
getPercent(40, screen.widthPixels),
|
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||||
getPercent(90, screen.heightPixels),
|
getPercent(40, screen.widthPixels),
|
||||||
getPercent(10, screen.widthPixels),
|
getPercent(90, screen.heightPixels),
|
||||||
getPercent(10, screen.heightPixels)
|
getPercent(10, screen.widthPixels),
|
||||||
);
|
getPercent(10, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
VirtualControllerElement.EID_START,
|
||||||
getPercent(40, screen.widthPixels)+getPercent(10, screen.widthPixels),
|
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||||
getPercent(90, screen.heightPixels),
|
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
|
||||||
getPercent(10, screen.widthPixels),
|
getPercent(90, screen.heightPixels),
|
||||||
getPercent(10, 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,
|
||||||
NOT IMPLEMENTED YET,
|
|
||||||
this should later be used to store and load a profile for the virtual controller
|
|
||||||
public static void saveProfile(final String name,
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
final Context context) {
|
||||||
|
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
|
||||||
|
|
||||||
SharedPreferences preferences = context.getSharedPreferences(PROFILE_PATH + "/" +
|
|
||||||
name, Activity.MODE_PRIVATE);
|
|
||||||
|
|
||||||
JSONArray elementConfigurations = new JSONArray();
|
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
for (VirtualControllerElement element : controller.getElements()) {
|
||||||
JSONObject elementConfiguration = new JSONObject();
|
String prefKey = ""+element.elementId;
|
||||||
try {
|
try {
|
||||||
elementConfiguration.put("TYPE", element.getClass().getName());
|
prefEditor.putString(prefKey, element.getConfiguration().toString());
|
||||||
elementConfiguration.put("CONFIGURATION", element.getConfiguration());
|
} catch (JSONException e) {
|
||||||
elementConfigurations.put(elementConfiguration);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedPreferences.Editor editor= preferences.edit();
|
prefEditor.apply();
|
||||||
editor.putString("ELEMENTS", elementConfigurations.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadFromPreferences(final VirtualController controller) {
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
@@ -14,10 +14,30 @@ import android.view.MotionEvent;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public abstract class VirtualControllerElement extends View {
|
public abstract class VirtualControllerElement extends View {
|
||||||
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
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 VirtualController virtualController;
|
||||||
|
protected final int elementId;
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
@@ -40,10 +60,11 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
|
|
||||||
private Mode currentMode = Mode.Normal;
|
private Mode currentMode = Mode.Normal;
|
||||||
|
|
||||||
protected VirtualControllerElement(VirtualController controller, Context context) {
|
protected VirtualControllerElement(VirtualController controller, Context context, int elementId) {
|
||||||
super(context);
|
super(context);
|
||||||
|
|
||||||
this.virtualController = controller;
|
this.virtualController = controller;
|
||||||
|
this.elementId = elementId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
||||||
@@ -145,58 +166,54 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void showConfigurationDialog() {
|
protected void showConfigurationDialog() {
|
||||||
try {
|
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
||||||
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
|
||||||
|
|
||||||
alertBuilder.setTitle("Configuration");
|
alertBuilder.setTitle("Configuration");
|
||||||
|
|
||||||
CharSequence functions[] = new CharSequence[]{
|
CharSequence functions[] = new CharSequence[]{
|
||||||
"Move",
|
"Move",
|
||||||
"Resize",
|
"Resize",
|
||||||
/*election
|
/*election
|
||||||
"Set n
|
"Set n
|
||||||
Disable color sormal color",
|
Disable color sormal color",
|
||||||
"Set pressed 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;
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
"Cancel"
|
default: { // cancel
|
||||||
};
|
actionCancel();
|
||||||
|
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
case 3: { // set pressed color
|
|
||||||
actionShowPressedColorChooser();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
default: { // cancel
|
|
||||||
actionCancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
AlertDialog alert = alertBuilder.create();
|
});
|
||||||
// show menu
|
AlertDialog alert = alertBuilder.create();
|
||||||
alert.show();
|
// show menu
|
||||||
} catch (Exception e) {
|
alert.show();
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -278,13 +295,28 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
return getWidth() > getHeight() ? getHeight() : getWidth();
|
return getWidth() > getHeight() ? getHeight() : getWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
public JSONObject getConfiguration () {
|
|
||||||
JSONObject configuration = new JSONObject();
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadConfiguration (JSONObject configuration) {
|
public JSONObject getConfiguration() throws JSONException {
|
||||||
}
|
JSONObject configuration = new JSONObject();
|
||||||
*/
|
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
configuration.put("LEFT", layoutParams.leftMargin);
|
||||||
|
configuration.put("TOP", layoutParams.topMargin);
|
||||||
|
configuration.put("WIDTH", layoutParams.width);
|
||||||
|
configuration.put("HEIGHT", layoutParams.height);
|
||||||
|
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadConfiguration(JSONObject configuration) throws JSONException {
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
layoutParams.leftMargin = configuration.getInt("LEFT");
|
||||||
|
layoutParams.topMargin = configuration.getInt("TOP");
|
||||||
|
layoutParams.width = configuration.getInt("WIDTH");
|
||||||
|
layoutParams.height = configuration.getInt("HEIGHT");
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
public interface CrashListener {
|
||||||
|
void notifyCrash(Exception e);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import android.media.MediaFormat;
|
|||||||
import android.media.MediaCodec.BufferInfo;
|
import android.media.MediaCodec.BufferInfo;
|
||||||
import android.media.MediaCodec.CodecException;
|
import android.media.MediaCodec.CodecException;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.util.Range;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||||
@@ -31,13 +32,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private MediaCodecInfo avcDecoder;
|
private MediaCodecInfo avcDecoder;
|
||||||
private MediaCodecInfo hevcDecoder;
|
private MediaCodecInfo hevcDecoder;
|
||||||
|
|
||||||
// Used for HEVC only
|
|
||||||
private byte[] vpsBuffer;
|
private byte[] vpsBuffer;
|
||||||
private byte[] spsBuffer;
|
private byte[] spsBuffer;
|
||||||
|
private byte[] ppsBuffer;
|
||||||
|
private boolean submittedCsd;
|
||||||
|
private boolean submitCsdNextCall;
|
||||||
|
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private Thread[] spinnerThreads;
|
|
||||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private boolean adaptivePlayback, directSubmit;
|
private boolean adaptivePlayback, directSubmit;
|
||||||
private boolean constrainedHighProfile;
|
private boolean constrainedHighProfile;
|
||||||
@@ -47,19 +49,30 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private int videoFormat;
|
private int videoFormat;
|
||||||
private SurfaceHolder renderTarget;
|
private SurfaceHolder renderTarget;
|
||||||
private volatile boolean stopping;
|
private volatile boolean stopping;
|
||||||
|
private CrashListener crashListener;
|
||||||
|
private boolean reportedCrash;
|
||||||
|
private int consecutiveCrashCount;
|
||||||
|
private String glRenderer;
|
||||||
|
private boolean foreground = true;
|
||||||
|
private boolean legacyFrameDropRendering = false;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
private SeqParameterSet savedSps;
|
private SeqParameterSet savedSps;
|
||||||
|
|
||||||
|
private RendererException initialException;
|
||||||
|
private long initialExceptionTimestamp;
|
||||||
|
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
|
||||||
|
|
||||||
private long lastTimestampUs;
|
private long lastTimestampUs;
|
||||||
private long decoderTimeMs;
|
private long decoderTimeMs;
|
||||||
private long totalTimeMs;
|
private long totalTimeMs;
|
||||||
private int totalFrames;
|
private int totalFramesReceived;
|
||||||
|
private int totalFramesRendered;
|
||||||
private int frameLossEvents;
|
private int frameLossEvents;
|
||||||
private int framesLost;
|
private int framesLost;
|
||||||
private int lastFrameNumber;
|
private int lastFrameNumber;
|
||||||
private int refreshRate;
|
private int refreshRate;
|
||||||
private int bitrate;
|
private PreferenceConfiguration prefs;
|
||||||
|
|
||||||
private int numSpsIn;
|
private int numSpsIn;
|
||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
@@ -73,9 +86,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return decoder;
|
return decoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaCodecInfo findHevcDecoder(int videoFormat) {
|
private MediaCodecInfo findHevcDecoder(PreferenceConfiguration prefs, boolean meteredNetwork, boolean requestedHdr) {
|
||||||
// Don't return anything if H.265 is forced off
|
// Don't return anything if H.265 is forced off
|
||||||
if (videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
|
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,10 +99,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// for even required levels of HEVC.
|
// for even required levels of HEVC.
|
||||||
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
||||||
if (decoderInfo != null) {
|
if (decoderInfo != null) {
|
||||||
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName())) {
|
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork)) {
|
||||||
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
|
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
|
||||||
|
|
||||||
if (videoFormat == PreferenceConfiguration.FORCE_H265_ON) {
|
// HDR implies HEVC forced on, since HEVCMain10HDR10 is required for HDR
|
||||||
|
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_ON || requestedHdr) {
|
||||||
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
|
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -105,12 +119,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
this.renderTarget = renderTarget;
|
this.renderTarget = renderTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaCodecDecoderRenderer(int videoFormat, int bitrate) {
|
public MediaCodecDecoderRenderer(PreferenceConfiguration prefs,
|
||||||
|
CrashListener crashListener, int consecutiveCrashCount,
|
||||||
|
boolean meteredData, boolean requestedHdr,
|
||||||
|
String glRenderer) {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
|
|
||||||
this.bitrate = bitrate;
|
this.prefs = prefs;
|
||||||
|
this.crashListener = crashListener;
|
||||||
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
|
this.consecutiveCrashCount = consecutiveCrashCount;
|
||||||
|
this.glRenderer = glRenderer;
|
||||||
|
|
||||||
avcDecoder = findAvcDecoder();
|
avcDecoder = findAvcDecoder();
|
||||||
if (avcDecoder != null) {
|
if (avcDecoder != null) {
|
||||||
@@ -120,7 +138,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
LimeLog.warning("No AVC decoder found");
|
LimeLog.warning("No AVC decoder found");
|
||||||
}
|
}
|
||||||
|
|
||||||
hevcDecoder = findHevcDecoder(videoFormat);
|
hevcDecoder = findHevcDecoder(prefs, meteredData, requestedHdr);
|
||||||
if (hevcDecoder != null) {
|
if (hevcDecoder != null) {
|
||||||
LimeLog.info("Selected HEVC decoder: "+hevcDecoder.getName());
|
LimeLog.info("Selected HEVC decoder: "+hevcDecoder.getName());
|
||||||
}
|
}
|
||||||
@@ -135,10 +153,15 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// shared between AVC and HEVC decoders on the same device.
|
// shared between AVC and HEVC decoders on the same device.
|
||||||
if (avcDecoder != null) {
|
if (avcDecoder != null) {
|
||||||
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName());
|
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName());
|
||||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder.getName());
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder);
|
||||||
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName());
|
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
|
||||||
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
|
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
|
||||||
|
|
||||||
|
if (consecutiveCrashCount % 2 == 1) {
|
||||||
|
refFrameInvalidationAvc = refFrameInvalidationHevc = false;
|
||||||
|
LimeLog.warning("Disabling RFI due to previous crash");
|
||||||
|
}
|
||||||
|
|
||||||
if (directSubmit) {
|
if (directSubmit) {
|
||||||
LimeLog.info("Decoder "+avcDecoder.getName()+" will use direct submit");
|
LimeLog.info("Decoder "+avcDecoder.getName()+" will use direct submit");
|
||||||
}
|
}
|
||||||
@@ -159,6 +182,38 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return avcDecoder != null;
|
return avcDecoder != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean is49FpsBlacklisted() {
|
||||||
|
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedFor49Fps(avcDecoder.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableLegacyFrameDropRendering() {
|
||||||
|
LimeLog.info("Legacy frame drop rendering enabled");
|
||||||
|
legacyFrameDropRendering = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHevcMain10Hdr10Supported() {
|
||||||
|
if (hevcDecoder == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MediaCodecInfo.CodecProfileLevel profileLevel : hevcDecoder.getCapabilitiesForType("video/hevc").profileLevels) {
|
||||||
|
if (profileLevel.profile == MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10) {
|
||||||
|
LimeLog.info("HEVC decoder "+hevcDecoder.getName()+" supports HEVC Main10 HDR10");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyVideoForeground() {
|
||||||
|
foreground = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyVideoBackground() {
|
||||||
|
foreground = false;
|
||||||
|
}
|
||||||
|
|
||||||
public int getActiveVideoFormat() {
|
public int getActiveVideoFormat() {
|
||||||
return this.videoFormat;
|
return this.videoFormat;
|
||||||
}
|
}
|
||||||
@@ -173,7 +228,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
String mimeType;
|
String mimeType;
|
||||||
String selectedDecoderName;
|
String selectedDecoderName;
|
||||||
|
|
||||||
if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
|
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
|
||||||
mimeType = "video/avc";
|
mimeType = "video/avc";
|
||||||
selectedDecoderName = avcDecoder.getName();
|
selectedDecoderName = avcDecoder.getName();
|
||||||
|
|
||||||
@@ -202,7 +257,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
refFrameInvalidationActive = refFrameInvalidationAvc;
|
refFrameInvalidationActive = refFrameInvalidationAvc;
|
||||||
}
|
}
|
||||||
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
|
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
|
||||||
mimeType = "video/hevc";
|
mimeType = "video/hevc";
|
||||||
selectedDecoderName = hevcDecoder.getName();
|
selectedDecoderName = hevcDecoder.getName();
|
||||||
|
|
||||||
@@ -294,13 +349,35 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only throw if the surface is still around and we're not stopping
|
// Only throw if we're not stopping
|
||||||
if (!stopping && renderTarget.getSurface().isValid()) {
|
if (!stopping) {
|
||||||
if (buf != null || codecFlags != 0) {
|
//
|
||||||
throw new RendererException(this, e, buf, codecFlags);
|
// There seems to be a race condition with decoder/surface teardown causing some
|
||||||
|
// decoders to to throw IllegalStateExceptions even before 'stopping' is set.
|
||||||
|
// To workaround this while allowing real exceptions to propagate, we will eat the
|
||||||
|
// first exception. If we are still receiving exceptions 3 seconds later, we will
|
||||||
|
// throw the original exception again.
|
||||||
|
//
|
||||||
|
if (initialException != null) {
|
||||||
|
// This isn't the first time we've had an exception processing video
|
||||||
|
if (System.currentTimeMillis() - initialExceptionTimestamp >= EXCEPTION_REPORT_DELAY_MS) {
|
||||||
|
// It's been over 3 seconds and we're still getting exceptions. Throw the original now.
|
||||||
|
if (!reportedCrash) {
|
||||||
|
reportedCrash = true;
|
||||||
|
crashListener.notifyCrash(initialException);
|
||||||
|
}
|
||||||
|
throw initialException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new RendererException(this, e);
|
// This is the first exception we've hit
|
||||||
|
if (buf != null || codecFlags != 0) {
|
||||||
|
initialException = new RendererException(this, e, buf, codecFlags);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
initialException = new RendererException(this, e);
|
||||||
|
}
|
||||||
|
initialExceptionTimestamp = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +388,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
while (!isInterrupted()) {
|
while (!stopping) {
|
||||||
try {
|
try {
|
||||||
// Try to output a frame
|
// Try to output a frame
|
||||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
||||||
@@ -328,7 +405,23 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the last buffer
|
// Render the last buffer
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (legacyFrameDropRendering) {
|
||||||
|
// Use a PTS that will cause this frame to be dropped if another comes in within
|
||||||
|
// the same V-sync period
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
||||||
|
// is disabled
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalFramesRendered++;
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||||
@@ -361,32 +454,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
rendererThread.start();
|
rendererThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startSpinnerThreads() {
|
|
||||||
LimeLog.info("Using "+spinnerThreads.length+" spinner threads");
|
|
||||||
for (int i = 0; i < spinnerThreads.length; i++) {
|
|
||||||
spinnerThreads[i] = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// This thread exists to keep the CPU at a higher DVFS state on devices
|
|
||||||
// where the governor scales clock speed sporadically, causing dropped frames.
|
|
||||||
while (!isInterrupted()) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(0, 1);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spinnerThreads[i].setName("Spinner-"+i);
|
|
||||||
spinnerThreads[i].setPriority(Thread.MIN_PRIORITY);
|
|
||||||
spinnerThreads[i].start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int dequeueInputBuffer() {
|
private int dequeueInputBuffer() {
|
||||||
int index = -1;
|
int index = -1;
|
||||||
long startTime, queueTime;
|
long startTime;
|
||||||
|
|
||||||
startTime = MediaCodecHelper.getMonotonicMillis();
|
startTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
|
||||||
@@ -399,14 +469,24 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0) {
|
int deltaMs = (int)(MediaCodecHelper.getMonotonicMillis() - startTime);
|
||||||
return index;
|
|
||||||
|
if (deltaMs >= 20) {
|
||||||
|
LimeLog.warning("Dequeue input buffer ran long: " + deltaMs + " ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
queueTime = MediaCodecHelper.getMonotonicMillis();
|
if (index < 0) {
|
||||||
|
// We've been hung for 5 seconds and no other exception was reported,
|
||||||
if (queueTime - startTime >= 20) {
|
// so generate a decoder hung exception
|
||||||
LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms");
|
if (deltaMs >= 5000 && initialException == null) {
|
||||||
|
DecoderHungException decoderHungException = new DecoderHungException(deltaMs);
|
||||||
|
if (!reportedCrash) {
|
||||||
|
reportedCrash = true;
|
||||||
|
crashListener.notifyCrash(decoderHungException);
|
||||||
|
}
|
||||||
|
throw new RendererException(this, decoderHungException);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
return index;
|
return index;
|
||||||
@@ -415,49 +495,28 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
startRendererThread();
|
startRendererThread();
|
||||||
startSpinnerThreads();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// !!! May be called even if setup()/start() fails !!!
|
||||||
public void prepareForStop() {
|
public void prepareForStop() {
|
||||||
// Let the decoding code know to ignore codec exceptions now
|
// Let the decoding code know to ignore codec exceptions now
|
||||||
stopping = true;
|
stopping = true;
|
||||||
|
|
||||||
|
// Halt the rendering thread
|
||||||
|
if (rendererThread != null) {
|
||||||
|
rendererThread.interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
stopping = true;
|
// May be called already, but we'll call it now to be safe
|
||||||
|
prepareForStop();
|
||||||
// Halt the rendering thread
|
|
||||||
rendererThread.interrupt();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Invalidate pending decode buffers
|
|
||||||
videoDecoder.flush();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the renderer thread to shut down
|
// Wait for the renderer thread to shut down
|
||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException ignored) { }
|
} catch (InterruptedException ignored) { }
|
||||||
|
|
||||||
try {
|
|
||||||
// Stop the video decoder
|
|
||||||
videoDecoder.stop();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Halt the spinner threads
|
|
||||||
for (Thread t : spinnerThreads) {
|
|
||||||
t.interrupt();
|
|
||||||
}
|
|
||||||
for (Thread t : spinnerThreads) {
|
|
||||||
try {
|
|
||||||
t.join();
|
|
||||||
} catch (InterruptedException ignored) { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -505,22 +564,28 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// High Profile which allows the decoder to assume there will be no B-frames and
|
// High Profile which allows the decoder to assume there will be no B-frames and
|
||||||
// reduce delay and buffering accordingly. Some devices (Marvell, Exynos 4) don't
|
// reduce delay and buffering accordingly. Some devices (Marvell, Exynos 4) don't
|
||||||
// like it so we only set them on devices that are confirmed to benefit from it.
|
// like it so we only set them on devices that are confirmed to benefit from it.
|
||||||
if (sps.profile_idc == 100 && constrainedHighProfile) {
|
if (sps.profileIdc == 100 && constrainedHighProfile) {
|
||||||
LimeLog.info("Setting constraint set flags for constrained high profile");
|
LimeLog.info("Setting constraint set flags for constrained high profile");
|
||||||
sps.constraint_set_4_flag = true;
|
sps.constraintSet4Flag = true;
|
||||||
sps.constraint_set_5_flag = true;
|
sps.constraintSet5Flag = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Force the constraints unset otherwise (some may be set by default)
|
// Force the constraints unset otherwise (some may be set by default)
|
||||||
sps.constraint_set_4_flag = false;
|
sps.constraintSet4Flag = false;
|
||||||
sps.constraint_set_5_flag = false;
|
sps.constraintSet5Flag = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public int submitDecodeUnit(byte[] frameData, int frameLength, int frameNumber, long receiveTimeMs) {
|
public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||||
totalFrames++;
|
int frameNumber, long receiveTimeMs) {
|
||||||
|
if (stopping) {
|
||||||
|
// Don't bother if we're stopping
|
||||||
|
return MoonBridge.DR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalFramesReceived++;
|
||||||
|
|
||||||
// We can receive the same "frame" multiple times if it's an IDR frame.
|
// We can receive the same "frame" multiple times if it's an IDR frame.
|
||||||
// In that case, each frame start NALU is submitted independently.
|
// In that case, each frame start NALU is submitted independently.
|
||||||
@@ -550,14 +615,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
lastTimestampUs = timestampUs;
|
lastTimestampUs = timestampUs;
|
||||||
|
|
||||||
int codecFlags = 0;
|
int codecFlags = 0;
|
||||||
boolean needsSpsReplay = false;
|
|
||||||
|
|
||||||
// H264 SPS
|
// H264 SPS
|
||||||
if (frameData[4] == 0x67) {
|
if (decodeUnitData[4] == 0x67) {
|
||||||
numSpsIn++;
|
numSpsIn++;
|
||||||
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
|
||||||
|
|
||||||
ByteBuffer spsBuf = ByteBuffer.wrap(frameData);
|
ByteBuffer spsBuf = ByteBuffer.wrap(decodeUnitData);
|
||||||
|
|
||||||
// Skip to the start of the NALU data
|
// Skip to the start of the NALU data
|
||||||
spsBuf.position(5);
|
spsBuf.position(5);
|
||||||
@@ -571,15 +634,20 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// for known resolution combinations. Reference frame invalidation may need
|
// for known resolution combinations. Reference frame invalidation may need
|
||||||
// these, so leave them be for those decoders.
|
// these, so leave them be for those decoders.
|
||||||
if (!refFrameInvalidationActive) {
|
if (!refFrameInvalidationActive) {
|
||||||
if (initialWidth == 1280 && initialHeight == 720) {
|
if (initialWidth <= 720 && initialHeight <= 480) {
|
||||||
|
// Max 5 buffered frames at 720x480x60
|
||||||
|
LimeLog.info("Patching level_idc to 31");
|
||||||
|
sps.levelIdc = 31;
|
||||||
|
}
|
||||||
|
else if (initialWidth <= 1280 && initialHeight <= 720) {
|
||||||
// Max 5 buffered frames at 1280x720x60
|
// Max 5 buffered frames at 1280x720x60
|
||||||
LimeLog.info("Patching level_idc to 32");
|
LimeLog.info("Patching level_idc to 32");
|
||||||
sps.level_idc = 32;
|
sps.levelIdc = 32;
|
||||||
}
|
}
|
||||||
else if (initialWidth == 1920 && initialHeight == 1080) {
|
else if (initialWidth <= 1920 && initialHeight <= 1080) {
|
||||||
// Max 4 buffered frames at 1920x1080x64
|
// Max 4 buffered frames at 1920x1080x64
|
||||||
LimeLog.info("Patching level_idc to 42");
|
LimeLog.info("Patching level_idc to 42");
|
||||||
sps.level_idc = 42;
|
sps.levelIdc = 42;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Leave the profile alone (currently 5.0)
|
// Leave the profile alone (currently 5.0)
|
||||||
@@ -597,14 +665,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// where we've enabled reference frame invalidation.
|
// where we've enabled reference frame invalidation.
|
||||||
if (!refFrameInvalidationActive) {
|
if (!refFrameInvalidationActive) {
|
||||||
LimeLog.info("Patching num_ref_frames in SPS");
|
LimeLog.info("Patching num_ref_frames in SPS");
|
||||||
sps.num_ref_frames = 1;
|
sps.numRefFrames = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GFE 2.5.11 changed the SPS to add additional extensions
|
// GFE 2.5.11 changed the SPS to add additional extensions
|
||||||
// Some devices don't like these so we remove them here.
|
// Some devices don't like these so we remove them here.
|
||||||
sps.vuiParams.video_signal_type_present_flag = false;
|
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||||
sps.vuiParams.colour_description_present_flag = false;
|
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||||
sps.vuiParams.chroma_loc_info_present_flag = false;
|
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||||
|
|
||||||
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
|
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
|
||||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||||
@@ -614,22 +682,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
if (sps.vuiParams.bitstreamRestriction == null) {
|
if (sps.vuiParams.bitstreamRestriction == null) {
|
||||||
LimeLog.info("Adding bitstream restrictions");
|
LimeLog.info("Adding bitstream restrictions");
|
||||||
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
||||||
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
|
sps.vuiParams.bitstreamRestriction.motionVectorsOverPicBoundariesFlag = true;
|
||||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
|
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthHorizontal = 16;
|
||||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
|
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthVertical = 16;
|
||||||
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
sps.vuiParams.bitstreamRestriction.numReorderFrames = 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LimeLog.info("Patching bitstream restrictions");
|
LimeLog.info("Patching bitstream restrictions");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
|
// Some devices throw errors if maxDecFrameBuffering < numRefFrames
|
||||||
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
|
sps.vuiParams.bitstreamRestriction.maxDecFrameBuffering = sps.numRefFrames;
|
||||||
|
|
||||||
// These values are the defaults for the fields, but they are more aggressive
|
// These values are the defaults for the fields, but they are more aggressive
|
||||||
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
|
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
|
||||||
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
|
sps.vuiParams.bitstreamRestriction.maxBytesPerPicDenom = 2;
|
||||||
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
|
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
|
||||||
|
|
||||||
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
|
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
|
||||||
// conservative values by GFE 2.5.11. We'll let those values stand.
|
// conservative values by GFE 2.5.11. We'll let those values stand.
|
||||||
@@ -643,126 +711,122 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// If we need to hack this SPS to say we're baseline, do so now
|
// If we need to hack this SPS to say we're baseline, do so now
|
||||||
if (needsBaselineSpsHack) {
|
if (needsBaselineSpsHack) {
|
||||||
LimeLog.info("Hacking SPS to baseline");
|
LimeLog.info("Hacking SPS to baseline");
|
||||||
sps.profile_idc = 66;
|
sps.profileIdc = 66;
|
||||||
savedSps = sps;
|
savedSps = sps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch the SPS constraint flags
|
// Patch the SPS constraint flags
|
||||||
doProfileSpecificSpsPatching(sps);
|
doProfileSpecificSpsPatching(sps);
|
||||||
|
|
||||||
inputBufferIndex = dequeueInputBuffer();
|
|
||||||
if (inputBufferIndex < 0) {
|
|
||||||
// We're being torn down now
|
|
||||||
return MoonBridge.DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
|
||||||
if (buf == null) {
|
|
||||||
// We're being torn down now
|
|
||||||
return MoonBridge.DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the annex B header
|
|
||||||
buf.put(frameData, 0, 5);
|
|
||||||
|
|
||||||
// The H264Utils.writeSPS function safely handles
|
// The H264Utils.writeSPS function safely handles
|
||||||
// Annex B NALUs (including NALUs with escape sequences)
|
// Annex B NALUs (including NALUs with escape sequences)
|
||||||
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, frameLength);
|
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, decodeUnitLength);
|
||||||
buf.put(escapedNalu);
|
|
||||||
|
|
||||||
if (queueInputBuffer(inputBufferIndex,
|
// Batch this to submit together with PPS
|
||||||
0, buf.position(),
|
spsBuffer = new byte[5 + escapedNalu.limit()];
|
||||||
timestampUs, codecFlags)) {
|
System.arraycopy(decodeUnitData, 0, spsBuffer, 0, 5);
|
||||||
return MoonBridge.DR_OK;
|
escapedNalu.get(spsBuffer, 5, escapedNalu.limit());
|
||||||
}
|
return MoonBridge.DR_OK;
|
||||||
else {
|
|
||||||
return MoonBridge.DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// H264 PPS
|
|
||||||
} else if (frameData[4] == 0x68) {
|
|
||||||
numPpsIn++;
|
|
||||||
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
|
||||||
|
|
||||||
inputBufferIndex = dequeueInputBuffer();
|
|
||||||
if (inputBufferIndex < 0) {
|
|
||||||
// We're being torn down now
|
|
||||||
return MoonBridge.DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
|
||||||
if (buf == null) {
|
|
||||||
// We're being torn down now
|
|
||||||
return MoonBridge.DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsBaselineSpsHack) {
|
|
||||||
LimeLog.info("Saw PPS; disabling SPS hack");
|
|
||||||
needsBaselineSpsHack = false;
|
|
||||||
|
|
||||||
// Give the decoder the SPS again with the proper profile now
|
|
||||||
needsSpsReplay = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (frameData[4] == 0x40) {
|
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_VPS) {
|
||||||
numVpsIn++;
|
numVpsIn++;
|
||||||
|
|
||||||
// Batch this to submit together with SPS and PPS per AOSP docs
|
// Batch this to submit together with SPS and PPS per AOSP docs
|
||||||
vpsBuffer = new byte[frameLength];
|
vpsBuffer = new byte[decodeUnitLength];
|
||||||
System.arraycopy(frameData, 0, vpsBuffer, 0, frameLength);
|
System.arraycopy(decodeUnitData, 0, vpsBuffer, 0, decodeUnitLength);
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_OK;
|
||||||
}
|
}
|
||||||
else if (frameData[4] == 0x42) {
|
// Only the HEVC SPS hits this path (H.264 is handled above)
|
||||||
|
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_SPS) {
|
||||||
numSpsIn++;
|
numSpsIn++;
|
||||||
|
|
||||||
// Batch this to submit together with VPS and PPS per AOSP docs
|
// Batch this to submit together with VPS and PPS per AOSP docs
|
||||||
spsBuffer = new byte[frameLength];
|
spsBuffer = new byte[decodeUnitLength];
|
||||||
System.arraycopy(frameData, 0, spsBuffer, 0, frameLength);
|
System.arraycopy(decodeUnitData, 0, spsBuffer, 0, decodeUnitLength);
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_OK;
|
||||||
}
|
}
|
||||||
else if (frameData[4] == 0x44) {
|
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_PPS) {
|
||||||
numPpsIn++;
|
numPpsIn++;
|
||||||
|
|
||||||
inputBufferIndex = dequeueInputBuffer();
|
// If this is the first CSD blob or we aren't supporting
|
||||||
if (inputBufferIndex < 0) {
|
// adaptive playback, we will submit the CSD blob in a
|
||||||
// We're being torn down now
|
// separate input buffer.
|
||||||
|
if (!submittedCsd || !adaptivePlayback) {
|
||||||
|
inputBufferIndex = dequeueInputBuffer();
|
||||||
|
if (inputBufferIndex < 0) {
|
||||||
|
// We're being torn down now
|
||||||
|
return MoonBridge.DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||||
|
if (buf == null) {
|
||||||
|
// We're being torn down now
|
||||||
|
return MoonBridge.DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we get the PPS, submit the VPS and SPS together with
|
||||||
|
// the PPS, as required by AOSP docs on use of MediaCodec.
|
||||||
|
if (vpsBuffer != null) {
|
||||||
|
buf.put(vpsBuffer);
|
||||||
|
}
|
||||||
|
if (spsBuffer != null) {
|
||||||
|
buf.put(spsBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the CSD blob
|
||||||
|
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Batch this to submit together with the next I-frame
|
||||||
|
ppsBuffer = new byte[decodeUnitLength];
|
||||||
|
System.arraycopy(decodeUnitData, 0, ppsBuffer, 0, decodeUnitLength);
|
||||||
|
|
||||||
|
// Next call will be I-frame data
|
||||||
|
submitCsdNextCall = true;
|
||||||
|
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
|
||||||
if (buf == null) {
|
|
||||||
// We're being torn down now
|
|
||||||
return MoonBridge.DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we get the PPS, submit the VPS and SPS together with
|
|
||||||
// the PPS, as required by AOSP docs on use of HEVC and MediaCodec.
|
|
||||||
if (vpsBuffer != null) {
|
|
||||||
buf.put(vpsBuffer);
|
|
||||||
}
|
|
||||||
if (spsBuffer != null) {
|
|
||||||
buf.put(spsBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the HEVC CSD blob
|
|
||||||
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
inputBufferIndex = dequeueInputBuffer();
|
inputBufferIndex = dequeueInputBuffer();
|
||||||
if (inputBufferIndex < 0) {
|
if (inputBufferIndex < 0) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||||
if (buf == null) {
|
if (buf == null) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submitCsdNextCall) {
|
||||||
|
if (vpsBuffer != null) {
|
||||||
|
buf.put(vpsBuffer);
|
||||||
|
}
|
||||||
|
if (spsBuffer != null) {
|
||||||
|
buf.put(spsBuffer);
|
||||||
|
}
|
||||||
|
if (ppsBuffer != null) {
|
||||||
|
buf.put(ppsBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitCsdNextCall = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (decodeUnitLength > buf.limit() - buf.position()) {
|
||||||
|
IllegalArgumentException exception = new IllegalArgumentException(
|
||||||
|
"Decode unit length "+decodeUnitLength+" too large for input buffer "+buf.limit());
|
||||||
|
if (!reportedCrash) {
|
||||||
|
reportedCrash = true;
|
||||||
|
crashListener.notifyCrash(exception);
|
||||||
|
}
|
||||||
|
throw new RendererException(this, exception);
|
||||||
|
}
|
||||||
|
|
||||||
// Copy data from our buffer list into the input buffer
|
// Copy data from our buffer list into the input buffer
|
||||||
buf.put(frameData, 0, frameLength);
|
buf.put(decodeUnitData, 0, decodeUnitLength);
|
||||||
|
|
||||||
if (!queueInputBuffer(inputBufferIndex,
|
if (!queueInputBuffer(inputBufferIndex,
|
||||||
0, buf.position(),
|
0, buf.position(),
|
||||||
@@ -770,12 +834,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return MoonBridge.DR_NEED_IDR;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsSpsReplay) {
|
if ((codecFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||||
if (!replaySps()) {
|
submittedCsd = true;
|
||||||
return MoonBridge.DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("SPS replay complete");
|
if (needsBaselineSpsHack) {
|
||||||
|
needsBaselineSpsHack = false;
|
||||||
|
|
||||||
|
if (!replaySps()) {
|
||||||
|
return MoonBridge.DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("SPS replay complete");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_OK;
|
||||||
@@ -796,7 +866,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||||
|
|
||||||
// Switch the H264 profile back to high
|
// Switch the H264 profile back to high
|
||||||
savedSps.profile_idc = 100;
|
savedSps.profileIdc = 100;
|
||||||
|
|
||||||
// Patch the SPS constraint flags
|
// Patch the SPS constraint flags
|
||||||
doProfileSpecificSpsPatching(savedSps);
|
doProfileSpecificSpsPatching(savedSps);
|
||||||
@@ -840,53 +910,82 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getAverageEndToEndLatency() {
|
public int getAverageEndToEndLatency() {
|
||||||
if (totalFrames == 0) {
|
if (totalFramesReceived == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return (int)(totalTimeMs / totalFrames);
|
return (int)(totalTimeMs / totalFramesReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getAverageDecoderLatency() {
|
public int getAverageDecoderLatency() {
|
||||||
if (totalFrames == 0) {
|
if (totalFramesReceived == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return (int)(decoderTimeMs / totalFrames);
|
return (int)(decoderTimeMs / totalFramesReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RendererException extends RuntimeException {
|
static class DecoderHungException extends RuntimeException {
|
||||||
private static final long serialVersionUID = 8985937536997012406L;
|
private int hangTimeMs;
|
||||||
|
|
||||||
private final Exception originalException;
|
DecoderHungException(int hangTimeMs) {
|
||||||
private final MediaCodecDecoderRenderer renderer;
|
this.hangTimeMs = hangTimeMs;
|
||||||
private ByteBuffer currentBuffer;
|
|
||||||
private int currentCodecFlags;
|
|
||||||
|
|
||||||
public RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
|
|
||||||
this.renderer = renderer;
|
|
||||||
this.originalException = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
|
|
||||||
this.renderer = renderer;
|
|
||||||
this.originalException = e;
|
|
||||||
this.currentBuffer = currentBuffer;
|
|
||||||
this.currentCodecFlags = currentCodecFlags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
String str = "";
|
String str = "";
|
||||||
|
|
||||||
str += "Format: "+renderer.videoFormat+"\n";
|
str += "Hang time: "+hangTimeMs+" ms\n";
|
||||||
|
str += super.toString();
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RendererException extends RuntimeException {
|
||||||
|
private static final long serialVersionUID = 8985937536997012406L;
|
||||||
|
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
|
||||||
|
this.text = generateText(renderer, e, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
|
||||||
|
this.text = generateText(renderer, e, currentBuffer, currentCodecFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) {
|
||||||
|
String str = "";
|
||||||
|
|
||||||
|
str += "Format: "+String.format("%x", renderer.videoFormat)+"\n";
|
||||||
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
||||||
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
||||||
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
|
||||||
|
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
||||||
|
str += "AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper()+"\n";
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
|
||||||
|
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
||||||
|
str += "HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper()+"\n";
|
||||||
|
}
|
||||||
|
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
|
||||||
|
str += "GL Renderer: "+renderer.glRenderer+"\n";
|
||||||
|
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
||||||
|
str += "Foreground: "+renderer.foreground+"\n";
|
||||||
|
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
||||||
|
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
|
||||||
|
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||||
str += "FPS target: "+renderer.refreshRate+"\n";
|
str += "FPS target: "+renderer.refreshRate+"\n";
|
||||||
str += "Bitrate: "+renderer.bitrate+" Mbps \n";
|
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
||||||
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
||||||
str += "Total frames: "+renderer.totalFrames+"\n";
|
str += "Total frames received: "+renderer.totalFramesReceived+"\n";
|
||||||
str += "Frame losses: "+renderer.framesLost+" in "+frameLossEvents+" loss events\n";
|
str += "Total frames rendered: "+renderer.totalFramesRendered+"\n";
|
||||||
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
|
str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\n";
|
||||||
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
|
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
|
||||||
|
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
|
||||||
|
|
||||||
if (currentBuffer != null) {
|
if (currentBuffer != null) {
|
||||||
str += "Current buffer: ";
|
str += "Current buffer: ";
|
||||||
@@ -900,6 +999,27 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
|
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (originalException instanceof CodecException) {
|
||||||
|
CodecException ce = (CodecException) originalException;
|
||||||
|
|
||||||
|
str += "Diagnostic Info: "+ce.getDiagnosticInfo()+"\n";
|
||||||
|
str += "Recoverable: "+ce.isRecoverable()+"\n";
|
||||||
|
str += "Transient: "+ce.isTransient()+"\n";
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
str += "Codec Error Code: "+ce.getErrorCode()+"\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str += "/proc/cpuinfo:\n";
|
||||||
|
try {
|
||||||
|
str += MediaCodecHelper.readCpuinfo();
|
||||||
|
} catch (Exception e) {
|
||||||
|
str += e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
str += "Full decoder dump:\n";
|
str += "Full decoder dump:\n";
|
||||||
try {
|
try {
|
||||||
str += MediaCodecHelper.dumpDecoders();
|
str += MediaCodecHelper.dumpDecoders();
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import java.util.Collections;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.ConfigurationInfo;
|
import android.content.pm.ConfigurationInfo;
|
||||||
@@ -27,13 +28,18 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
private static final List<String> blacklistedDecoderPrefixes;
|
private static final List<String> blacklistedDecoderPrefixes;
|
||||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
|
||||||
|
private static final List<String> deprioritizedHevcDecoders;
|
||||||
private static final List<String> baselineProfileHackPrefixes;
|
private static final List<String> baselineProfileHackPrefixes;
|
||||||
private static final List<String> directSubmitPrefixes;
|
private static final List<String> directSubmitPrefixes;
|
||||||
private static final List<String> constrainedHighProfilePrefixes;
|
private static final List<String> constrainedHighProfilePrefixes;
|
||||||
private static final List<String> whitelistedHevcDecoders;
|
private static final List<String> whitelistedHevcDecoders;
|
||||||
private static final List<String> refFrameInvalidationAvcPrefixes;
|
private static final List<String> refFrameInvalidationAvcPrefixes;
|
||||||
private static final List<String> refFrameInvalidationHevcPrefixes;
|
private static final List<String> refFrameInvalidationHevcPrefixes;
|
||||||
|
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
||||||
|
|
||||||
|
private static boolean isLowEndSnapdragon = false;
|
||||||
|
private static boolean initialized = false;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
directSubmitPrefixes = new LinkedList<>();
|
directSubmitPrefixes = new LinkedList<>();
|
||||||
@@ -65,16 +71,14 @@ public class MediaCodecHelper {
|
|||||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
// Blacklist software decoders that don't support H264 high profile,
|
// Blacklist software decoders that don't support H264 high profile,
|
||||||
// but exclude the official AOSP emulator from this restriction.
|
// but exclude the official AOSP and CrOS emulator from this restriction.
|
||||||
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
|
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
|
// Never use ffmpeg decoders since they're software decoders
|
||||||
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
|
||||||
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
|
|
||||||
|
|
||||||
// Force these decoders disabled because:
|
// Force these decoders disabled because:
|
||||||
// 1) They are software decoders, so the performance is terrible
|
// 1) They are software decoders, so the performance is terrible
|
||||||
@@ -94,11 +98,13 @@ public class MediaCodecHelper {
|
|||||||
baselineProfileHackPrefixes = new LinkedList<>();
|
baselineProfileHackPrefixes = new LinkedList<>();
|
||||||
baselineProfileHackPrefixes.add("omx.intel");
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
|
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
// if adaptive playback was enabled so let's avoid it to be safe.
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
// 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 = new LinkedList<>();
|
||||||
constrainedHighProfilePrefixes.add("omx.intel");
|
constrainedHighProfilePrefixes.add("omx.intel");
|
||||||
@@ -115,14 +121,21 @@ public class MediaCodecHelper {
|
|||||||
// Exynos seems to be the only HEVC decoder that works reliably
|
// Exynos seems to be the only HEVC decoder that works reliably
|
||||||
whitelistedHevcDecoders.add("omx.exynos");
|
whitelistedHevcDecoders.add("omx.exynos");
|
||||||
|
|
||||||
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
|
||||||
//whitelistedHevcDecoders.add("omx.nvidia");
|
// 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).
|
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
||||||
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
|
// I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
|
||||||
// to actually be tested. Ugh...
|
// to actually be tested.
|
||||||
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
|
whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
}
|
}
|
||||||
|
|
||||||
// These theoretically have good HEVC decoding capabilities (potentially better than
|
// These theoretically have good HEVC decoding capabilities (potentially better than
|
||||||
@@ -134,20 +147,94 @@ public class MediaCodecHelper {
|
|||||||
// during initialization to avoid SoCs with broken HEVC decoders.
|
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initializeWithContext(Context context) {
|
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 activityManager =
|
||||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
||||||
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
||||||
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
||||||
|
|
||||||
|
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
|
||||||
|
|
||||||
// Tegra K1 and later can do reference frame invalidation properly
|
// Tegra K1 and later can do reference frame invalidation properly
|
||||||
if (configInfo.reqGlEsVersion >= 0x30000) {
|
if (configInfo.reqGlEsVersion >= 0x30000) {
|
||||||
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
||||||
|
|
||||||
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
|
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
||||||
|
|
||||||
// Prior to M, we were tricking the decoder into using baseline profile, which
|
// Prior to M, we were tricking the decoder into using baseline profile, which
|
||||||
// won't support RFI properly.
|
// won't support RFI properly.
|
||||||
@@ -155,26 +242,49 @@ public class MediaCodecHelper {
|
|||||||
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
// 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:
|
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
||||||
// 3xx - bad
|
// 3xx - bad
|
||||||
// 4xx - good
|
// 4xx - good
|
||||||
//
|
//
|
||||||
// Unfortunately, it's not that easy to get that information here, so I'll use an
|
// The "good" GPUs support GLES 3.1, but we can't just check that directly
|
||||||
// approximation by checking the GLES level (<= 3.0 is bad).
|
// (see comment on isGLES31SnapdragonRenderer).
|
||||||
if (configInfo.reqGlEsVersion > 0x30000) {
|
//
|
||||||
// FIXME: We prefer reference frame invalidation support (which is only doable on AVC on
|
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
||||||
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
// We prefer reference frame invalidation support (which is only doable on AVC on
|
||||||
// to force HEVC on.
|
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
||||||
//LimeLog.info("Added omx.qcom to supported HEVC decoders based on GLES 3.1+ support");
|
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
|
||||||
//whitelistedHevcDecoders.add("omx.qcom");
|
// HEVC anyway.
|
||||||
}
|
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
|
||||||
|
deprioritizedHevcDecoders.add("omx.qcom");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
|
||||||
|
// PowerVR GPUs have good HEVC support.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) {
|
||||||
|
LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU");
|
||||||
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
|
|
||||||
|
// This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting
|
||||||
|
// required to make it work adds a huge amount of latency.
|
||||||
|
LimeLog.info("Added omx.mtk to RFI list for HEVC");
|
||||||
|
refFrameInvalidationHevcPrefixes.add("omx.mtk");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
|
if (!initialized) {
|
||||||
|
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
||||||
|
}
|
||||||
|
|
||||||
for (String badPrefix : decoderList) {
|
for (String badPrefix : decoderList) {
|
||||||
if (decoderName.length() >= badPrefix.length()) {
|
if (decoderName.length() >= badPrefix.length()) {
|
||||||
String prefix = decoderName.substring(0, badPrefix.length());
|
String prefix = decoderName.substring(0, badPrefix.length());
|
||||||
@@ -190,20 +300,15 @@ public class MediaCodecHelper {
|
|||||||
public static long getMonotonicMillis() {
|
public static long getMonotonicMillis() {
|
||||||
return System.nanoTime() / 1000000L;
|
return System.nanoTime() / 1000000L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
|
|
||||||
/*
|
|
||||||
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
|
||||||
so we'll keep it off for now, since we don't know whether other devices also do the same
|
|
||||||
|
|
||||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
|
||||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possibly enable adaptive playback on KitKat and above
|
// Possibly enable adaptive playback on KitKat and above
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
|
||||||
|
LimeLog.info("Decoder blacklisted for adaptive playback");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (decoderInfo.getCapabilitiesForType("video/avc").
|
if (decoderInfo.getCapabilitiesForType("video/avc").
|
||||||
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
||||||
@@ -215,7 +320,7 @@ public class MediaCodecHelper {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Tolerate buggy codecs
|
// Tolerate buggy codecs
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -236,7 +341,22 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String 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);
|
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +364,7 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
|
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
|
||||||
// TODO: Shield Tablet K1/LTE?
|
// TODO: Shield Tablet K1/LTE?
|
||||||
//
|
//
|
||||||
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
||||||
@@ -276,6 +396,15 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
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);
|
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +452,10 @@ public class MediaCodecHelper {
|
|||||||
// This is a different algorithm than the other findXXXDecoder functions,
|
// This is a different algorithm than the other findXXXDecoder functions,
|
||||||
// because we want to evaluate the decoders in our list's order
|
// because we want to evaluate the decoders in our list's order
|
||||||
// rather than MediaCodecList's order
|
// rather than MediaCodecList's order
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
||||||
|
}
|
||||||
|
|
||||||
for (String preferredDecoder : preferredDecoders) {
|
for (String preferredDecoder : preferredDecoders) {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.limelight.computers;
|
package com.limelight.computers;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.net.UnknownHostException;
|
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.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
@@ -17,13 +18,15 @@ import android.database.sqlite.SQLiteDatabase;
|
|||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
public class ComputerDatabaseManager {
|
public class ComputerDatabaseManager {
|
||||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
private static final String COMPUTER_DB_NAME = "computers2.db";
|
||||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
|
||||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
|
||||||
private static final String MAC_COLUMN_NAME = "Mac";
|
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;
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
@@ -36,20 +39,35 @@ public class ComputerDatabaseManager {
|
|||||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
}
|
}
|
||||||
initializeDb();
|
initializeDb(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
computerDb.close();
|
computerDb.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeDb() {
|
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
|
// Create tables if they aren't already there
|
||||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
computerDb.execSQL(String.format((Locale)null,
|
||||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
"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_TABLE_NAME,
|
||||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||||
REMOTE_IP_COLUMN_NAME, MAC_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) {
|
public void deleteComputer(String name) {
|
||||||
@@ -58,57 +76,64 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
||||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
|
||||||
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
||||||
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
||||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
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);
|
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() {
|
public List<ComputerDetails> getAllComputers() {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
|
|
||||||
details.name = c.getString(0);
|
// If a critical field is corrupt or missing, skip the database entry
|
||||||
|
if (details.uuid == null) {
|
||||||
String uuidStr = c.getString(1);
|
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
|
|
||||||
// If a field is corrupt or missing, skip the database entry
|
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
|
||||||
details.macAddress == null) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
computerList.add(details);
|
computerList.add(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,49 +142,19 @@ public class ComputerDatabaseManager {
|
|||||||
return computerList;
|
return computerList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails getComputerByName(String name) {
|
public ComputerDetails getComputerByUUID(String uuid) {
|
||||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
if (!c.moveToFirst()) {
|
if (!c.moveToFirst()) {
|
||||||
// No matching computer
|
// No matching computer
|
||||||
c.close();
|
c.close();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
details.name = c.getString(0);
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
|
|
||||||
String uuidStr = c.getString(1);
|
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
// If a critical field is corrupt or missing, delete the database entry
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
if (details.uuid == null) {
|
||||||
|
|
||||||
// If a field is corrupt or missing, delete the database entry
|
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
|
||||||
details.macAddress == null) {
|
|
||||||
deleteComputer(details.name);
|
deleteComputer(details.name);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package com.limelight.computers;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
public interface ComputerManagerListener {
|
public interface ComputerManagerListener {
|
||||||
public void notifyComputerUpdated(ComputerDetails details);
|
void notifyComputerUpdated(ComputerDetails details);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,25 @@ package com.limelight.computers;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.discovery.DiscoveryService;
|
import com.limelight.discovery.DiscoveryService;
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -35,7 +37,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||||
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
private static final int FAST_POLL_TIMEOUT = 500;
|
private static final int FAST_POLL_TIMEOUT = 1000;
|
||||||
private static final int OFFLINE_POLL_TRIES = 5;
|
private static final int OFFLINE_POLL_TRIES = 5;
|
||||||
private static final int INITIAL_POLL_TRIES = 2;
|
private static final int INITIAL_POLL_TRIES = 2;
|
||||||
private static final int EMPTY_LIST_THRESHOLD = 3;
|
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||||
@@ -93,7 +95,6 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
details.state = ComputerDetails.State.OFFLINE;
|
details.state = ComputerDetails.State.OFFLINE;
|
||||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
@@ -104,17 +105,27 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
// If it's online, update our persistent state
|
// If it's online, update our persistent state
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
if (!newPc) {
|
ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
|
||||||
// Check if it's in the database because it could have been
|
|
||||||
// removed after this was issued
|
// Check if it's in the database because it could have been
|
||||||
if (dbManager.getComputerByName(details.name) == null) {
|
// removed after this was issued
|
||||||
// It's gone
|
if (!newPc && existingComputer == null) {
|
||||||
releaseLocalDatabaseReference();
|
// It's gone
|
||||||
return false;
|
releaseLocalDatabaseReference();
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
dbManager.updateComputer(details);
|
// 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
|
// Don't call the listener if this is a failed lookup of a new PC
|
||||||
@@ -132,7 +143,7 @@ public class ComputerManagerService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
int offlineCount = 0;
|
int offlineCount = 0;
|
||||||
while (!isInterrupted() && pollingActive) {
|
while (!isInterrupted() && pollingActive && tuple.thread == this) {
|
||||||
try {
|
try {
|
||||||
// Only allow one request to the machine at a time
|
// Only allow one request to the machine at a time
|
||||||
synchronized (tuple.networkLock) {
|
synchronized (tuple.networkLock) {
|
||||||
@@ -154,7 +165,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
|
t.setName("Polling thread for " + tuple.computer.name);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +186,6 @@ public class ComputerManagerService extends Service {
|
|||||||
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
||||||
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
||||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report this computer initially
|
// Report this computer initially
|
||||||
@@ -210,8 +220,8 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(InetAddress addr) {
|
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(String name) {
|
||||||
@@ -231,7 +241,7 @@ public class ComputerManagerService extends Service {
|
|||||||
return idManager.getUniqueId();
|
return idManager.getUniqueId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails getComputer(UUID uuid) {
|
public ComputerDetails getComputer(String uuid) {
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
if (uuid.equals(tuple.computer.uuid)) {
|
if (uuid.equals(tuple.computer.uuid)) {
|
||||||
@@ -243,7 +253,7 @@ public class ComputerManagerService extends Service {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void invalidateStateForComputer(UUID uuid) {
|
public void invalidateStateForComputer(String uuid) {
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
if (uuid.equals(tuple.computer.uuid)) {
|
if (uuid.equals(tuple.computer.uuid)) {
|
||||||
@@ -251,7 +261,6 @@ public class ComputerManagerService extends Service {
|
|||||||
// from wiping this change out
|
// from wiping this change out
|
||||||
synchronized (tuple.networkLock) {
|
synchronized (tuple.networkLock) {
|
||||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,7 +298,7 @@ public class ComputerManagerService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
// Kick off a serverinfo poll on this machine
|
// Kick off a serverinfo poll on this machine
|
||||||
addComputerBlocking(computer.getAddress());
|
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -310,10 +319,8 @@ public class ComputerManagerService extends Service {
|
|||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
// Check if this is the same computer
|
// Check if this is the same computer
|
||||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||||
// Update details anyway in case this machine has been re-added by IP
|
// Update the saved computer with potentially new details
|
||||||
// after not being reachable by our existing information
|
tuple.computer.update(details);
|
||||||
tuple.computer.localIp = details.localIp;
|
|
||||||
tuple.computer.remoteIp = details.remoteIp;
|
|
||||||
|
|
||||||
// Start a polling thread if polling is active
|
// Start a polling thread if polling is active
|
||||||
if (pollingActive && tuple.thread == null) {
|
if (pollingActive && tuple.thread == null) {
|
||||||
@@ -338,15 +345,43 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(InetAddress addr) {
|
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||||
// Setup a placeholder
|
// Setup a placeholder
|
||||||
ComputerDetails fakeDetails = new ComputerDetails();
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
fakeDetails.localIp = addr;
|
|
||||||
fakeDetails.remoteIp = addr;
|
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
|
// Block while we try to fill the details
|
||||||
try {
|
try {
|
||||||
runPoll(fakeDetails, true, 0);
|
// 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) {
|
} catch (InterruptedException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -360,6 +395,10 @@ public class ComputerManagerService extends Service {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (!manuallyAdded) {
|
||||||
|
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,6 +418,7 @@ public class ComputerManagerService extends Service {
|
|||||||
if (tuple.thread != null) {
|
if (tuple.thread != null) {
|
||||||
// Interrupt the thread on this entry
|
// Interrupt the thread on this entry
|
||||||
tuple.thread.interrupt();
|
tuple.thread.interrupt();
|
||||||
|
tuple.thread = null;
|
||||||
}
|
}
|
||||||
pollingTuples.remove(tuple);
|
pollingTuples.remove(tuple);
|
||||||
break;
|
break;
|
||||||
@@ -404,38 +444,51 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
|
||||||
// Fast poll this address first to determine if we can connect at the TCP layer
|
// Fast poll this address first to determine if we can connect at the TCP layer
|
||||||
if (!fastPollIp(ipAddr)) {
|
if (!fastPollIp(address)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
ComputerDetails newDetails = http.getComputerDetails();
|
ComputerDetails newDetails = http.getComputerDetails();
|
||||||
|
|
||||||
// Check if this is the PC we expected
|
// Check if this is the PC we expected
|
||||||
if (details.uuid != null && newDetails.uuid != null &&
|
if (newDetails.uuid == null) {
|
||||||
!details.uuid.equals(newDetails.uuid)) {
|
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!
|
// We got the wrong PC!
|
||||||
LimeLog.info("Polling returned the wrong PC!");
|
LimeLog.info("Polling returned the wrong PC!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the new active address
|
||||||
|
newDetails.activeAddress = address;
|
||||||
|
|
||||||
return newDetails;
|
return newDetails;
|
||||||
} catch (Exception e) {
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just try to establish a TCP connection to speculatively detect a running
|
// Just try to establish a TCP connection to speculatively detect a running
|
||||||
// GFE server
|
// GFE server
|
||||||
private boolean fastPollIp(InetAddress addr) {
|
private boolean fastPollIp(String address) {
|
||||||
|
if (address == null) {
|
||||||
|
// Don't bother if our address is null
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Socket s = new Socket();
|
Socket s = new Socket();
|
||||||
try {
|
try {
|
||||||
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||||
s.close();
|
s.close();
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -443,11 +496,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
private void startFastPollThread(final String address, final boolean[] info) {
|
||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean pollRes = fastPollIp(addr);
|
boolean pollRes = fastPollIp(address);
|
||||||
|
|
||||||
synchronized (info) {
|
synchronized (info) {
|
||||||
info[0] = true; // Done
|
info[0] = true; // Done
|
||||||
@@ -457,16 +510,18 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Fast Poll - "+addr.getHostAddress());
|
t.setName("Fast Poll - "+address);
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException {
|
||||||
final boolean[] remoteInfo = new boolean[2];
|
final boolean[] remoteInfo = new boolean[2];
|
||||||
final boolean[] localInfo = new boolean[2];
|
final boolean[] localInfo = new boolean[2];
|
||||||
|
final boolean[] manualInfo = new boolean[2];
|
||||||
|
|
||||||
startFastPollThread(local, localInfo);
|
startFastPollThread(localAddress, localInfo);
|
||||||
startFastPollThread(remote, remoteInfo);
|
startFastPollThread(remoteAddress, remoteInfo);
|
||||||
|
startFastPollThread(manualAddress, manualInfo);
|
||||||
|
|
||||||
// Check local first
|
// Check local first
|
||||||
synchronized (localInfo) {
|
synchronized (localInfo) {
|
||||||
@@ -475,125 +530,78 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (localInfo[1]) {
|
if (localInfo[1]) {
|
||||||
return ComputerDetails.Reachability.LOCAL;
|
return localAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now remote
|
// Now manual
|
||||||
|
synchronized (manualInfo) {
|
||||||
|
while (!manualInfo[0]) {
|
||||||
|
manualInfo.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manualInfo[1]) {
|
||||||
|
return manualAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And finally, remote
|
||||||
synchronized (remoteInfo) {
|
synchronized (remoteInfo) {
|
||||||
while (!remoteInfo[0]) {
|
while (!remoteInfo[0]) {
|
||||||
remoteInfo.wait(500);
|
remoteInfo.wait(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteInfo[1]) {
|
if (remoteInfo[1]) {
|
||||||
return ComputerDetails.Reachability.REMOTE;
|
return remoteAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ComputerDetails.Reachability.OFFLINE;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
|
||||||
ComputerDetails polledDetails;
|
|
||||||
ComputerDetails.Reachability reachability;
|
|
||||||
|
|
||||||
// If the local address is routable across the Internet,
|
|
||||||
// always consider this PC remote to be conservative
|
|
||||||
if (details.localIp.equals(details.remoteIp)) {
|
|
||||||
reachability = ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
|
||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
|
||||||
reachability = fastPollPc(details.localIp, details.remoteIp);
|
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
|
||||||
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
|
||||||
|
|
||||||
if (localFirst) {
|
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
|
||||||
}
|
|
||||||
|
|
||||||
InetAddress reachableAddr = null;
|
|
||||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
|
||||||
// Failed, so let's try the fallback
|
|
||||||
if (!localFirst) {
|
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polledDetails != null) {
|
|
||||||
// The fallback poll worked
|
|
||||||
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (polledDetails != null) {
|
|
||||||
reachableAddr = localFirst ? details.localIp : details.remoteIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reachableAddr == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polledDetails.remoteIp.equals(reachableAddr)) {
|
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
|
||||||
else if (polledDetails.localIp.equals(reachableAddr)) {
|
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReachabilityTuple(polledDetails, reachableAddr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||||
ReachabilityTuple initialReachTuple = pollForReachability(details);
|
ComputerDetails polledDetails;
|
||||||
if (initialReachTuple == null) {
|
|
||||||
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
// Try using the active address from fast-poll
|
||||||
// Neither IP address reported in the serverinfo response was the one we used.
|
polledDetails = tryPollIp(details, candidateAddress);
|
||||||
// Poll again to see if we can contact this machine on either of its reported addresses.
|
if (polledDetails == null) {
|
||||||
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
// If that failed, try all unique addresses except what we've
|
||||||
if (confirmationReachTuple == null) {
|
// already tried
|
||||||
// Neither of those seem to work, so we'll hold onto the address that did work
|
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||||
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
|
uniqueAddresses.add(details.localAddress);
|
||||||
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
uniqueAddresses.add(details.remoteAddress);
|
||||||
}
|
uniqueAddresses.add(details.manualAddress);
|
||||||
else {
|
for (String addr : uniqueAddresses) {
|
||||||
// We got it on one of the returned addresses; replace the original reach tuple
|
if (addr == null || addr.equals(candidateAddress)) {
|
||||||
// with the new one
|
continue;
|
||||||
initialReachTuple = confirmationReachTuple;
|
}
|
||||||
|
polledDetails = tryPollIp(details, addr);
|
||||||
|
if (polledDetails != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the old MAC address
|
if (polledDetails != null) {
|
||||||
String savedMacAddress = details.macAddress;
|
details.update(polledDetails);
|
||||||
|
return true;
|
||||||
// If we got here, it's reachable
|
}
|
||||||
details.update(initialReachTuple.computer);
|
else {
|
||||||
|
return false;
|
||||||
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
|
||||||
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
|
||||||
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
|
||||||
details.macAddress = savedMacAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -694,10 +702,9 @@ public class ComputerManagerService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
int emptyAppListResponses = 0;
|
int emptyAppListResponses = 0;
|
||||||
do {
|
do {
|
||||||
InetAddress selectedAddr;
|
// Can't poll if it's not online or paired
|
||||||
|
if (computer.state != ComputerDetails.State.ONLINE ||
|
||||||
// Can't poll if it's not online
|
computer.pairState != PairingManager.PairState.PAIRED) {
|
||||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.notifyComputerUpdated(computer);
|
listener.notifyComputerUpdated(computer);
|
||||||
}
|
}
|
||||||
@@ -709,19 +716,12 @@ public class ComputerManagerService extends Service {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
selectedAddr = computer.localIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectedAddr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
|
||||||
|
|
||||||
PollingTuple tuple = getPollingTuple(computer);
|
PollingTuple tuple = getPollingTuple(computer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||||
|
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
String appList;
|
String appList;
|
||||||
if (tuple != null) {
|
if (tuple != null) {
|
||||||
// If we're polling this machine too, grab the network lock
|
// If we're polling this machine too, grab the network lock
|
||||||
@@ -744,12 +744,12 @@ public class ComputerManagerService extends Service {
|
|||||||
// in a row, we'll go ahead and believe it.
|
// in a row, we'll go ahead and believe it.
|
||||||
emptyAppListResponses++;
|
emptyAppListResponses++;
|
||||||
}
|
}
|
||||||
if (appList != null && !appList.isEmpty() &&
|
if (!appList.isEmpty() &&
|
||||||
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||||
// Open the cache file
|
// Open the cache file
|
||||||
OutputStream cacheOut = null;
|
OutputStream cacheOut = null;
|
||||||
try {
|
try {
|
||||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
|
||||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -776,7 +776,7 @@ public class ComputerManagerService extends Service {
|
|||||||
listener.notifyComputerUpdated(computer);
|
listener.notifyComputerUpdated(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (appList == null || appList.isEmpty()) {
|
else if (appList.isEmpty()) {
|
||||||
LimeLog.warning("Null app list received from "+computer.uuid);
|
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -787,6 +787,7 @@ public class ComputerManagerService extends Service {
|
|||||||
} while (waitPollingDelay());
|
} while (waitPollingDelay());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
thread.setName("App list polling thread for " + computer.name);
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,10 +817,10 @@ class PollingTuple {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ReachabilityTuple {
|
class ReachabilityTuple {
|
||||||
public final InetAddress reachableAddress;
|
public final String reachableAddress;
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
|
|
||||||
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
|
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.reachableAddress = reachableAddress;
|
this.reachableAddress = reachableAddress;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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,7 +1,6 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -46,13 +45,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
}
|
}
|
||||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||||
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = (int) scalingDivisor;
|
|
||||||
|
|
||||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||||
new NetworkAssetLoader(context, uniqueId),
|
new NetworkAssetLoader(context, uniqueId),
|
||||||
new MemoryAssetLoader(),
|
new MemoryAssetLoader(),
|
||||||
new DiskAssetLoader(context.getCacheDir()));
|
new DiskAssetLoader(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
public void cancelQueuedOperations() {
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
convertView = inflater.inflate(layoutId, viewGroup, false);
|
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
ImageView imgView = convertView.findViewById(R.id.grid_image);
|
||||||
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
|
||||||
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
TextView txtView = convertView.findViewById(R.id.grid_text);
|
||||||
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner);
|
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
||||||
|
|
||||||
if (imgView != null) {
|
if (imgView != null) {
|
||||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.widget.TextView;
|
|||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -46,7 +47,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
imgView.setAlpha(0.4f);
|
imgView.setAlpha(0.4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
|
||||||
prgView.setVisibility(View.VISIBLE);
|
prgView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -77,6 +78,14 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
overlayView.setAlpha(0.4f);
|
overlayView.setAlpha(0.4f);
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.limelight.grid.assets;
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.ImageDecoder;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
@@ -13,45 +17,117 @@ import java.io.OutputStream;
|
|||||||
|
|
||||||
public class DiskAssetLoader {
|
public class DiskAssetLoader {
|
||||||
// 5 MB
|
// 5 MB
|
||||||
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
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;
|
private final File cacheDir;
|
||||||
|
|
||||||
public DiskAssetLoader(File cacheDir) {
|
public DiskAssetLoader(Context context) {
|
||||||
this.cacheDir = cacheDir;
|
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) {
|
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
||||||
InputStream in = null;
|
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
||||||
Bitmap bmp = null;
|
// Raw height and width of image
|
||||||
try {
|
final int height = options.outHeight;
|
||||||
// Make sure the cached asset doesn't exceed the maximum size
|
final int width = options.outWidth;
|
||||||
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
int inSampleSize = 1;
|
||||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
|
||||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
if (height > reqHeight || width > reqWidth) {
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = sampleSize;
|
final int halfHeight = height / 2;
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
final int halfWidth = width / 2;
|
||||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
|
||||||
} catch (IOException ignored) {
|
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
|
||||||
} finally {
|
// height and width larger than the requested height and width.
|
||||||
if (in != null) {
|
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||||
try {
|
inSampleSize *= 2;
|
||||||
in.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bmp != null) {
|
return inSampleSize;
|
||||||
LimeLog.info("Disk cache hit for tuple: "+tuple);
|
}
|
||||||
|
|
||||||
|
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;
|
return bmp;
|
||||||
@@ -61,7 +137,7 @@ public class DiskAssetLoader {
|
|||||||
OutputStream out = null;
|
OutputStream out = null;
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||||
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||||
success = true;
|
success = true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -75,7 +151,7 @@ public class DiskAssetLoader {
|
|||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
||||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class MemoryAssetLoader {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import android.content.Context;
|
|||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.InetAddress;
|
|
||||||
|
|
||||||
public class NetworkAssetLoader {
|
public class NetworkAssetLoader {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@@ -21,10 +20,10 @@ public class NetworkAssetLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
|
||||||
|
|
||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
try {
|
try {
|
||||||
|
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
|
||||||
|
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
|
||||||
in = http.getBoxArt(tuple.app);
|
in = http.getBoxArt(tuple.app);
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
@@ -37,13 +36,4 @@ public class NetworkAssetLoader {
|
|||||||
|
|
||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
return computer.localIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return computer.remoteIp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.net.InterfaceAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.SocketException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Locale;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -17,10 +21,10 @@ import android.content.ComponentName;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -43,42 +47,90 @@ public class AddComputerManually extends Activity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
private void doAddPc(String host) {
|
||||||
String msg;
|
boolean wrongSiteLocal = false;
|
||||||
boolean finish = false;
|
boolean success;
|
||||||
|
|
||||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||||
getResources().getString(R.string.msg_add_pc), false);
|
getResources().getString(R.string.msg_add_pc), false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InetAddress addr = InetAddress.getByName(host);
|
success = managerBinder.addComputerBlocking(host, true);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
if (!managerBinder.addComputerBlocking(addr)){
|
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||||
msg = getResources().getString(R.string.addpc_fail);
|
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
||||||
}
|
e.printStackTrace();
|
||||||
else {
|
success = false;
|
||||||
msg = getResources().getString(R.string.addpc_success);
|
}
|
||||||
finish = true;
|
if (!success){
|
||||||
}
|
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
msg = getResources().getString(R.string.addpc_unknown_host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
|
||||||
final boolean toastFinish = finish;
|
if (wrongSiteLocal) {
|
||||||
final String toastMsg = msg;
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
}
|
||||||
@Override
|
else if (!success) {
|
||||||
public void run() {
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
|
||||||
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
}
|
||||||
|
else {
|
||||||
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_success), Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
if (toastFinish && !isFinishing()) {
|
if (!isFinishing()) {
|
||||||
// Close the activity
|
// Close the activity
|
||||||
AddComputerManually.this.finish();
|
AddComputerManually.this.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startAddThread() {
|
private void startAddThread() {
|
||||||
@@ -142,7 +194,7 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
this.hostText = findViewById(R.id.hostTextView);
|
||||||
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -151,12 +203,7 @@ public class AddComputerManually extends Activity {
|
|||||||
(keyEvent != null &&
|
(keyEvent != null &&
|
||||||
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||||
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
||||||
if (hostText.getText().length() == 0) {
|
return handleDoneEvent();
|
||||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
computersToAdd.add(hostText.getText().toString().trim());
|
|
||||||
}
|
}
|
||||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
||||||
// This is how the Fire TV dismisses the keyboard
|
// This is how the Fire TV dismisses the keyboard
|
||||||
@@ -169,8 +216,28 @@ public class AddComputerManually extends Activity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.addPcButton).setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
handleDoneEvent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Bind to the ComputerManager service
|
// Bind to the ComputerManager service
|
||||||
bindService(new Intent(AddComputerManually.this,
|
bindService(new Intent(AddComputerManually.this,
|
||||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,17 @@ package com.limelight.preferences;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
public class PreferenceConfiguration {
|
||||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
|
||||||
|
|
||||||
|
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 STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||||
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
||||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||||
@@ -23,16 +27,19 @@ public class PreferenceConfiguration {
|
|||||||
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
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 VIDEO_FORMAT_PREF_STRING = "video_format";
|
||||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
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";
|
||||||
|
|
||||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
static final String DEFAULT_RESOLUTION = "720p";
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
static final String DEFAULT_FPS = "60";
|
||||||
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
|
||||||
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
|
||||||
private static final int BITRATE_DEFAULT_4K_30 = 40;
|
|
||||||
private static final int BITRATE_DEFAULT_4K_60 = 80;
|
|
||||||
|
|
||||||
private static final String DEFAULT_RES_FPS = "720p60";
|
|
||||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
|
||||||
private static final boolean DEFAULT_STRETCH = false;
|
private static final boolean DEFAULT_STRETCH = false;
|
||||||
private static final boolean DEFAULT_SOPS = true;
|
private static final boolean DEFAULT_SOPS = true;
|
||||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||||
@@ -45,6 +52,16 @@ public class PreferenceConfiguration {
|
|||||||
private static final boolean DEFAULT_USB_DRIVER = true;
|
private static final boolean DEFAULT_USB_DRIVER = true;
|
||||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||||
|
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
||||||
|
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
||||||
|
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||||
|
private static final boolean DEFAULT_ENABLE_PIP = false;
|
||||||
|
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
||||||
|
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
||||||
|
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
||||||
|
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
||||||
|
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
||||||
|
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
||||||
|
|
||||||
public static final int FORCE_H265_ON = -1;
|
public static final int FORCE_H265_ON = -1;
|
||||||
public static final int AUTOSELECT_H265 = 0;
|
public static final int AUTOSELECT_H265 = 0;
|
||||||
@@ -58,29 +75,103 @@ public class PreferenceConfiguration {
|
|||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||||
public boolean onscreenController;
|
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;
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
private static int getHeightFromResolutionString(String resString) {
|
||||||
if (resFpsString.equals("720p30")) {
|
if (resString.equalsIgnoreCase("360p")) {
|
||||||
return BITRATE_DEFAULT_720_30;
|
return 360;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("720p60")) {
|
else if (resString.equalsIgnoreCase("480p")) {
|
||||||
return BITRATE_DEFAULT_720_60;
|
return 480;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("1080p30")) {
|
else if (resString.equalsIgnoreCase("720p")) {
|
||||||
return BITRATE_DEFAULT_1080_30;
|
return 720;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("1080p60")) {
|
else if (resString.equalsIgnoreCase("1080p")) {
|
||||||
return BITRATE_DEFAULT_1080_60;
|
return 1080;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("4K30")) {
|
else if (resString.equalsIgnoreCase("1440p")) {
|
||||||
return BITRATE_DEFAULT_4K_30;
|
return 1440;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("4K60")) {
|
else if (resString.equalsIgnoreCase("4K")) {
|
||||||
return BITRATE_DEFAULT_4K_60;
|
return 2160;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should never get here
|
// Should be unreachable
|
||||||
return DEFAULT_BITRATE;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +197,9 @@ public class PreferenceConfiguration {
|
|||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
return getDefaultBitrate(
|
||||||
|
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
|
||||||
|
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getVideoFormatValue(Context context) {
|
private static int getVideoFormatValue(Context context) {
|
||||||
@@ -128,47 +221,93 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void resetStreamingSettings(Context context) {
|
||||||
|
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
prefs.edit()
|
||||||
|
.remove(BITRATE_PREF_STRING)
|
||||||
|
.remove(BITRATE_PREF_OLD_STRING)
|
||||||
|
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||||
|
.remove(RESOLUTION_PREF_STRING)
|
||||||
|
.remove(FPS_PREF_STRING)
|
||||||
|
.remove(VIDEO_FORMAT_PREF_STRING)
|
||||||
|
.remove(ENABLE_HDR_PREF_STRING)
|
||||||
|
.remove(UNLOCK_FPS_STRING)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
public static PreferenceConfiguration readPreferences(Context context) {
|
public static PreferenceConfiguration readPreferences(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||||
|
|
||||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
// Migrate legacy preferences to the new locations
|
||||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
||||||
if (str.equals("720p30")) {
|
if (str != null) {
|
||||||
config.width = 1280;
|
if (str.equals("360p30")) {
|
||||||
config.height = 720;
|
config.width = 640;
|
||||||
config.fps = 30;
|
config.height = 360;
|
||||||
}
|
config.fps = 30;
|
||||||
else if (str.equals("720p60")) {
|
}
|
||||||
config.width = 1280;
|
else if (str.equals("360p60")) {
|
||||||
config.height = 720;
|
config.width = 640;
|
||||||
config.fps = 60;
|
config.height = 360;
|
||||||
}
|
config.fps = 60;
|
||||||
else if (str.equals("1080p30")) {
|
}
|
||||||
config.width = 1920;
|
else if (str.equals("720p30")) {
|
||||||
config.height = 1080;
|
config.width = 1280;
|
||||||
config.fps = 30;
|
config.height = 720;
|
||||||
}
|
config.fps = 30;
|
||||||
else if (str.equals("1080p60")) {
|
}
|
||||||
config.width = 1920;
|
else if (str.equals("720p60")) {
|
||||||
config.height = 1080;
|
config.width = 1280;
|
||||||
config.fps = 60;
|
config.height = 720;
|
||||||
}
|
config.fps = 60;
|
||||||
else if (str.equals("4K30")) {
|
}
|
||||||
config.width = 3840;
|
else if (str.equals("1080p30")) {
|
||||||
config.height = 2160;
|
config.width = 1920;
|
||||||
config.fps = 30;
|
config.height = 1080;
|
||||||
}
|
config.fps = 30;
|
||||||
else if (str.equals("4K60")) {
|
}
|
||||||
config.width = 3840;
|
else if (str.equals("1080p60")) {
|
||||||
config.height = 2160;
|
config.width = 1920;
|
||||||
config.fps = 60;
|
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 {
|
else {
|
||||||
// Should never get here
|
// Use the new preference location
|
||||||
config.width = 1280;
|
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||||
config.height = 720;
|
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||||
config.fps = 60;
|
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.videoFormat = getVideoFormatValue(context);
|
||||||
@@ -188,6 +327,16 @@ public class PreferenceConfiguration {
|
|||||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
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.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
||||||
|
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
||||||
|
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
||||||
|
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
||||||
|
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
||||||
|
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
||||||
|
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
||||||
|
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
||||||
|
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
||||||
|
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
||||||
|
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import android.widget.TextView;
|
|||||||
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
||||||
public class SeekBarPreference extends DialogPreference
|
public class SeekBarPreference extends DialogPreference
|
||||||
{
|
{
|
||||||
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
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 SeekBar seekBar;
|
||||||
private TextView valueText;
|
private TextView valueText;
|
||||||
@@ -27,6 +28,7 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
private final int defaultValue;
|
private final int defaultValue;
|
||||||
private final int maxValue;
|
private final int maxValue;
|
||||||
private final int minValue;
|
private final int minValue;
|
||||||
|
private final int stepSize;
|
||||||
private int currentValue;
|
private int currentValue;
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
@@ -34,27 +36,28 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
// Read the message from XML
|
// Read the message from XML
|
||||||
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
|
int dialogMessageId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "dialogMessage", 0);
|
||||||
if (dialogMessageId == 0) {
|
if (dialogMessageId == 0) {
|
||||||
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
|
dialogMessage = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "dialogMessage");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dialogMessage = context.getString(dialogMessageId);
|
dialogMessage = context.getString(dialogMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the suffix for the number displayed in the dialog
|
// Get the suffix for the number displayed in the dialog
|
||||||
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
|
int suffixId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "text", 0);
|
||||||
if (suffixId == 0) {
|
if (suffixId == 0) {
|
||||||
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
|
suffix = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "text");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
suffix = context.getString(suffixId);
|
suffix = context.getString(suffixId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get default, min, and max seekbar values
|
// Get default, min, and max seekbar values
|
||||||
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
defaultValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||||
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
||||||
minValue = 1;
|
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
||||||
|
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -89,6 +92,12 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int roundedValue = ((value + (stepSize - 1))/stepSize)*stepSize;
|
||||||
|
if (roundedValue != value) {
|
||||||
|
seekBar.setProgress(roundedValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String t = String.valueOf(value);
|
String t = String.valueOf(value);
|
||||||
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,35 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceCategory;
|
import android.preference.PreferenceCategory;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.util.Range;
|
||||||
|
import android.view.Display;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
|
void reloadSettings() {
|
||||||
|
getFragmentManager().beginTransaction().replace(
|
||||||
|
R.id.stream_settings, new SettingsFragment()
|
||||||
|
).commit();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -29,9 +40,7 @@ public class StreamSettings extends Activity {
|
|||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
|
|
||||||
setContentView(R.layout.activity_stream_settings);
|
setContentView(R.layout.activity_stream_settings);
|
||||||
getFragmentManager().beginTransaction().replace(
|
reloadSettings();
|
||||||
R.id.stream_settings, new SettingsFragment()
|
|
||||||
).commit();
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
}
|
}
|
||||||
@@ -53,10 +62,68 @@ public class StreamSettings extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
|
|
||||||
|
private void setValue(String preferenceKey, String value) {
|
||||||
|
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||||
|
|
||||||
|
pref.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||||
|
int matchingCount = 0;
|
||||||
|
|
||||||
|
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||||
|
|
||||||
|
// Count the number of matching entries we'll be removing
|
||||||
|
for (CharSequence seq : pref.getEntryValues()) {
|
||||||
|
if (seq.toString().equalsIgnoreCase(value)) {
|
||||||
|
matchingCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new arrays
|
||||||
|
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
|
||||||
|
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
||||||
|
int outIndex = 0;
|
||||||
|
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
||||||
|
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
||||||
|
// Skip matching values
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries[outIndex] = pref.getEntries()[i];
|
||||||
|
entryValues[outIndex] = pref.getEntryValues()[i];
|
||||||
|
outIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pref.getValue().equalsIgnoreCase(value)) {
|
||||||
|
onMatched.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the preference with the new list
|
||||||
|
pref.setEntries(entries);
|
||||||
|
pref.setEntryValues(entryValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
||||||
|
if (res == null) {
|
||||||
|
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||||
|
}
|
||||||
|
if (fps == null) {
|
||||||
|
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit()
|
||||||
|
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||||
|
PreferenceConfiguration.getDefaultBitrate(res, fps))
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
PreferenceScreen screen = getPreferenceScreen();
|
PreferenceScreen screen = getPreferenceScreen();
|
||||||
|
|
||||||
@@ -68,20 +135,247 @@ public class StreamSettings extends Activity {
|
|||||||
screen.removePreference(category);
|
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
|
// Add a listener to the FPS and resolution preference
|
||||||
// so the bitrate can be auto-adjusted
|
// so the bitrate can be auto-adjusted
|
||||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
String valueStr = (String) newValue;
|
String valueStr = (String) newValue;
|
||||||
|
|
||||||
// Write the new bitrate value
|
// Write the new bitrate value
|
||||||
prefs.edit()
|
resetBitrateToDefault(prefs, valueStr, null);
|
||||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
|
||||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
// Allow the original preference change to take place
|
||||||
.apply();
|
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
|
// Allow the original preference change to take place
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package com.limelight.ui;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
|
|
||||||
public interface AdapterFragmentCallbacks {
|
public interface AdapterFragmentCallbacks {
|
||||||
public int getAdapterFragmentLayoutId();
|
int getAdapterFragmentLayoutId();
|
||||||
public void receiveAbsListView(AbsListView gridView);
|
void receiveAbsListView(AbsListView gridView);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.limelight.ui;
|
package com.limelight.ui;
|
||||||
|
|
||||||
public interface GameGestures {
|
public interface GameGestures {
|
||||||
public void showKeyboard();
|
void showKeyboard();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,21 @@ package com.limelight.ui;
|
|||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
|
|
||||||
public class StreamView extends SurfaceView {
|
public class StreamView extends SurfaceView {
|
||||||
private double desiredAspectRatio;
|
private double desiredAspectRatio;
|
||||||
|
private InputCallbacks inputCallbacks;
|
||||||
|
|
||||||
public void setDesiredAspectRatio(double aspectRatio) {
|
public void setDesiredAspectRatio(double aspectRatio) {
|
||||||
this.desiredAspectRatio = aspectRatio;
|
this.desiredAspectRatio = aspectRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setInputCallbacks(InputCallbacks callbacks) {
|
||||||
|
this.inputCallbacks = callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
public StreamView(Context context) {
|
public StreamView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
@@ -52,4 +58,29 @@ public class StreamView extends SurfaceView {
|
|||||||
|
|
||||||
setMeasuredDimension(measuredWidth, measuredHeight);
|
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||||
|
// This callbacks allows us to override dumb IME behavior like when
|
||||||
|
// Samsung's default keyboard consumes Shift+Space.
|
||||||
|
if (inputCallbacks != null) {
|
||||||
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||||
|
if (inputCallbacks.handleKeyDown(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||||
|
if (inputCallbacks.handleKeyUp(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onKeyPreIme(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface InputCallbacks {
|
||||||
|
boolean handleKeyUp(KeyEvent event);
|
||||||
|
boolean handleKeyDown(KeyEvent event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.io.OutputStream;
|
|||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
||||||
public class CacheHelper {
|
public class CacheHelper {
|
||||||
private static File openPath(boolean createPath, File root, String... path) {
|
public static File openPath(boolean createPath, File root, String... path) {
|
||||||
File f = root;
|
File f = root;
|
||||||
for (int i = 0; i < path.length; i++) {
|
for (int i = 0; i < path.length; i++) {
|
||||||
String component = path[i];
|
String component = path[i];
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ public class Dialog implements Runnable {
|
|||||||
private final String title;
|
private final String title;
|
||||||
private final String message;
|
private final String message;
|
||||||
private final Activity activity;
|
private final Activity activity;
|
||||||
private final boolean endAfterDismiss;
|
private final Runnable runOnDismiss;
|
||||||
|
|
||||||
private AlertDialog alert;
|
private AlertDialog alert;
|
||||||
|
|
||||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
||||||
|
|
||||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
private Dialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.endAfterDismiss = endAfterDismiss;
|
this.runOnDismiss = runOnDismiss;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs()
|
||||||
@@ -40,9 +40,21 @@ public class Dialog implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
public static void displayDialog(final Activity activity, String title, String message, final boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
activity.runOnUiThread(new Dialog(activity, title, message, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (endAfterDismiss) {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void displayDialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
||||||
|
{
|
||||||
|
activity.runOnUiThread(new Dialog(activity, title, message, runOnDismiss));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -65,9 +77,7 @@ public class Dialog implements Runnable {
|
|||||||
alert.dismiss();
|
alert.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endAfterDismiss) {
|
runOnDismiss.run();
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
||||||
@@ -77,9 +87,7 @@ public class Dialog implements Runnable {
|
|||||||
alert.dismiss();
|
alert.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endAfterDismiss) {
|
runOnDismiss.run();
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
HelpLauncher.launchTroubleshooting(activity);
|
HelpLauncher.launchTroubleshooting(activity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,43 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.limelight.HelpActivity;
|
import com.limelight.HelpActivity;
|
||||||
|
|
||||||
public class HelpLauncher {
|
public class HelpLauncher {
|
||||||
|
|
||||||
|
private static boolean isKnownBrowser(Context context, Intent i) {
|
||||||
|
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
|
||||||
|
if (resolvedActivity == null) {
|
||||||
|
// No browser
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = resolvedActivity.activityInfo.name;
|
||||||
|
if (name == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
name = name.toLowerCase();
|
||||||
|
return name.contains("chrome") || name.contains("firefox");
|
||||||
|
}
|
||||||
|
|
||||||
private static void launchUrl(Context context, String url) {
|
private static void launchUrl(Context context, String url) {
|
||||||
// Try to launch the default browser
|
// Try to launch the default browser
|
||||||
try {
|
try {
|
||||||
// Fire TV devices will lie and say they do have a browser
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
// even though the OS just shows an error dialog if we
|
i.setData(Uri.parse(url));
|
||||||
// try to use it.
|
|
||||||
if (!"Amazon".equalsIgnoreCase(Build.MANUFACTURER)) {
|
// Several Android TV devices will lie and say they do have a browser
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
// even though the OS just shows an error dialog if we try to use it. We need to
|
||||||
i.setData(Uri.parse(url));
|
// be a bit more clever on these devices and detect if the browser is a legitimate
|
||||||
|
// browser or just a fake error message activity.
|
||||||
|
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
|
||||||
|
isKnownBrowser(context, i)) {
|
||||||
context.startActivity(i);
|
context.startActivity(i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,39 +13,50 @@ import com.limelight.nvstream.http.GfeHttpResponseException;
|
|||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.net.InetAddress;
|
import java.io.IOException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
|
||||||
public class ServerHelper {
|
public class ServerHelper {
|
||||||
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
return computer.activeAddress;
|
||||||
computer.localIp : computer.remoteIp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
Intent intent = new Intent(parent, Game.class);
|
Intent intent = new Intent(parent, Game.class);
|
||||||
intent.putExtra(Game.EXTRA_HOST,
|
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
|
||||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
|
||||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
|
||||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid);
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
|
||||||
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
|
|
||||||
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||||
|
try {
|
||||||
|
if (computer.serverCert != null) {
|
||||||
|
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
|
||||||
|
}
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
|
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||||
|
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||||
|
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
public static void doQuit(final Activity parent,
|
||||||
final InetAddress address,
|
final ComputerDetails computer,
|
||||||
final NvApp app,
|
final NvApp app,
|
||||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
final Runnable onComplete) {
|
final Runnable onComplete) {
|
||||||
@@ -56,8 +67,8 @@ public class ServerHelper {
|
|||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
httpConn = new NvHTTP(address,
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
|
||||||
if (httpConn.quitApp()) {
|
if (httpConn.quitApp()) {
|
||||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||||
} else {
|
} else {
|
||||||
@@ -76,8 +87,9 @@ public class ServerHelper {
|
|||||||
message = parent.getResources().getString(R.string.error_unknown_host);
|
message = parent.getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = parent.getResources().getString(R.string.error_404);
|
message = parent.getResources().getString(R.string.error_404);
|
||||||
} catch (Exception e) {
|
} catch (IOException | XmlPullParserException e) {
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
if (onComplete != null) {
|
if (onComplete != null) {
|
||||||
onComplete.run();
|
onComplete.run();
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ShortcutInfo;
|
import android.content.pm.ShortcutInfo;
|
||||||
import android.content.pm.ShortcutManager;
|
import android.content.pm.ShortcutManager;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.Icon;
|
import android.graphics.drawable.Icon;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.AppView;
|
import com.limelight.AppView;
|
||||||
import com.limelight.AppViewShortcutTrampoline;
|
import com.limelight.ShortcutTrampoline;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -80,8 +82,7 @@ public class ShortcutHelper {
|
|||||||
|
|
||||||
public void reportShortcutUsed(String id) {
|
public void reportShortcutUsed(String id) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
ShortcutInfo sinfo = getInfoForId(id);
|
if (getInfoForId(id) != null) {
|
||||||
if (sinfo != null) {
|
|
||||||
sm.reportShortcutUsed(id);
|
sm.reportShortcutUsed(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +90,7 @@ public class ShortcutHelper {
|
|||||||
|
|
||||||
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
Intent i = new Intent(context, AppViewShortcutTrampoline.class);
|
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||||
i.putExtra(AppView.NAME_EXTRA, computerName);
|
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||||
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||||
i.setAction(Intent.ACTION_DEFAULT);
|
i.setAction(Intent.ACTION_DEFAULT);
|
||||||
@@ -124,13 +125,45 @@ public class ShortcutHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
||||||
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
|
createAppViewShortcut(id, details.name, details.uuid, forceAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
|
||||||
|
if (sm.isRequestPinShortcutSupported()) {
|
||||||
|
Icon appIcon;
|
||||||
|
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||||
|
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||||
|
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||||
|
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
|
||||||
|
i.setAction(Intent.ACTION_DEFAULT);
|
||||||
|
|
||||||
|
if (iconBits != null) {
|
||||||
|
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
|
||||||
|
} else {
|
||||||
|
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
|
||||||
|
.setIntent(i)
|
||||||
|
.setShortLabel(appName + " (" + computerName + ")")
|
||||||
|
.setIcon(appIcon)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return sm.requestPinShortcut(sInfo, null);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
|
||||||
|
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid, app.getAppName(), Integer.valueOf(app.getAppId()).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disableShortcut(String id, CharSequence reason) {
|
public void disableShortcut(String id, CharSequence reason) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
ShortcutInfo sinfo = getInfoForId(id);
|
if (getInfoForId(id) != null) {
|
||||||
if (sinfo != null) {
|
|
||||||
sm.disableShortcuts(Collections.singletonList(id), reason);
|
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import android.app.AlertDialog;
|
|||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.os.Build;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -56,6 +60,54 @@ public class UiHelper {
|
|||||||
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
// Allow this non-streaming activity to layout under notches.
|
||||||
|
//
|
||||||
|
// We should NOT do this for the Game activity unless
|
||||||
|
// the user specifically opts in, because it can obscure
|
||||||
|
// parts of the streaming surface.
|
||||||
|
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showDecoderCrashDialog(Activity activity) {
|
||||||
|
final SharedPreferences prefs = activity.getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
final int crashCount = prefs.getInt("CrashCount", 0);
|
||||||
|
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
|
||||||
|
|
||||||
|
// Remember the last crash count we notified at, so we don't
|
||||||
|
// display the crash dialog every time the app is started until
|
||||||
|
// they stream again
|
||||||
|
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
|
||||||
|
if (crashCount % 3 == 0) {
|
||||||
|
// At 3 consecutive crashes, we'll forcefully reset their settings
|
||||||
|
PreferenceConfiguration.resetStreamingSettings(activity);
|
||||||
|
Dialog.displayDialog(activity,
|
||||||
|
activity.getResources().getString(R.string.title_decoding_reset),
|
||||||
|
activity.getResources().getString(R.string.message_decoding_reset),
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Mark notification as acknowledged on dismissal
|
||||||
|
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Dialog.displayDialog(activity,
|
||||||
|
activity.getResources().getString(R.string.title_decoding_error),
|
||||||
|
activity.getResources().getString(R.string.message_decoding_error),
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Mark notification as acknowledged on dismissal
|
||||||
|
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
||||||
@@ -84,4 +136,32 @@ public class UiHelper {
|
|||||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void displayDeletePcConfirmationDialog(Activity parent, ComputerDetails computer, final Runnable onYes, final Runnable onNo) {
|
||||||
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which){
|
||||||
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
|
if (onYes != null) {
|
||||||
|
onYes.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DialogInterface.BUTTON_NEGATIVE:
|
||||||
|
if (onNo != null) {
|
||||||
|
onNo.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||||
|
builder.setMessage(parent.getResources().getString(R.string.delete_pc_msg))
|
||||||
|
.setTitle(computer.name)
|
||||||
|
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
||||||
|
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
# Application.mk for Limelight
|
# Application.mk for Moonlight
|
||||||
|
|
||||||
# Our minimum version is Android 4.1
|
# Our minimum version is Android 4.1
|
||||||
APP_PLATFORM := android-16
|
APP_PLATFORM := android-16
|
||||||
|
|
||||||
# Support all modern ABIs
|
|
||||||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips mips64
|
|
||||||
|
|
||||||
# We want an optimized build
|
|
||||||
APP_OPTIM := release
|
|
||||||
|
|||||||
@@ -5,28 +5,32 @@ include $(call all-subdir-makefiles)
|
|||||||
|
|
||||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
# Only build evdev_reader for the rooted APK flavor
|
||||||
LOCAL_MODULE := evdev_reader
|
ifeq (root,$(PRODUCT_FLAVOR))
|
||||||
LOCAL_SRC_FILES := evdev_reader.c
|
include $(CLEAR_VARS)
|
||||||
LOCAL_LDLIBS := -llog
|
LOCAL_MODULE := evdev_reader
|
||||||
|
LOCAL_SRC_FILES := evdev_reader.c
|
||||||
|
LOCAL_LDLIBS := -llog
|
||||||
|
|
||||||
|
|
||||||
# This next portion of the makefile is mostly copied from build-executable.mk but
|
# This next portion of the makefile is mostly copied from build-executable.mk but
|
||||||
# creates a binary with the libXXX.so form so the APK will install and drop
|
# creates a binary with the libXXX.so form so the APK will install and drop
|
||||||
# the binary correctly.
|
# the binary correctly.
|
||||||
|
|
||||||
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
||||||
LOCAL_MAKEFILE := $(local-makefile)
|
LOCAL_MAKEFILE := $(local-makefile)
|
||||||
|
|
||||||
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
||||||
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
||||||
$(call check-LOCAL_MODULE_FILENAME)
|
$(call check-LOCAL_MODULE_FILENAME)
|
||||||
|
|
||||||
# we are building target objects
|
# we are building target objects
|
||||||
my := TARGET_
|
my := TARGET_
|
||||||
|
|
||||||
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
||||||
$(call handle-module-built)
|
$(call handle-module-built)
|
||||||
|
|
||||||
|
LOCAL_MODULE_CLASS := EXECUTABLE
|
||||||
|
include $(BUILD_SYSTEM)/build-module.mk
|
||||||
|
endif
|
||||||
|
|
||||||
LOCAL_MODULE_CLASS := EXECUTABLE
|
|
||||||
include $(BUILD_SYSTEM)/build-module.mk
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
</vector>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
tools:context=".AddComputerManually" >
|
tools:context=".preferences.AddComputerManually" >
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manuallyAddPcText"
|
android:id="@+id/manuallyAddPcText"
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/manuallyAddPcText"
|
android:layout_below="@+id/manuallyAddPcText"
|
||||||
|
android:layout_toLeftOf="@+id/addPcButton"
|
||||||
|
android:layout_toStartOf="@+id/addPcButton"
|
||||||
android:layout_marginTop="25dp"
|
android:layout_marginTop="25dp"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentLeft="true"
|
||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:inputType="textNoSuggestions"
|
android:inputType="textNoSuggestions"
|
||||||
@@ -34,5 +34,15 @@
|
|||||||
|
|
||||||
<requestFocus />
|
<requestFocus />
|
||||||
</EditText>
|
</EditText>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/addPcButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="25dp"
|
||||||
|
android:layout_below="@+id/manuallyAddPcText"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:text="@android:string/ok"/>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|||||||
@@ -10,4 +10,16 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notificationOverlay"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_gravity="right"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:gravity="right"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</merge>
|
</merge>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/activity_help"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
|
||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
|
||||||
tools:context="com.limelight.HelpActivity">
|
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_alignParentStart="true" />
|
|
||||||
</RelativeLayout>
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<!-- Portrait orientation only -->
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
android:id="@+id/stream_settings"
|
android:id="@+id/stream_settings"
|
||||||
tools:context=".StreamSettings">
|
tools:context=".preferences.StreamSettings">
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_pc_scut_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_pc_scut_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 715 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 471 B |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 816 B |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |