Compare commits
296 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7a9a37a0e | |||
| 836b9240de | |||
| bdac2df4b9 | |||
| 57b507ad50 | |||
| 35201b69f6 | |||
| 0d138c26e9 | |||
| b4a7393dca | |||
| d86092df1a | |||
| b392d7f8e3 | |||
| 7cc7953879 | |||
| 7b26852a1f | |||
| f26b384697 | |||
| ab0531aa76 | |||
| 6873720d81 | |||
| 1e30c4a219 | |||
| 0a0e3ff970 | |||
| 5c42fd86a6 | |||
| 16cc829906 | |||
| 829e7cf33c | |||
| 02bfa90417 | |||
| 0b2466cf26 | |||
| 9d8df04c5c | |||
| 34a1697d50 | |||
| 17cf711c3d | |||
| ce0b19605a | |||
| 35bd9ecda3 | |||
| ca89849dd2 | |||
| ac1cb6d56b | |||
| dfbffea0fc | |||
| 7ae9c993f1 | |||
| 91d739f8d6 | |||
| f0c625d85c | |||
| b5f5e73076 | |||
| 1fb5eff7f1 | |||
| 5116cfd141 | |||
| e53a1f90b0 | |||
| 766c9628b0 | |||
| 6a4abdd74c | |||
| fc8bc5ba1e | |||
| 0fde5d44c0 | |||
| dc6b5a3d49 | |||
| 396522f249 | |||
| 86ab39e4ca | |||
| a4c9cb0e55 | |||
| e6c6feac10 | |||
| ca0aee58ab | |||
| 6391f2c43d | |||
| 32171bb70c | |||
| fd6675a3a3 | |||
| 9d883978a8 | |||
| 1aae65575c | |||
| c5d58e1aab | |||
| 56394471fa | |||
| 4cae6959df | |||
| f02d7b4516 | |||
| f5c83112df | |||
| a413dc81c1 | |||
| c9eddab191 | |||
| ec1268bd71 | |||
| 22eb2b5823 | |||
| 9669da026f | |||
| 7b14e54eab | |||
| 6b30ee4593 | |||
| 17c47a15da | |||
| 8f55517236 | |||
| 41ad086dfa | |||
| e19ef7dcae | |||
| f361265d70 | |||
| ef72e3ef77 | |||
| 770f1a1ca0 | |||
| e8fc91191f | |||
| 105ad3317d | |||
| 22bf4775cd | |||
| 5c6be7969a | |||
| c6e23f4be2 | |||
| 05547c22ec | |||
| cc7ac79fa6 | |||
| 4c5c27dfc1 | |||
| 4aabfbd52e | |||
| 6eab842361 | |||
| b729dfd702 | |||
| 6366840781 | |||
| 704a2ee90b | |||
| 484be9bfe6 | |||
| a99e070c26 | |||
| bf803f88af | |||
| 9af6febca5 | |||
| 0101d0a1bd | |||
| 266874609d | |||
| 2ba7feedfc | |||
| 43c67b4939 | |||
| 2d9915e43a | |||
| 2329b41bce | |||
| 536496184e | |||
| 429c32477c | |||
| f5d51b2061 | |||
| 2ad1aaa277 | |||
| 3afd32dbc1 | |||
| 092830ed07 | |||
| d118a6d3ff | |||
| fe97ffdc2f | |||
| 964d2ce59c | |||
| dc52684cbc | |||
| 191bedc56f | |||
| 47b2ace7fd | |||
| 9fb7359a3e | |||
| 4a5de26406 | |||
| 6fa18e126f | |||
| 1149002e0c | |||
| d704cb0b50 | |||
| d59e5ae9cf | |||
| 4587c1550d | |||
| b5bd329ada | |||
| beccd7a4ac | |||
| 61262fa939 | |||
| 7c6b006631 | |||
| dbd149354a | |||
| 4306ba5004 | |||
| 6de370b82f | |||
| 45781666b8 | |||
| 538231eb6f | |||
| eb74f87f2c | |||
| 59d71ffdcf | |||
| d1b93d4011 | |||
| d8ddf2e740 | |||
| 581327dc8e | |||
| 76e4512a0c | |||
| efdd55beca | |||
| 2c115649b9 | |||
| 2ddcc31a93 | |||
| 3bcce5b749 | |||
| 80dac27214 | |||
| 4a1177d048 | |||
| 4725d8f270 | |||
| 07b3528515 | |||
| d2d1b1ea26 | |||
| 232b897abc | |||
| efd076bc6c | |||
| cc877480ff | |||
| 363145a284 | |||
| 755571ad33 | |||
| 39edb55721 | |||
| 15aa7ecc2e | |||
| ce9e91153e | |||
| 9ee0a46606 | |||
| 20dc351f4c | |||
| c30c54d562 | |||
| 45ff51c0d2 | |||
| 5b86e99138 | |||
| 0c72910eb7 | |||
| 3b0f485b41 | |||
| 2be2c95212 | |||
| e7aeeb8bd5 | |||
| 73df93f86a | |||
| 9cd4d5e2aa | |||
| c3b81554f4 | |||
| 6f79c52fc5 | |||
| 29bc3e022b | |||
| 7d03203d83 | |||
| 11dde835d1 | |||
| 52c47c288c | |||
| 63072aa8e1 | |||
| 4cca3ac922 | |||
| 604bc1ec11 | |||
| 5d7fbf3195 | |||
| 8c56e6f0d4 | |||
| 2069be7932 | |||
| 9c1c2991a9 | |||
| 81dabf2713 | |||
| 27520cb77e | |||
| f555d3dae0 | |||
| 70f1a2cacb | |||
| 7f15aaa2e5 | |||
| e5726205c4 | |||
| 07fabc0663 | |||
| 800f97ae85 | |||
| 3ee5b284e1 | |||
| c0389f0da9 | |||
| a7a4d7ded5 | |||
| 87cd974b79 | |||
| 7faaac31ff | |||
| 7386eb2a78 | |||
| 49a1524f4f | |||
| c957b8b06b | |||
| a3a6e14d80 | |||
| 7231f5468b | |||
| 4dfb0d7220 | |||
| 2f4f53b048 | |||
| b6e8389544 | |||
| d113878613 | |||
| f7ed7e06db | |||
| 977a1d4a3c | |||
| eefc08db47 | |||
| ab2b1663d3 | |||
| 04b8a718e3 | |||
| 37cf260ba6 | |||
| 8f91fe4cd1 | |||
| 9246ad412f | |||
| 1ccbbdd4fb | |||
| 16cf37994d | |||
| 01e84624c2 | |||
| 939cd7cf70 | |||
| 4b11603035 | |||
| ca18b6b052 | |||
| 3d0d19e561 | |||
| ae463a8735 | |||
| 7e797829ae | |||
| 431ed6bc5d | |||
| e9bb711c42 | |||
| 623bc5c156 | |||
| cfefef4619 | |||
| 4a9a881c1f | |||
| 13a06d585c | |||
| 1c8ad64da0 | |||
| 1d8925de57 | |||
| 0eb7e779b8 | |||
| a4b86eefe2 | |||
| 902a58bc70 | |||
| a34a44f29a | |||
| 454fe80172 | |||
| 81b6a8a311 | |||
| 3011a5bad7 | |||
| dcb7be3acd | |||
| 68a6b510b1 | |||
| dca3e89303 | |||
| bae6fef588 | |||
| 37f65e43a5 | |||
| 8c910101c7 | |||
| 112d9c41eb | |||
| c91d1097f6 | |||
| 105c2c9eef | |||
| b754d2de28 | |||
| f6425c7ec6 | |||
| e690c9b8c8 | |||
| f87cbac77c | |||
| 150bd313cf | |||
| bc90cb894c | |||
| c51a75a681 | |||
| 68aa9bd12d | |||
| 1fb6bf4d70 | |||
| b4df3658f1 | |||
| 4efb4d9b24 | |||
| d61e893731 | |||
| 951e44728e | |||
| 8dcdf73222 | |||
| 44c3b0af57 | |||
| 2b295400ac | |||
| aa8d8e93d2 | |||
| 89be7eac0e | |||
| f3847b932b | |||
| 5e4f37532c | |||
| f3f5ca74a3 | |||
| e50b7076a1 | |||
| 36ab5aa1b6 | |||
| a0a2b299d9 | |||
| 14d354fc29 | |||
| 342515f916 | |||
| 5f5944c237 | |||
| c025432ad6 | |||
| 171a6437fe | |||
| 11b3648fac | |||
| d1fae89d6d | |||
| 5c06848fe9 | |||
| b50e506e58 | |||
| 59fafa163d | |||
| 22d84b5763 | |||
| 6d186892a8 | |||
| 88d6143897 | |||
| b729fba75e | |||
| c0d3f9fa48 | |||
| af5e7a0e33 | |||
| 371d96ea65 | |||
| e9e332ff85 | |||
| e133ac2815 | |||
| 1dba5d147e | |||
| 1616c0b022 | |||
| bcee2cf0e3 | |||
| 3e7ddab0e9 | |||
| 5da0177356 | |||
| 7e21638811 | |||
| db5b7ab867 | |||
| 3bcc1c84bb | |||
| d46053f8d6 | |||
| 00a5fed9e9 | |||
| b6315a715a | |||
| 0da8303468 | |||
| c821c4684f | |||
| 6bae33f822 | |||
| 08d4ab67a6 | |||
| 62203d2f21 | |||
| 4968dcc558 | |||
| 6d66d1371f | |||
| b87ca71103 | |||
| c251cd2e8f | |||
| 593616d2d9 | |||
| fdd4c0bbe1 |
@@ -0,0 +1,4 @@
|
|||||||
|
issuesOpened: >
|
||||||
|
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||||
|
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||||
|
Thank you, and happy streaming!
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
*.ap_
|
*.ap_
|
||||||
*.aab
|
*.aab
|
||||||
output.json
|
output.json
|
||||||
|
output-metadata.json
|
||||||
out/
|
out/
|
||||||
|
|
||||||
# files for the dex VM
|
# files for the dex VM
|
||||||
|
|||||||
+6
-3
@@ -8,8 +8,11 @@ android:
|
|||||||
components:
|
components:
|
||||||
- tools
|
- tools
|
||||||
- platform-tools
|
- platform-tools
|
||||||
- build-tools-29.0.1
|
- build-tools-30.0.0
|
||||||
- android-29
|
- android-30
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- sdkmanager --list
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- yes | sdkmanager "ndk-bundle"
|
- yes | sdkmanager "ndk;21.0.6113669"
|
||||||
|
|||||||
@@ -2,55 +2,26 @@
|
|||||||
|
|
||||||
[](https://travis-ci.org/moonlight-stream/moonlight-android)
|
[](https://travis-ci.org/moonlight-stream/moonlight-android)
|
||||||
|
|
||||||
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight for Android](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.
|
|
||||||
|
|
||||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
Moonlight for Android 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.
|
||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [iOS/tvOS client](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
## Features
|
Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed project information, setup guide, or troubleshooting steps.
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
## Downloads
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* [Google Play Store](https://play.google.com/store/apps/details?id=com.limelight)
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* [Amazon App Store](https://www.amazon.com/gp/product/B00JK4MFN2)
|
||||||
|
* [F-Droid](https://f-droid.org/packages/com.limelight)
|
||||||
## Installation
|
* [APK](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||||
|
|
||||||
* Download and install Moonlight for Android from
|
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/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
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
|
||||||
* Android device running 4.1 (Jelly Bean) or higher
|
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
|
||||||
* If you are connecting from outside the same network, turn on internet
|
|
||||||
streaming
|
|
||||||
* 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 and add the PIN if needed
|
|
||||||
* Tap your PC again to view the list of apps to stream
|
|
||||||
* Play games!
|
|
||||||
|
|
||||||
## Contribute
|
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
|
||||||
|
|
||||||
1. Fork us
|
|
||||||
2. Write code
|
|
||||||
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 or gradle
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -1,14 +1,14 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
|
|
||||||
versionName "8.2"
|
versionName "9.8"
|
||||||
versionCode = 199
|
versionCode = 251
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "root"
|
flavorDimensions "root"
|
||||||
@@ -114,10 +114,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.62'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.66'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.62'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.66'
|
||||||
implementation 'org.jcodec:jcodec:0.2.3'
|
implementation 'org.jcodec:jcodec:0.2.3'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.12.3'
|
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
|
||||||
implementation 'com.squareup.okio:okio:1.17.4'
|
implementation 'com.squareup.okio:okio:1.17.5'
|
||||||
implementation 'org.jmdns:jmdns:3.5.5'
|
implementation 'org.jmdns:jmdns:3.5.5'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:installLocation="auto"
|
android:installLocation="auto"
|
||||||
|
android:gwpAsanMode="always"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
<provider
|
<provider
|
||||||
android:name=".PosterContentProvider"
|
android:name=".PosterContentProvider"
|
||||||
@@ -120,10 +121,20 @@
|
|||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:theme="@style/StreamTheme">
|
android:theme="@style/StreamTheme"
|
||||||
|
android:preferMinimalPostProcessing="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.AppView" />
|
android:value="com.limelight.AppView" />
|
||||||
|
|
||||||
|
<!-- Special metadata for NVIDIA Shield devices to prevent input buffering
|
||||||
|
and most importantly, opt out of mouse acceleration while streaming -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.nvidia.immediateInput"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.nvidia.rawCursorInput"
|
||||||
|
android:value="true" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.limelight;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
@@ -26,6 +27,8 @@ 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.SharedPreferences;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -58,17 +61,22 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private int lastRunningAppId;
|
private int lastRunningAppId;
|
||||||
private boolean suspendGridUpdates;
|
private boolean suspendGridUpdates;
|
||||||
private boolean inForeground;
|
private boolean inForeground;
|
||||||
|
private boolean showHiddenApps;
|
||||||
|
private HashSet<Integer> hiddenAppIds = new HashSet<>();
|
||||||
|
|
||||||
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 START_WITH_QUIT = 4;
|
private final static int START_WITH_QUIT = 4;
|
||||||
private final static int VIEW_DETAILS_ID = 5;
|
private final static int VIEW_DETAILS_ID = 5;
|
||||||
private final static int CREATE_SHORTCUT_ID = 6;
|
private final static int CREATE_SHORTCUT_ID = 6;
|
||||||
|
private final static int HIDE_APP_ID = 7;
|
||||||
|
|
||||||
|
public final static String HIDDEN_APPS_PREF_FILENAME = "HiddenApps";
|
||||||
|
|
||||||
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";
|
||||||
public final static String NEW_PAIR_EXTRA = "NewPair";
|
public final static String NEW_PAIR_EXTRA = "NewPair";
|
||||||
|
public final static String SHOW_HIDDEN_APPS_EXTRA = "ShowHiddenApps";
|
||||||
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
@@ -96,15 +104,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
PreferenceConfiguration.readPreferences(AppView.this),
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
computer, localBinder.getUniqueId(),
|
||||||
computer, localBinder.getUniqueId());
|
showHiddenApps);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appGridAdapter.updateHiddenApps(hiddenAppIds, true);
|
||||||
|
|
||||||
// Now make the binder visible. We must do this after appGridAdapter
|
// Now make the binder visible. We must do this after appGridAdapter
|
||||||
// is set to prevent us from reaching updateUiWithServerinfo() and
|
// is set to prevent us from reaching updateUiWithServerinfo() and
|
||||||
// touching the appGridAdapter prior to initialization.
|
// touching the appGridAdapter prior to initialization.
|
||||||
@@ -147,6 +157,27 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
|
||||||
|
// If appGridAdapter is initialized, let it know about the configuration change.
|
||||||
|
// If not, it will pick it up when it initializes.
|
||||||
|
if (appGridAdapter != null) {
|
||||||
|
// Update the app grid adapter to create grid items with the correct layout
|
||||||
|
appGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reinflate the app grid itself to pick up the layout change
|
||||||
|
getFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||||
|
.commitAllowingStateLoss();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
// Don't start polling if we're not bound or in the foreground
|
// Don't start polling if we're not bound or in the foreground
|
||||||
if (managerBinder == null || !inForeground) {
|
if (managerBinder == null || !inForeground) {
|
||||||
@@ -264,8 +295,14 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
showHiddenApps = getIntent().getBooleanExtra(SHOW_HIDDEN_APPS_EXTRA, false);
|
||||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||||
|
|
||||||
|
SharedPreferences hiddenAppsPrefs = getSharedPreferences(HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE);
|
||||||
|
for (String hiddenAppIdStr : hiddenAppsPrefs.getStringSet(uuidString, new HashSet<String>())) {
|
||||||
|
hiddenAppIds.add(Integer.parseInt(hiddenAppIdStr));
|
||||||
|
}
|
||||||
|
|
||||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
|
||||||
TextView label = findViewById(R.id.appListText);
|
TextView label = findViewById(R.id.appListText);
|
||||||
@@ -277,6 +314,21 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
Service.BIND_AUTO_CREATE);
|
Service.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateHiddenApps(boolean hideImmediately) {
|
||||||
|
HashSet<String> hiddenAppIdStringSet = new HashSet<>();
|
||||||
|
|
||||||
|
for (Integer hiddenAppId : hiddenAppIds) {
|
||||||
|
hiddenAppIdStringSet.add(hiddenAppId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
getSharedPreferences(HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putStringSet(uuidString, hiddenAppIdStringSet)
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
appGridAdapter.updateHiddenApps(hiddenAppIds, hideImmediately);
|
||||||
|
}
|
||||||
|
|
||||||
private void populateAppGridWithCache() {
|
private void populateAppGridWithCache() {
|
||||||
try {
|
try {
|
||||||
// Try to load from cache
|
// Try to load from cache
|
||||||
@@ -334,9 +386,12 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
|
|
||||||
|
menu.setHeaderTitle(selectedApp.app.getAppName());
|
||||||
|
|
||||||
if (lastRunningAppId != 0) {
|
if (lastRunningAppId != 0) {
|
||||||
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
||||||
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
@@ -344,10 +399,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, START_WITH_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, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
|
||||||
|
// Only show the hide checkbox if this is not the currently running app or it's already hidden
|
||||||
|
if (lastRunningAppId != selectedApp.app.getAppId() || selectedApp.isHidden) {
|
||||||
|
MenuItem hideAppItem = menu.add(Menu.NONE, HIDE_APP_ID, 3, getResources().getString(R.string.applist_menu_hide_app));
|
||||||
|
hideAppItem.setCheckable(true);
|
||||||
|
hideAppItem.setChecked(selectedApp.isHidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.add(Menu.NONE, VIEW_DETAILS_ID, 4, getResources().getString(R.string.applist_menu_details));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
// Only add an option to create shortcut if box art is loaded
|
// Only add an option to create shortcut if box art is loaded
|
||||||
@@ -358,7 +420,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
||||||
if (drawable != null && drawable.getBitmap() != null) {
|
if (drawable != null && drawable.getBitmap() != null) {
|
||||||
// We have a bitmap loaded too
|
// We have a bitmap loaded too
|
||||||
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 5, getResources().getString(R.string.applist_menu_scut));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,12 +471,20 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case CANCEL_ID:
|
case VIEW_DETAILS_ID:
|
||||||
|
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details), app.app.toString(), false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case VIEW_DETAILS_ID:
|
case HIDE_APP_ID:
|
||||||
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
|
if (item.isChecked()) {
|
||||||
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
|
// Transitioning hidden to shown
|
||||||
|
hiddenAppIds.remove(app.app.getAppId());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Transitioning shown to hidden
|
||||||
|
hiddenAppIds.add(app.app.getAppId());
|
||||||
|
}
|
||||||
|
updateHiddenApps(false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case CREATE_SHORTCUT_ID:
|
case CREATE_SHORTCUT_ID:
|
||||||
@@ -496,6 +566,12 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
if (!foundExistingApp) {
|
if (!foundExistingApp) {
|
||||||
// This app must be new
|
// This app must be new
|
||||||
appGridAdapter.addApp(new AppObject(app));
|
appGridAdapter.addApp(new AppObject(app));
|
||||||
|
|
||||||
|
// We could have a leftover shortcut from last time this PC was paired
|
||||||
|
// or if this app was removed then added again. Enable those shortcuts
|
||||||
|
// again if present.
|
||||||
|
shortcutHelper.enableAppShortcut(computer, app);
|
||||||
|
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,9 +614,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAdapterFragmentLayoutId() {
|
public int getAdapterFragmentLayoutId() {
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
return PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
||||||
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
R.layout.app_grid_view_small : R.layout.app_grid_view;
|
||||||
R.layout.app_grid_view_small : R.layout.app_grid_view);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -565,9 +640,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
listView.requestFocus();
|
listView.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppObject {
|
public static class AppObject {
|
||||||
public final NvApp app;
|
public final NvApp app;
|
||||||
public boolean isRunning;
|
public boolean isRunning;
|
||||||
|
public boolean isHidden;
|
||||||
|
|
||||||
public AppObject(NvApp app) {
|
public AppObject(NvApp app) {
|
||||||
if (app == null) {
|
if (app == null) {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import com.limelight.binding.input.ControllerHandler;
|
|||||||
import com.limelight.binding.input.KeyboardTranslator;
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
import com.limelight.binding.input.capture.InputCaptureManager;
|
import com.limelight.binding.input.capture.InputCaptureManager;
|
||||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
import com.limelight.binding.input.TouchContext;
|
import com.limelight.binding.input.touch.AbsoluteTouchContext;
|
||||||
|
import com.limelight.binding.input.touch.RelativeTouchContext;
|
||||||
import com.limelight.binding.input.driver.UsbDriverService;
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
import com.limelight.binding.input.touch.TouchContext;
|
||||||
import com.limelight.binding.input.virtual_controller.VirtualController;
|
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||||
import com.limelight.binding.video.CrashListener;
|
import com.limelight.binding.video.CrashListener;
|
||||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||||
@@ -27,6 +29,8 @@ import com.limelight.preferences.PreferenceConfiguration;
|
|||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
import com.limelight.ui.StreamView;
|
import com.limelight.ui.StreamView;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.NetHelper;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.ShortcutHelper;
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
@@ -42,6 +46,7 @@ import android.content.Intent;
|
|||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
@@ -59,6 +64,7 @@ import android.view.Display;
|
|||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
import android.view.Surface;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnGenericMotionListener;
|
import android.view.View.OnGenericMotionListener;
|
||||||
@@ -72,6 +78,7 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
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;
|
||||||
@@ -83,8 +90,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
|
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
|
||||||
PerfOverlayListener
|
PerfOverlayListener
|
||||||
{
|
{
|
||||||
private int lastMouseX = Integer.MIN_VALUE;
|
|
||||||
private int lastMouseY = Integer.MIN_VALUE;
|
|
||||||
private int lastButtonState = 0;
|
private int lastButtonState = 0;
|
||||||
|
|
||||||
// Only 2 touches are supported
|
// Only 2 touches are supported
|
||||||
@@ -94,6 +99,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private static final int REFERENCE_HORIZ_RES = 1280;
|
private static final int REFERENCE_HORIZ_RES = 1280;
|
||||||
private static final int REFERENCE_VERT_RES = 720;
|
private static final int REFERENCE_VERT_RES = 720;
|
||||||
|
|
||||||
|
private static final int STYLUS_DOWN_DEAD_ZONE_DELAY = 100;
|
||||||
|
private static final int STYLUS_DOWN_DEAD_ZONE_RADIUS = 20;
|
||||||
|
|
||||||
|
private static final int STYLUS_UP_DEAD_ZONE_DELAY = 150;
|
||||||
|
private static final int STYLUS_UP_DEAD_ZONE_RADIUS = 50;
|
||||||
|
|
||||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||||
|
|
||||||
private ControllerHandler controllerHandler;
|
private ControllerHandler controllerHandler;
|
||||||
@@ -115,6 +126,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean grabbedInput = true;
|
private boolean grabbedInput = true;
|
||||||
private boolean grabComboDown = false;
|
private boolean grabComboDown = false;
|
||||||
private StreamView streamView;
|
private StreamView streamView;
|
||||||
|
private long lastAbsTouchUpTime = 0;
|
||||||
|
private long lastAbsTouchDownTime = 0;
|
||||||
|
private float lastAbsTouchUpX, lastAbsTouchUpY;
|
||||||
|
private float lastAbsTouchDownX, lastAbsTouchDownY;
|
||||||
|
|
||||||
private boolean isHidingOverlays;
|
private boolean isHidingOverlays;
|
||||||
private TextView notificationOverlayView;
|
private TextView notificationOverlayView;
|
||||||
@@ -199,11 +214,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && prefConfig.stretchVideo) {
|
if (prefConfig.stretchVideo) {
|
||||||
// Allow the activity to layout under notches if the fill-screen option
|
// Allow the activity to layout under notches if the fill-screen option
|
||||||
// was turned on by the user
|
// was turned on by the user
|
||||||
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for events on the game surface
|
// Listen for events on the game surface
|
||||||
@@ -225,7 +246,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
|
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
|
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
|
||||||
return handleMotionEvent(motionEvent);
|
return handleMotionEvent(view, motionEvent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -397,45 +418,61 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Hopefully, we can get rid of this once someone comes up with a better way
|
// Hopefully, we can get rid of this once someone comes up with a better way
|
||||||
// to track the state of the pipeline and time frames.
|
// to track the state of the pipeline and time frames.
|
||||||
int roundedRefreshRate = Math.round(displayRefreshRate);
|
int roundedRefreshRate = Math.round(displayRefreshRate);
|
||||||
if ((!prefConfig.disableFrameDrop || prefConfig.unlockFps) && prefConfig.fps >= roundedRefreshRate) {
|
int chosenFrameRate = prefConfig.fps;
|
||||||
if (prefConfig.unlockFps) {
|
if (!prefConfig.disableFrameDrop || prefConfig.unlockFps) {
|
||||||
// Use frame drops when rendering above the screen frame rate
|
if (Build.DEVICE.equals("coral") || Build.DEVICE.equals("flame")) {
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
// HACK: Pixel 4 (XL) ignores the preferred display mode and lowers refresh rate,
|
||||||
LimeLog.info("Using drop mode for FPS > Hz");
|
// causing frame pacing issues. See https://issuetracker.google.com/issues/143401475
|
||||||
|
// To work around this, use frame drop mode if we want to stream at >= 60 FPS.
|
||||||
|
if (prefConfig.fps >= 60) {
|
||||||
|
LimeLog.info("Using Pixel 4 rendering hack");
|
||||||
|
decoderRenderer.enableLegacyFrameDropRendering();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (roundedRefreshRate <= 49) {
|
else if (prefConfig.fps >= roundedRefreshRate) {
|
||||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
if (prefConfig.unlockFps) {
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
// Use frame drops when rendering above the screen frame rate
|
||||||
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
|
decoderRenderer.enableLegacyFrameDropRendering();
|
||||||
}
|
LimeLog.info("Using drop mode for FPS > Hz");
|
||||||
// HACK: Avoid crashing on some MTK devices
|
} else if (roundedRefreshRate <= 49) {
|
||||||
else if (roundedRefreshRate == 50 && decoderRenderer.is49FpsBlacklisted()) {
|
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
||||||
// Use the old rendering strategy on these broken devices
|
decoderRenderer.enableLegacyFrameDropRendering();
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
LimeLog.info("Bogus refresh rate: " + roundedRefreshRate);
|
||||||
}
|
}
|
||||||
else {
|
// HACK: Avoid crashing on some MTK devices
|
||||||
prefConfig.fps = roundedRefreshRate - 1;
|
else if (decoderRenderer.isBlacklistedForFrameRate(roundedRefreshRate - 1)) {
|
||||||
LimeLog.info("Adjusting FPS target for screen to "+prefConfig.fps);
|
// Use the old rendering strategy on these broken devices
|
||||||
|
decoderRenderer.enableLegacyFrameDropRendering();
|
||||||
|
} else {
|
||||||
|
chosenFrameRate = roundedRefreshRate - 1;
|
||||||
|
LimeLog.info("Adjusting FPS target for screen to " + chosenFrameRate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean vpnActive = NetHelper.isActiveNetworkVpn(this);
|
||||||
|
if (vpnActive) {
|
||||||
|
LimeLog.info("Detected active network is a VPN");
|
||||||
|
}
|
||||||
|
|
||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setLaunchRefreshRate(prefConfig.fps)
|
||||||
|
.setRefreshRate(chosenFrameRate)
|
||||||
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
|
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
|
||||||
.setBitrate(prefConfig.bitrate)
|
.setBitrate(prefConfig.bitrate)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
.setMaxPacketSize(1392)
|
.setMaxPacketSize(vpnActive ? 1024 : 1392) // Lower MTU on VPN
|
||||||
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO)
|
.setRemoteConfiguration(vpnActive ? // Use remote optimizations on VPN
|
||||||
|
StreamConfiguration.STREAM_CFG_REMOTE :
|
||||||
|
StreamConfiguration.STREAM_CFG_AUTO)
|
||||||
.setHevcBitratePercentageMultiplier(75)
|
.setHevcBitratePercentageMultiplier(75)
|
||||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||||
.setEnableHdr(willStreamHdr)
|
.setEnableHdr(willStreamHdr)
|
||||||
.setAttachedGamepadMask(gamepadMask)
|
.setAttachedGamepadMask(gamepadMask)
|
||||||
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
|
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
|
||||||
.setAudioConfiguration(prefConfig.enable51Surround ?
|
.setAudioConfiguration(prefConfig.audioConfiguration)
|
||||||
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
|
|
||||||
MoonBridge.AUDIO_CONFIGURATION_STEREO)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
@@ -447,9 +484,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Initialize touch contexts
|
// Initialize touch contexts
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
touchContextMap[i] = new TouchContext(conn, i,
|
if (!prefConfig.touchscreenTrackpad) {
|
||||||
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
touchContextMap[i] = new AbsoluteTouchContext(conn, i, streamView);
|
||||||
streamView);
|
}
|
||||||
|
else {
|
||||||
|
touchContextMap[i] = new RelativeTouchContext(conn, i,
|
||||||
|
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
||||||
|
streamView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use sustained performance mode on N+ to ensure consistent
|
// Use sustained performance mode on N+ to ensure consistent
|
||||||
@@ -575,6 +617,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isRefreshRateGoodMatch(float refreshRate) {
|
||||||
|
return refreshRate >= prefConfig.fps &&
|
||||||
|
Math.round(refreshRate) % prefConfig.fps <= 3;
|
||||||
|
}
|
||||||
|
|
||||||
private float prepareDisplayForRendering() {
|
private float prepareDisplayForRendering() {
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||||
@@ -583,41 +630,59 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// On M, we can explicitly set the optimal display mode
|
// On M, we can explicitly set the optimal display mode
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Display.Mode bestMode = display.getMode();
|
Display.Mode bestMode = display.getMode();
|
||||||
|
boolean refreshRateIsGood = isRefreshRateGoodMatch(bestMode.getRefreshRate());
|
||||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate();
|
boolean refreshRateReduced = candidate.getRefreshRate() < bestMode.getRefreshRate();
|
||||||
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
boolean resolutionReduced = candidate.getPhysicalWidth() < bestMode.getPhysicalWidth() ||
|
||||||
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
candidate.getPhysicalHeight() < bestMode.getPhysicalHeight();
|
||||||
candidate.getPhysicalWidth() <= 4096;
|
boolean resolutionFitsStream = candidate.getPhysicalWidth() >= prefConfig.width &&
|
||||||
|
candidate.getPhysicalHeight() >= prefConfig.height;
|
||||||
|
|
||||||
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
|
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
|
||||||
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
|
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
|
||||||
|
|
||||||
// On non-4K streams, we force the resolution to never change
|
if (candidate.getPhysicalWidth() > 4096) {
|
||||||
if (prefConfig.width < 3840) {
|
// Avoid resolutions options above 4K to be safe
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On non-4K streams, we force the resolution to never change unless it's above
|
||||||
|
// 60 FPS, which may require a resolution reduction due to HDMI bandwidth limitations.
|
||||||
|
if (prefConfig.width < 3840 && prefConfig.fps <= 60) {
|
||||||
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
|
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
|
||||||
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
|
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
// Make sure the resolution doesn't regress unless if it's over 60 FPS
|
||||||
if (prefConfig.fps <= 60) {
|
// where we may need to reduce resolution to achieve the desired refresh rate.
|
||||||
if (candidate.getRefreshRate() >= 63) {
|
if (resolutionReduced && !(prefConfig.fps > 60 && resolutionFitsStream)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshRateIsGood) {
|
||||||
|
// We have a good matching refresh rate, so we're looking for equal or greater
|
||||||
|
// that is also a good matching refresh rate for our stream frame rate.
|
||||||
|
if (refreshRateReduced || !isRefreshRateGoodMatch(candidate.getRefreshRate())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (!isRefreshRateGoodMatch(candidate.getRefreshRate())) {
|
||||||
// Make sure the refresh rate doesn't regress
|
// We didn't have a good match and this match isn't good either, so just don't
|
||||||
if (!refreshRateOk) {
|
// reduce the refresh rate.
|
||||||
continue;
|
if (refreshRateReduced) {
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
// Make sure the resolution doesn't regress
|
} else {
|
||||||
if (!resolutionOk) {
|
// We didn't have a good match and this match is good. Prefer this refresh rate
|
||||||
continue;
|
// even if it reduces the refresh rate. Lowering the refresh rate can be beneficial
|
||||||
|
// when streaming a 60 FPS stream on a 90 Hz device. We want to select 60 Hz to
|
||||||
|
// match the frame rate even if the active display mode is 90 Hz.
|
||||||
}
|
}
|
||||||
|
|
||||||
bestMode = candidate;
|
bestMode = candidate;
|
||||||
|
refreshRateIsGood = isRefreshRateGoodMatch(candidate.getRefreshRate());
|
||||||
}
|
}
|
||||||
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||||
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||||
@@ -628,9 +693,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
float bestRefreshRate = display.getRefreshRate();
|
float bestRefreshRate = display.getRefreshRate();
|
||||||
for (float candidate : display.getSupportedRefreshRates()) {
|
for (float candidate : display.getSupportedRefreshRates()) {
|
||||||
if (candidate > bestRefreshRate) {
|
LimeLog.info("Examining refresh rate: "+candidate);
|
||||||
LimeLog.info("Examining refresh rate: "+candidate);
|
|
||||||
|
|
||||||
|
if (candidate > bestRefreshRate) {
|
||||||
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
||||||
if (prefConfig.fps <= 60) {
|
if (prefConfig.fps <= 60) {
|
||||||
if (candidate >= 63) {
|
if (candidate >= 63) {
|
||||||
@@ -651,6 +716,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
displayRefreshRate = display.getRefreshRate();
|
displayRefreshRate = display.getRefreshRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable HDMI ALLM (game mode) on Android R
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
windowLayoutParams.preferMinimalPostProcessing = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the display mode change
|
// Apply the display mode change
|
||||||
getWindow().setAttributes(windowLayoutParams);
|
getWindow().setAttributes(windowLayoutParams);
|
||||||
|
|
||||||
@@ -685,7 +755,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
return displayRefreshRate;
|
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION) ||
|
||||||
|
getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
|
// TVs may take a few moments to switch refresh rates, and we can probably assume
|
||||||
|
// it will be eventually activated.
|
||||||
|
// TODO: Improve this
|
||||||
|
return displayRefreshRate;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use the actual refresh rate of the display, since the preferred refresh rate or mode
|
||||||
|
// may not actually be applied (ex: Pixel 4 with Smooth Display disabled).
|
||||||
|
return getWindowManager().getDefaultDisplay().getRefreshRate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
@@ -794,34 +875,36 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
stopConnection();
|
stopConnection();
|
||||||
|
|
||||||
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
if (prefConfig.enableLatencyToast) {
|
||||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||||
String message = null;
|
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||||
if (averageEndToEndLat > 0) {
|
String message = null;
|
||||||
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
if (averageEndToEndLat > 0) {
|
||||||
if (averageDecoderLat > 0) {
|
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
||||||
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
if (averageDecoderLat > 0) {
|
||||||
|
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (averageDecoderLat > 0) {
|
||||||
|
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (averageDecoderLat > 0) {
|
|
||||||
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the video codec to the post-stream toast
|
// Add the video codec to the post-stream toast
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
|
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
|
||||||
message += " [H.265 HDR]";
|
message += " [H.265 HDR]";
|
||||||
|
}
|
||||||
|
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
|
||||||
|
message += " [H.265]";
|
||||||
|
}
|
||||||
|
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
|
||||||
|
message += " [H.264]";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
|
|
||||||
message += " [H.265]";
|
|
||||||
}
|
|
||||||
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
|
|
||||||
message += " [H.264]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the tombstone count if we terminated normally
|
// Clear the tombstone count if we terminated normally
|
||||||
@@ -944,11 +1027,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Handle a synthetic back button event that some Android OS versions
|
// Handle a synthetic back button event that some Android OS versions
|
||||||
// create as a result of a right-click. This event WILL repeat if
|
// create as a result of a right-click. This event WILL repeat if
|
||||||
// the right mouse button is held down, so we ignore those.
|
// the right mouse button is held down, so we ignore those.
|
||||||
if (!prefConfig.mouseNavButtons &&
|
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
(event.getSource() == InputDevice.SOURCE_MOUSE ||
|
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
|
||||||
|
// Send the right mouse button event if mouse back and forward
|
||||||
|
// are disabled. If they are enabled, handleMotionEvent() will take
|
||||||
|
// care of this.
|
||||||
|
if (!prefConfig.mouseNavButtons) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always return true, otherwise the back press will be propagated
|
||||||
|
// up to the parent and finish the activity.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,6 +1063,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eat repeat down events
|
||||||
|
if (event.getRepeatCount() > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Pass through keyboard input if we're not grabbing
|
// Pass through keyboard input if we're not grabbing
|
||||||
if (!grabbedInput) {
|
if (!grabbedInput) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1002,11 +1098,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Handle a synthetic back button event that some Android OS versions
|
// Handle a synthetic back button event that some Android OS versions
|
||||||
// create as a result of a right-click.
|
// create as a result of a right-click.
|
||||||
if (!prefConfig.mouseNavButtons &&
|
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
(event.getSource() == InputDevice.SOURCE_MOUSE ||
|
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
|
||||||
|
// Send the right mouse button event if mouse back and forward
|
||||||
|
// are disabled. If they are enabled, handleMotionEvent() will take
|
||||||
|
// care of this.
|
||||||
|
if (!prefConfig.mouseNavButtons) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always return true, otherwise the back press will be propagated
|
||||||
|
// up to the parent and finish the activity.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,7 +1168,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the event was consumed
|
// Returns true if the event was consumed
|
||||||
private boolean handleMotionEvent(MotionEvent event) {
|
// NB: View is only present if called from a view callback
|
||||||
|
private boolean handleMotionEvent(View view, MotionEvent event) {
|
||||||
// Pass through keyboard input if we're not grabbing
|
// Pass through keyboard input if we're not grabbing
|
||||||
if (!grabbedInput) {
|
if (!grabbedInput) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1078,17 +1183,38 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
|
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
|
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
|
||||||
{
|
{
|
||||||
// This case is for mice
|
// This case is for mice and non-finger touch devices
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
|
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
|
||||||
(event.getPointerCount() >= 1 &&
|
(event.getPointerCount() >= 1 &&
|
||||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
|
(event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE ||
|
||||||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS ||
|
||||||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER)))
|
||||||
{
|
{
|
||||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||||
|
|
||||||
// Ignore mouse input if we're not capturing from our input source
|
// Ignore mouse input if we're not capturing from our input source
|
||||||
if (!inputCaptureProvider.isCapturingActive()) {
|
if (!inputCaptureProvider.isCapturingActive()) {
|
||||||
return false;
|
// We return true here because otherwise the events may end up causing
|
||||||
|
// Android to synthesize d-pad events.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update the position before sending any button events. If we're
|
||||||
|
// dealing with a stylus without hover support, our position might be
|
||||||
|
// significantly different than before.
|
||||||
|
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
||||||
|
// Send the deltas straight from the motion event
|
||||||
|
short deltaX = (short)inputCaptureProvider.getRelativeAxisX(event);
|
||||||
|
short deltaY = (short)inputCaptureProvider.getRelativeAxisY(event);
|
||||||
|
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
conn.sendMouseMove(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (view != null) {
|
||||||
|
// Otherwise send absolute position
|
||||||
|
updateMousePosition(view, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
||||||
@@ -1096,13 +1222,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||||
conn.sendMouseScroll(vScrollClicks);
|
conn.sendMouseScroll(vScrollClicks);
|
||||||
}
|
}
|
||||||
else if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER ||
|
|
||||||
event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
|
|
||||||
// On some devices (Galaxy S8 without Oreo pointer capture), we can
|
|
||||||
// get spurious ACTION_HOVER_ENTER events when right clicking with
|
|
||||||
// incorrect X and Y coordinates. Just eat this event without processing it.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
@@ -1113,8 +1232,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
|
// Mouse secondary or stylus primary is right click (stylus down is left click)
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
if ((changedButtons & (MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) {
|
||||||
|
if ((event.getButtonState() & (MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) {
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -1122,8 +1242,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
|
// Mouse tertiary or stylus secondary is middle click
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
if ((changedButtons & (MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_SECONDARY)) != 0) {
|
||||||
|
if ((event.getButtonState() & (MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_SECONDARY)) != 0) {
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -1151,36 +1272,58 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get relative axis values if we can
|
// Handle stylus presses
|
||||||
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
if (event.getPointerCount() == 1 && event.getActionIndex() == 0) {
|
||||||
// Send the deltas straight from the motion event
|
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||||
conn.sendMouseMove((short) inputCaptureProvider.getRelativeAxisX(event),
|
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
|
||||||
(short) inputCaptureProvider.getRelativeAxisY(event));
|
lastAbsTouchDownTime = SystemClock.uptimeMillis();
|
||||||
|
lastAbsTouchDownX = event.getX(0);
|
||||||
|
lastAbsTouchDownY = event.getY(0);
|
||||||
|
|
||||||
// We have to also update the position Android thinks the cursor is at
|
// Stylus is left click
|
||||||
// in order to avoid jumping when we stop moving or click.
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||||
lastMouseX = (int)event.getX();
|
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
|
||||||
lastMouseY = (int)event.getY();
|
lastAbsTouchDownTime = SystemClock.uptimeMillis();
|
||||||
}
|
lastAbsTouchDownX = event.getX(0);
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
lastAbsTouchDownY = event.getY(0);
|
||||||
// We get a normal (non-relative) MotionEvent when starting pointer capture to synchronize the
|
|
||||||
// location of the cursor with our app. We don't want this, so we must discard this event.
|
// Eraser is right click
|
||||||
lastMouseX = (int)event.getX();
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
lastMouseY = (int)event.getY();
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
|
||||||
// Don't process the history. We just want the current position now.
|
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
|
||||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
lastAbsTouchUpTime = SystemClock.uptimeMillis();
|
||||||
|
lastAbsTouchUpX = event.getX(0);
|
||||||
|
lastAbsTouchUpY = event.getY(0);
|
||||||
|
|
||||||
|
// Stylus is left click
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
|
||||||
|
lastAbsTouchUpTime = SystemClock.uptimeMillis();
|
||||||
|
lastAbsTouchUpX = event.getX(0);
|
||||||
|
lastAbsTouchUpY = event.getY(0);
|
||||||
|
|
||||||
|
// Eraser is right click
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastButtonState = event.getButtonState();
|
lastButtonState = event.getButtonState();
|
||||||
}
|
}
|
||||||
// This case is for touch-based input devices
|
// This case is for fingers
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (virtualController != null &&
|
if (virtualController != null &&
|
||||||
virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) {
|
(virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons ||
|
||||||
// Ignore presses when the virtual controller is in configuration mode
|
virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)) {
|
||||||
|
// Ignore presses when the virtual controller is being configured
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view == null && !prefConfig.touchscreenTrackpad) {
|
||||||
|
// Absolute touch events should be dropped outside our view.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1213,7 +1356,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
{
|
{
|
||||||
case MotionEvent.ACTION_POINTER_DOWN:
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
context.touchDownEvent(eventX, eventY);
|
for (TouchContext touchContext : touchContextMap) {
|
||||||
|
touchContext.setPointerCount(event.getPointerCount());
|
||||||
|
}
|
||||||
|
context.touchDownEvent(eventX, eventY, true);
|
||||||
break;
|
break;
|
||||||
case MotionEvent.ACTION_POINTER_UP:
|
case MotionEvent.ACTION_POINTER_UP:
|
||||||
case MotionEvent.ACTION_UP:
|
case MotionEvent.ACTION_UP:
|
||||||
@@ -1226,9 +1372,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.touchUpEvent(eventX, eventY);
|
context.touchUpEvent(eventX, eventY);
|
||||||
|
for (TouchContext touchContext : touchContextMap) {
|
||||||
|
touchContext.setPointerCount(event.getPointerCount() - 1);
|
||||||
|
}
|
||||||
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
|
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
|
||||||
// The original secondary touch now becomes primary
|
// The original secondary touch now becomes primary
|
||||||
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
|
context.touchDownEvent((int)event.getX(1), (int)event.getY(1), false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
@@ -1257,6 +1406,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
for (TouchContext aTouchContext : touchContextMap) {
|
||||||
|
aTouchContext.cancelTouch();
|
||||||
|
aTouchContext.setPointerCount(0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1272,48 +1427,67 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
return handleMotionEvent(event) || super.onTouchEvent(event);
|
return handleMotionEvent(null, event) || super.onTouchEvent(event);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||||
return handleMotionEvent(event) || super.onGenericMotionEvent(event);
|
return handleMotionEvent(null, event) || super.onGenericMotionEvent(event);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMousePosition(int eventX, int eventY) {
|
private void updateMousePosition(View view, MotionEvent event) {
|
||||||
// Send a mouse move if we already have a mouse location
|
// X and Y are already relative to the provided view object
|
||||||
// and the mouse coordinates change
|
float eventX = event.getX(0);
|
||||||
if (lastMouseX != Integer.MIN_VALUE &&
|
float eventY = event.getY(0);
|
||||||
lastMouseY != Integer.MIN_VALUE &&
|
|
||||||
!(lastMouseX == eventX && lastMouseY == eventY))
|
if (event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
|
||||||
|
(event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER ||
|
||||||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS))
|
||||||
{
|
{
|
||||||
int deltaX = eventX - lastMouseX;
|
switch (event.getActionMasked()) {
|
||||||
int deltaY = eventY - lastMouseY;
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_HOVER_ENTER:
|
||||||
|
case MotionEvent.ACTION_HOVER_EXIT:
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
if (SystemClock.uptimeMillis() - lastAbsTouchUpTime <= STYLUS_UP_DEAD_ZONE_DELAY &&
|
||||||
|
Math.sqrt(Math.pow(eventX - lastAbsTouchUpX, 2) + Math.pow(eventY - lastAbsTouchUpY, 2)) <= STYLUS_UP_DEAD_ZONE_RADIUS) {
|
||||||
|
// Enforce a small deadzone between touch up and hover or touch down to allow more precise double-clicking
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
// Scale the deltas if the device resolution is different
|
case MotionEvent.ACTION_MOVE:
|
||||||
// than the stream resolution
|
case MotionEvent.ACTION_UP:
|
||||||
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)streamView.getWidth()));
|
if (SystemClock.uptimeMillis() - lastAbsTouchDownTime <= STYLUS_DOWN_DEAD_ZONE_DELAY &&
|
||||||
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)streamView.getHeight()));
|
Math.sqrt(Math.pow(eventX - lastAbsTouchDownX, 2) + Math.pow(eventY - lastAbsTouchDownY, 2)) <= STYLUS_DOWN_DEAD_ZONE_RADIUS) {
|
||||||
|
// Enforce a small deadzone between touch down and move or touch up to allow more precise double-clicking
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update pointer location for delta calculation next time
|
// We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT.
|
||||||
lastMouseX = eventX;
|
// Normalize these to the view size. We can't just drop them because we won't always get an event
|
||||||
lastMouseY = eventY;
|
// right at the boundary of the view, so dropping them would result in our cursor never really
|
||||||
|
// reaching the sides of the screen.
|
||||||
|
eventX = Math.min(Math.max(eventX, 0), view.getWidth());
|
||||||
|
eventY = Math.min(Math.max(eventY, 0), view.getHeight());
|
||||||
|
|
||||||
|
conn.sendMousePosition((short)eventX, (short)eventY, (short)view.getWidth(), (short)view.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
public boolean onGenericMotion(View view, MotionEvent event) {
|
||||||
return handleMotionEvent(event);
|
return handleMotionEvent(view, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
public boolean onTouch(View view, MotionEvent event) {
|
||||||
return handleMotionEvent(event);
|
return handleMotionEvent(view, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1352,7 +1526,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stageFailed(final String stage, final long errorCode) {
|
public void stageFailed(final String stage, final int portFlags, final int errorCode) {
|
||||||
|
// Perform a connection test if the failure could be due to a blocked port
|
||||||
|
// This does network I/O, so don't do it on the main thread.
|
||||||
|
final int portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER, 443, portFlags);
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -1367,18 +1545,33 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// If video initialization failed and the surface is still valid, display extra information for the user
|
// If video initialization failed and the surface is still valid, display extra information for the user
|
||||||
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
|
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
|
||||||
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
|
Toast.makeText(Game.this, getResources().getText(R.string.video_decoder_init_failed), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title),
|
String dialogText = getResources().getString(R.string.conn_error_msg) + " " + stage +" (error "+errorCode+")";
|
||||||
getResources().getString(R.string.conn_error_msg) + " " + stage, true);
|
|
||||||
|
if (portFlags != 0) {
|
||||||
|
dialogText += "\n\n" + getResources().getString(R.string.check_ports_msg) + "\n" +
|
||||||
|
MoonBridge.stringifyPortFlags(portFlags, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portTestResult != MoonBridge.ML_TEST_RESULT_INCONCLUSIVE && portTestResult != 0) {
|
||||||
|
dialogText += "\n\n" + getResources().getString(R.string.nettest_text_blocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title), dialogText, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionTerminated(final long errorCode) {
|
public void connectionTerminated(final int errorCode) {
|
||||||
|
// Perform a connection test if the failure could be due to a blocked port
|
||||||
|
// This does network I/O, so don't do it on the main thread.
|
||||||
|
final int portFlags = MoonBridge.getPortFlagsFromTerminationErrorCode(errorCode);
|
||||||
|
final int portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER,443, portFlags);
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -1395,9 +1588,40 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Display the error dialog if it was an unexpected termination.
|
// Display the error dialog if it was an unexpected termination.
|
||||||
// Otherwise, just finish the activity immediately.
|
// Otherwise, just finish the activity immediately.
|
||||||
if (errorCode != 0) {
|
if (errorCode != MoonBridge.ML_ERROR_GRACEFUL_TERMINATION) {
|
||||||
|
String message;
|
||||||
|
|
||||||
|
if (portTestResult != MoonBridge.ML_TEST_RESULT_INCONCLUSIVE && portTestResult != 0) {
|
||||||
|
// If we got a blocked result, that supersedes any other error message
|
||||||
|
message = getResources().getString(R.string.nettest_text_blocked);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch (errorCode) {
|
||||||
|
case MoonBridge.ML_ERROR_NO_VIDEO_TRAFFIC:
|
||||||
|
message = getResources().getString(R.string.no_video_received_error);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MoonBridge.ML_ERROR_NO_VIDEO_FRAME:
|
||||||
|
message = getResources().getString(R.string.no_frame_received_error);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MoonBridge.ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
|
||||||
|
message = getResources().getString(R.string.early_termination_error);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
message = getResources().getString(R.string.conn_terminated_msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portFlags != 0) {
|
||||||
|
message += "\n\n" + getResources().getString(R.string.check_ports_msg) + "\n" +
|
||||||
|
MoonBridge.stringifyPortFlags(portFlags, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
|
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
|
||||||
getResources().getString(R.string.conn_terminated_msg), true);
|
message, true);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
finish();
|
finish();
|
||||||
@@ -1517,6 +1741,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
@Override
|
@Override
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
surfaceCreated = true;
|
surfaceCreated = true;
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
// Tell the OS about our frame rate to allow it to adapt the display refresh rate appropriately
|
||||||
|
holder.getSurface().setFrameRate(prefConfig.fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ import java.util.logging.FileHandler;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class LimeLog {
|
public class LimeLog {
|
||||||
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
|
||||||
|
|
||||||
public static void info(String msg) {
|
public static void info(String msg) {
|
||||||
LOGGER.info(msg);
|
LOGGER.info(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void warning(String msg) {
|
public static void warning(String msg) {
|
||||||
LOGGER.warning(msg);
|
LOGGER.warning(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void severe(String msg) {
|
public static void severe(String msg) {
|
||||||
LOGGER.severe(msg);
|
LOGGER.severe(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setFileHandler(String fileName) throws IOException {
|
public static void setFileHandler(String fileName) throws IOException {
|
||||||
LOGGER.addHandler(new FileHandler(fileName));
|
LOGGER.addHandler(new FileHandler(fileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static int APP_LIST_ID = 1;
|
|
||||||
private final static int PAIR_ID = 2;
|
private final static int PAIR_ID = 2;
|
||||||
private final static int UNPAIR_ID = 3;
|
private final static int UNPAIR_ID = 3;
|
||||||
private final static int WOL_ID = 4;
|
private final static int WOL_ID = 4;
|
||||||
@@ -117,6 +116,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
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 final static int VIEW_DETAILS_ID = 8;
|
||||||
|
private final static int FULL_APP_LIST_ID = 9;
|
||||||
|
private final static int TEST_NETWORK_ID = 10;
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
@@ -126,6 +127,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Set default preferences if we've never been run
|
// Set default preferences if we've never been run
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
|
||||||
|
// Set the correct layout for the PC grid
|
||||||
|
pcGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
||||||
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
||||||
@@ -223,9 +227,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||||
Service.BIND_AUTO_CREATE);
|
Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
pcGridAdapter = new PcGridAdapter(this,
|
pcGridAdapter = new PcGridAdapter(this, PreferenceConfiguration.readPreferences(this));
|
||||||
PreferenceConfiguration.readPreferences(this).listMode,
|
|
||||||
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
|
||||||
|
|
||||||
initializeViews();
|
initializeViews();
|
||||||
}
|
}
|
||||||
@@ -315,15 +317,29 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
|
|
||||||
|
// Add a header with PC status details
|
||||||
|
menu.clearHeader();
|
||||||
|
switch (computer.details.state)
|
||||||
|
{
|
||||||
|
case ONLINE:
|
||||||
|
menu.setHeaderTitle(R.string.pcview_menu_header_online);
|
||||||
|
break;
|
||||||
|
case OFFLINE:
|
||||||
|
menu.setHeaderIcon(R.drawable.ic_pc_offline);
|
||||||
|
menu.setHeaderTitle(R.string.pcview_menu_header_offline);
|
||||||
|
break;
|
||||||
|
case UNKNOWN:
|
||||||
|
menu.setHeaderTitle(R.string.pcview_menu_header_unknown);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Inflate the context menu
|
// Inflate the context menu
|
||||||
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
||||||
computer.details.state == ComputerDetails.State.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));
|
|
||||||
}
|
}
|
||||||
else if (computer.details.pairState != PairState.PAIRED) {
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
|
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (computer.details.runningGameId != 0) {
|
if (computer.details.runningGameId != 0) {
|
||||||
@@ -331,13 +347,12 @@ public class PcView 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
|
menu.add(Menu.NONE, FULL_APP_LIST_ID, 4, getResources().getString(R.string.pcview_menu_app_list));
|
||||||
|
|
||||||
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
|
|
||||||
// it with delete which actually work
|
|
||||||
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
}
|
||||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
|
|
||||||
|
menu.add(Menu.NONE, TEST_NETWORK_ID, 5, getResources().getString(R.string.pcview_menu_test_network));
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 6, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
|
menu.add(Menu.NONE, VIEW_DETAILS_ID, 7, getResources().getString(R.string.pcview_menu_details));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -441,7 +456,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
if (toastSuccess) {
|
if (toastSuccess) {
|
||||||
// Open the app list after a successful pairing attempt
|
// Open the app list after a successful pairing attempt
|
||||||
doAppList(computer, true);
|
doAppList(computer, true, false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Start polling again if we're still in the foreground
|
// Start polling again if we're still in the foreground
|
||||||
@@ -540,7 +555,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doAppList(ComputerDetails computer, boolean newlyPaired) {
|
private void doAppList(ComputerDetails computer, boolean newlyPaired, boolean showHiddenGames) {
|
||||||
if (computer.state == ComputerDetails.State.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;
|
||||||
@@ -554,6 +569,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||||
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
|
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
|
||||||
|
i.putExtra(AppView.SHOW_HIDDEN_APPS_EXTRA, showHiddenGames);
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,8 +607,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case APP_LIST_ID:
|
case FULL_APP_LIST_ID:
|
||||||
doAppList(computer.details, false);
|
doAppList(computer.details, false, true);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case RESUME_ID:
|
case RESUME_ID:
|
||||||
@@ -624,6 +640,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case TEST_NETWORK_ID:
|
||||||
|
ServerHelper.doNetworkTest(PcView.this);
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -634,6 +654,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
|
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
|
||||||
|
|
||||||
|
// Delete hidden games preference value
|
||||||
|
getSharedPreferences(AppView.HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.remove(details.uuid)
|
||||||
|
.apply();
|
||||||
|
|
||||||
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
|
|
||||||
@@ -691,9 +717,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAdapterFragmentLayoutId() {
|
public int getAdapterFragmentLayoutId() {
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
return R.layout.pc_grid_view;
|
||||||
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
|
|
||||||
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -712,7 +736,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Pair an unpaired machine by default
|
// Pair an unpaired machine by default
|
||||||
doPair(computer.details);
|
doPair(computer.details);
|
||||||
} else {
|
} else {
|
||||||
doAppList(computer.details, false);
|
doAppList(computer.details, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -720,7 +744,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
registerForContextMenu(listView);
|
registerForContextMenu(listView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ComputerObject {
|
public static class ComputerObject {
|
||||||
public ComputerDetails details;
|
public ComputerDetails details;
|
||||||
|
|
||||||
public ComputerObject(ComputerDetails details) {
|
public ComputerObject(ComputerDetails details) {
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
|
|
||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
private AudioTrack createAudioTrack(int channelConfig, int sampleRate, int bufferSize, boolean lowLatency) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
48000,
|
sampleRate,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
bufferSize,
|
bufferSize,
|
||||||
@@ -28,7 +28,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
.setUsage(AudioAttributes.USAGE_GAME);
|
.setUsage(AudioAttributes.USAGE_GAME);
|
||||||
AudioFormat format = new AudioFormat.Builder()
|
AudioFormat format = new AudioFormat.Builder()
|
||||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||||
.setSampleRate(48000)
|
.setSampleRate(sampleRate)
|
||||||
.setChannelMask(channelConfig)
|
.setChannelMask(channelConfig)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -64,25 +64,46 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int setup(int audioConfiguration) {
|
public int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bytesPerFrame;
|
int bytesPerFrame;
|
||||||
|
|
||||||
switch (audioConfiguration)
|
switch (audioConfiguration.channelCount)
|
||||||
{
|
{
|
||||||
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
|
case 2:
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
bytesPerFrame = 2 * 240 * 2;
|
|
||||||
break;
|
break;
|
||||||
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
|
case 4:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
bytesPerFrame = 6 * 240 * 2;
|
break;
|
||||||
|
case 8:
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// AudioFormat.CHANNEL_OUT_7POINT1_SURROUND isn't available until Android 6.0,
|
||||||
|
// yet the CHANNEL_OUT_SIDE_LEFT and CHANNEL_OUT_SIDE_RIGHT constants were added
|
||||||
|
// in 5.0, so just hardcode the constant so we can work on Lollipop.
|
||||||
|
channelConfig = 0x000018fc; // AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// On KitKat and lower, creation of the AudioTrack will fail if we specify
|
||||||
|
// CHANNEL_OUT_SIDE_LEFT or CHANNEL_OUT_SIDE_RIGHT. That leaves us with
|
||||||
|
// the old CHANNEL_OUT_7POINT1 which uses left-of-center and right-of-center
|
||||||
|
// speakers instead of side-left and side-right. This non-standard layout
|
||||||
|
// is probably not what the user wants, but we don't really have a choice.
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LimeLog.severe("Decoder returned unhandled channel count");
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Audio channel config: "+String.format("0x%X", channelConfig));
|
||||||
|
|
||||||
|
bytesPerFrame = audioConfiguration.channelCount * samplesPerFrame * 2;
|
||||||
|
|
||||||
// We're not supposed to request less than the minimum
|
// We're not supposed to request less than the minimum
|
||||||
// buffer size for our buffer, but it appears that we can
|
// buffer size for our buffer, but it appears that we can
|
||||||
// do this on many devices and it lowers audio latency.
|
// do this on many devices and it lowers audio latency.
|
||||||
@@ -122,7 +143,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
case 1:
|
case 1:
|
||||||
case 3:
|
case 3:
|
||||||
// Try the larger buffer size
|
// Try the larger buffer size
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
bytesPerFrame * 2);
|
bytesPerFrame * 2);
|
||||||
@@ -135,13 +156,13 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip low latency options if hardware sample rate isn't 48000Hz
|
// Skip low latency options if hardware sample rate doesn't match the content
|
||||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != sampleRate && lowLatency) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
track = createAudioTrack(channelConfig, sampleRate, bufferSize, lowLatency);
|
||||||
track.play();
|
track.play();
|
||||||
|
|
||||||
// Successfully created working AudioTrack. We're done here.
|
// Successfully created working AudioTrack. We're done here.
|
||||||
@@ -170,14 +191,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void playDecodedAudio(short[] audioData) {
|
public void playDecodedAudio(short[] audioData) {
|
||||||
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
||||||
if (MoonBridge.getPendingAudioFrames() < 8) {
|
if (MoonBridge.getPendingAudioDuration() < 40) {
|
||||||
// This will block until the write is completed. That can cause a backlog
|
// This will block until the write is completed. That can cause a backlog
|
||||||
// of pending audio data, so we do the above check to be able to bound
|
// of pending audio data, so we do the above check to be able to bound
|
||||||
// latency at 40 ms in that situation.
|
// latency at 40 ms in that situation.
|
||||||
track.write(audioData, 0, audioData.length);
|
track.write(audioData, 0, audioData.length);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LimeLog.info("Too many pending audio frames: " + MoonBridge.getPendingAudioFrames());
|
LimeLog.info("Too much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,9 +102,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
LimeLog.warning("Corrupted certificate");
|
LimeLog.warning("Corrupted certificate");
|
||||||
return false;
|
return false;
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
// Should never happen
|
throw new RuntimeException(e);
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (InvalidKeySpecException e) {
|
} catch (InvalidKeySpecException e) {
|
||||||
// May happen if the key is corrupt
|
// May happen if the key is corrupt
|
||||||
LimeLog.warning("Corrupted key");
|
LimeLog.warning("Corrupted key");
|
||||||
@@ -124,10 +122,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
||||||
keyPairGenerator.initialize(2048);
|
keyPairGenerator.initialize(2048);
|
||||||
keyPair = keyPairGenerator.generateKeyPair();
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
// Should never happen
|
throw new RuntimeException(e);
|
||||||
e1.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
@@ -152,8 +148,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).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
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
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.UsbDevice;
|
||||||
@@ -51,7 +52,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
|
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
|
||||||
|
|
||||||
private final NvConnection conn;
|
private final NvConnection conn;
|
||||||
private final Context activityContext;
|
private final Activity activityContext;
|
||||||
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;
|
||||||
@@ -61,7 +62,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private final PreferenceConfiguration prefConfig;
|
private final PreferenceConfiguration prefConfig;
|
||||||
private short currentControllers, initialControllers;
|
private short currentControllers, initialControllers;
|
||||||
|
|
||||||
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
|
public ControllerHandler(Activity 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;
|
||||||
@@ -108,6 +109,7 @@ 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;
|
||||||
|
defaultContext.external = false;
|
||||||
|
|
||||||
// Some devices (GPD XD) have a back button which sends input events
|
// Some devices (GPD XD) have a back button which sends input events
|
||||||
// with device ID == 0. This hits the default context which would normally
|
// with device ID == 0. This hits the default context which would normally
|
||||||
@@ -146,6 +148,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if (context != null) {
|
if (context != null) {
|
||||||
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
|
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
|
||||||
releaseControllerNumber(context);
|
releaseControllerNumber(context);
|
||||||
|
context.destroy();
|
||||||
inputDeviceContexts.remove(deviceId);
|
inputDeviceContexts.remove(deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,10 +163,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public void stop() {
|
public void stop() {
|
||||||
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||||
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
||||||
|
deviceContext.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
if (deviceContext.vibrator != null) {
|
for (int i = 0; i < usbDeviceContexts.size(); i++) {
|
||||||
deviceContext.vibrator.cancel();
|
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
|
||||||
}
|
deviceContext.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceVibrator.cancel();
|
deviceVibrator.cancel();
|
||||||
@@ -215,9 +220,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if (PreferenceConfiguration.readPreferences(context).usbDriver) {
|
if (PreferenceConfiguration.readPreferences(context).usbDriver) {
|
||||||
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
||||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||||
// We explicitly ask not to claim devices that appear as InputDevices
|
// We explicitly check not to claim devices that appear as InputDevices
|
||||||
// otherwise we will double count them.
|
// otherwise we will double count them.
|
||||||
if (UsbDriverService.shouldClaimDevice(dev, false)) {
|
if (UsbDriverService.shouldClaimDevice(dev, false) &&
|
||||||
|
!UsbDriverService.isRecognizedInputDevice(dev)) {
|
||||||
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
|
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
|
||||||
mask |= 1 << count++;
|
mask |= 1 << count++;
|
||||||
}
|
}
|
||||||
@@ -263,9 +269,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
InputDeviceContext devContext = (InputDeviceContext) context;
|
InputDeviceContext devContext = (InputDeviceContext) context;
|
||||||
|
|
||||||
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
||||||
if (devContext.name != null &&
|
if (!devContext.external) {
|
||||||
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
|
|
||||||
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
|
|
||||||
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
}
|
}
|
||||||
@@ -326,6 +330,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
context.id = device.getControllerId();
|
context.id = device.getControllerId();
|
||||||
context.device = device;
|
context.device = device;
|
||||||
|
context.external = true;
|
||||||
|
|
||||||
|
context.vendorId = device.getVendorId();
|
||||||
|
context.productId = device.getProductId();
|
||||||
|
|
||||||
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
@@ -335,6 +343,24 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isExternal(InputDevice dev) {
|
private static boolean isExternal(InputDevice dev) {
|
||||||
|
// The ASUS Tinker Board inaccurately reports Bluetooth gamepads as internal,
|
||||||
|
// causing shouldIgnoreBack() to believe it should pass through back as a
|
||||||
|
// navigation event for any attached gamepads.
|
||||||
|
if (Build.MODEL.equals("Tinker Board")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String deviceName = dev.getName();
|
||||||
|
if (deviceName.contains("gpio") || // This is the back button on Shield portable consoles
|
||||||
|
deviceName.contains("joy_key") || // These are the gamepad buttons on the Archos Gamepad 2
|
||||||
|
deviceName.contains("keypad") || // These are gamepad buttons on the XPERIA Play
|
||||||
|
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.01") || // Gamepad on Shield Portable
|
||||||
|
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.02")) // Gamepad on Shield Portable (?)
|
||||||
|
{
|
||||||
|
LimeLog.info(dev.getName()+" is internal by hardcoded mapping");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
|
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
|
||||||
return dev.isExternal();
|
return dev.isExternal();
|
||||||
@@ -368,9 +394,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Classify this device as a remote by name if it has no joystick axes
|
// Classify this device as a remote by name if it has no joystick axes
|
||||||
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) == null &&
|
if (!hasJoystickAxes(dev) && devName.toLowerCase().contains("remote")) {
|
||||||
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) == null &&
|
|
||||||
devName.toLowerCase().contains("remote")) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,11 +417,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
// Note that we are explicitly NOT excluding the current device we're examining here,
|
// 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.
|
// 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 (currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT)[0]) {
|
||||||
if (keys[0]) {
|
|
||||||
foundInternalSelect = true;
|
foundInternalSelect = true;
|
||||||
}
|
}
|
||||||
if (keys[1]) {
|
|
||||||
|
// We don't check KEYCODE_BUTTON_A here, since the Shield Android TV has a
|
||||||
|
// virtual mouse device that claims to have KEYCODE_BUTTON_A. Instead, we rely
|
||||||
|
// on the SOURCE_GAMEPAD flag to be set on gamepad devices.
|
||||||
|
if (hasGamepadButtons(currentDev)) {
|
||||||
foundInternalGamepad = true;
|
foundInternalGamepad = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,8 +436,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
|
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
|
||||||
return !foundInternalGamepad || foundInternalSelect;
|
return !foundInternalGamepad || foundInternalSelect;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
return false;
|
// For external devices, we want to pass through the back button if the device
|
||||||
|
// has no gamepad axes or gamepad buttons.
|
||||||
|
return !hasJoystickAxes(dev) && !hasGamepadButtons(dev);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
|
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
|
||||||
@@ -418,15 +448,33 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
String devName = dev.getName();
|
String devName = dev.getName();
|
||||||
|
|
||||||
LimeLog.info("Creating controller context for device: "+devName);
|
LimeLog.info("Creating controller context for device: "+devName);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
LimeLog.info("Vendor ID: "+dev.getVendorId());
|
||||||
|
LimeLog.info("Product ID: "+dev.getProductId());
|
||||||
|
}
|
||||||
LimeLog.info(dev.toString());
|
LimeLog.info(dev.toString());
|
||||||
|
|
||||||
context.name = devName;
|
context.name = devName;
|
||||||
context.id = dev.getId();
|
context.id = dev.getId();
|
||||||
|
context.external = isExternal(dev);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
context.vendorId = dev.getVendorId();
|
||||||
|
context.productId = dev.getProductId();
|
||||||
|
}
|
||||||
|
|
||||||
if (dev.getVibrator().hasVibrator()) {
|
if (dev.getVibrator().hasVibrator()) {
|
||||||
context.vibrator = dev.getVibrator();
|
context.vibrator = dev.getVibrator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect if the gamepad has Mode and Select buttons according to the Android key layouts.
|
||||||
|
// We do this first because other codepaths below may override these defaults.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
boolean[] buttons = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
|
||||||
|
context.hasMode = buttons[0];
|
||||||
|
context.hasSelect = buttons[1] || buttons[2];
|
||||||
|
}
|
||||||
|
|
||||||
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 &&
|
||||||
@@ -464,25 +512,49 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||||
if (rxRange != null && ryRange != null && devName != null) {
|
if (rxRange != null && ryRange != null && devName != null) {
|
||||||
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
// Xbox controllers use RX and RY for right stick
|
if (dev.getVendorId() == 0x054c) { // Sony
|
||||||
|
if (dev.hasKeys(KeyEvent.KEYCODE_BUTTON_C)[0]) {
|
||||||
|
LimeLog.info("Detected non-standard DualShock 4 mapping");
|
||||||
|
context.isNonStandardDualShock4 = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Detected DualShock 4 (Linux standard mapping)");
|
||||||
|
context.usesLinuxGamepadStandardFaceButtons = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!devName.contains("Xbox") && !devName.contains("XBox") && !devName.contains("X-Box")) {
|
||||||
|
LimeLog.info("Assuming non-standard DualShock 4 mapping on < 4.4");
|
||||||
|
context.isNonStandardDualShock4 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.isNonStandardDualShock4) {
|
||||||
|
// The old DS4 driver uses RX and RY for triggers
|
||||||
|
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||||
|
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||||
|
|
||||||
|
// DS4 has Select and Mode buttons (possibly mapped non-standard)
|
||||||
|
context.hasSelect = true;
|
||||||
|
context.hasMode = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If it's not a non-standard DS4 controller, it's probably an Xbox controller or
|
||||||
|
// other sane controller that uses RX and RY for right stick and Z and RZ for triggers.
|
||||||
context.rightStickXAxis = MotionEvent.AXIS_RX;
|
context.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||||
context.rightStickYAxis = MotionEvent.AXIS_RY;
|
context.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||||
|
|
||||||
// Xbox controllers use Z and RZ for triggers
|
// While it's likely that Z and RZ are triggers, we may have digital trigger buttons
|
||||||
context.leftTriggerAxis = MotionEvent.AXIS_Z;
|
// instead. We must check that we actually have Z and RZ axes before assigning them.
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z) != null &&
|
||||||
context.triggersIdleNegative = true;
|
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ) != null) {
|
||||||
context.isXboxController = true;
|
context.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||||
|
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// DS4 controller uses RX and RY for triggers
|
|
||||||
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
|
||||||
context.triggersIdleNegative = true;
|
|
||||||
|
|
||||||
context.isDualShock4 = true;
|
// Triggers always idle negative on axes that are centered at zero
|
||||||
}
|
context.triggersIdleNegative = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,13 +611,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
|
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
// The device name provided is just "Gamepad" which is pretty useless, so we
|
// The device name provided is just "Gamepad" which is pretty useless, so we
|
||||||
// use VID/PID instead
|
// use VID/PID instead
|
||||||
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
|
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
|
||||||
context.backIsStart = true;
|
context.backIsStart = true;
|
||||||
context.modeIsSelect = true;
|
context.modeIsSelect = true;
|
||||||
context.triggerDeadzone = 0.30f;
|
context.triggerDeadzone = 0.30f;
|
||||||
|
context.hasSelect = true;
|
||||||
|
context.hasMode = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,11 +632,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if (devName.contains("ASUS Gamepad")) {
|
if (devName.contains("ASUS Gamepad")) {
|
||||||
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
|
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
|
||||||
// is Android 5.0 anyway
|
// is Android 5.0 anyway
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
|
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
|
||||||
if (!hasStartKey[0] && !hasStartKey[1]) {
|
if (!hasStartKey[0] && !hasStartKey[1]) {
|
||||||
context.backIsStart = true;
|
context.backIsStart = true;
|
||||||
context.modeIsSelect = true;
|
context.modeIsSelect = true;
|
||||||
|
context.hasSelect = true;
|
||||||
|
context.hasMode = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,14 +647,26 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.triggerDeadzone = 0.30f;
|
context.triggerDeadzone = 0.30f;
|
||||||
}
|
}
|
||||||
// SHIELD controllers will use small stick deadzones
|
// SHIELD controllers will use small stick deadzones
|
||||||
else if (devName.contains("SHIELD")) {
|
else if (devName.contains("SHIELD") || devName.contains("NVIDIA Controller")) {
|
||||||
context.leftStickDeadzoneRadius = 0.07f;
|
context.leftStickDeadzoneRadius = 0.07f;
|
||||||
context.rightStickDeadzoneRadius = 0.07f;
|
context.rightStickDeadzoneRadius = 0.07f;
|
||||||
|
|
||||||
|
// The big Nvidia button on the Shield controllers acts like a Search button. It
|
||||||
|
// summons the Google Assistant on the Shield TV. On my Pixel 4, it seems to do
|
||||||
|
// nothing, so we can hijack it to act like a mode button.
|
||||||
|
if (devName.contains("NVIDIA Controller v01.03") || devName.contains("NVIDIA Controller v01.04")) {
|
||||||
|
context.searchIsMode = true;
|
||||||
|
context.hasMode = 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;
|
||||||
|
|
||||||
|
// Serval has Select and Mode buttons (possibly mapped non-standard)
|
||||||
|
context.hasMode = true;
|
||||||
|
context.hasSelect = 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
|
||||||
@@ -588,7 +676,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// required fixup is ignoring the select button.
|
// required fixup is ignoring the select button.
|
||||||
else if (devName.equals("Xbox Wireless Controller")) {
|
else if (devName.equals("Xbox Wireless Controller")) {
|
||||||
if (gasRange == null) {
|
if (gasRange == null) {
|
||||||
context.isXboxBtController = true;
|
context.isNonStandardXboxBtController = true;
|
||||||
|
|
||||||
|
// Xbox One S has Select and Mode buttons (possibly mapped non-standard)
|
||||||
|
context.hasMode = true;
|
||||||
|
context.hasSelect = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -767,7 +859,58 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.isDualShock4) {
|
// Override mode button for 8BitDo controllers
|
||||||
|
if (context.vendorId == 0x2dc8 && event.getScanCode() == 306) {
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((context.vendorId == 0x057e && context.productId == 0x2009) || // Switch Pro controller
|
||||||
|
(context.vendorId == 0x0f0d && context.productId == 0x00c1)) { // HORIPAD for Switch
|
||||||
|
switch (event.getScanCode()) {
|
||||||
|
case 0x130:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_A;
|
||||||
|
case 0x131:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_B;
|
||||||
|
case 0x132:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
|
case 0x133:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||||
|
case 0x134:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||||
|
case 0x135:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||||
|
case 0x136:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_L2;
|
||||||
|
case 0x137:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_R2;
|
||||||
|
case 0x138:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
|
case 0x139:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
case 0x13A:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||||
|
case 0x13B:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||||
|
case 0x13D:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.usesLinuxGamepadStandardFaceButtons) {
|
||||||
|
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
|
||||||
|
switch (event.getScanCode()) {
|
||||||
|
case 304:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_A;
|
||||||
|
case 305:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_B;
|
||||||
|
case 307:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||||
|
case 308:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.isNonStandardDualShock4) {
|
||||||
switch (event.getScanCode()) {
|
switch (event.getScanCode()) {
|
||||||
case 304:
|
case 304:
|
||||||
return KeyEvent.KEYCODE_BUTTON_X;
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
@@ -812,7 +955,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return KeyEvent.KEYCODE_BUTTON_START;
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (context.isXboxBtController) {
|
else if (context.isNonStandardXboxBtController) {
|
||||||
switch (event.getScanCode()) {
|
switch (event.getScanCode()) {
|
||||||
case 306:
|
case 306:
|
||||||
return KeyEvent.KEYCODE_BUTTON_X;
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
@@ -841,19 +984,25 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return KeyEvent.KEYCODE_BUTTON_MODE;
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (context.vendorId == 0x0b05 && // ASUS
|
||||||
|
(context.productId == 0x7900 || // Kunai - USB
|
||||||
|
context.productId == 0x7902)) // Kunai - Bluetooth
|
||||||
|
{
|
||||||
|
// ROG Kunai has special M1-M4 buttons that are accessible via the
|
||||||
|
// joycon-style detachable controllers that we should map to Start
|
||||||
|
// and Select.
|
||||||
|
switch (event.getScanCode()) {
|
||||||
|
case 264:
|
||||||
|
case 266:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
|
||||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
case 265:
|
||||||
switch (event.getKeyCode()) {
|
case 267:
|
||||||
// These are duplicate dpad events for hat input
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
||||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
|
||||||
case KeyEvent.KEYCODE_DPAD_UP:
|
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (context.hatXAxis == -1 &&
|
|
||||||
|
if (context.hatXAxis == -1 &&
|
||||||
context.hatYAxis == -1 &&
|
context.hatYAxis == -1 &&
|
||||||
/* FIXME: There's no good way to know for sure if xpad is bound
|
/* FIXME: There's no good way to know for sure if xpad is bound
|
||||||
to this device, so we won't use the name to validate if these
|
to this device, so we won't use the name to validate if these
|
||||||
@@ -907,10 +1056,29 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// Emulate the select button with mode
|
// Emulate the select button with mode
|
||||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
}
|
}
|
||||||
|
else if (context.searchIsMode && keyCode == KeyEvent.KEYCODE_SEARCH) {
|
||||||
|
// Emulate the mode button with search
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
return keyCode;
|
return keyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int handleFlipFaceButtons(int keyCode) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_A:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_B;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_A;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_X:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
|
default:
|
||||||
|
return keyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Vector2d populateCachedVector(float x, float y) {
|
private Vector2d populateCachedVector(float x, float y) {
|
||||||
// Reinitialize our cached Vector2d object
|
// Reinitialize our cached Vector2d object
|
||||||
inputVector.initialize(x, y);
|
inputVector.initialize(x, y);
|
||||||
@@ -984,17 +1152,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||||
if (hatX < -0.5) {
|
if (hatX < -0.5) {
|
||||||
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
|
context.hatXAxisUsed = true;
|
||||||
}
|
}
|
||||||
else if (hatX > 0.5) {
|
else if (hatX > 0.5) {
|
||||||
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
|
context.hatXAxisUsed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||||
if (hatY < -0.5) {
|
if (hatY < -0.5) {
|
||||||
context.inputMap |= ControllerPacket.UP_FLAG;
|
context.inputMap |= ControllerPacket.UP_FLAG;
|
||||||
|
context.hatYAxisUsed = true;
|
||||||
}
|
}
|
||||||
else if (hatY > 0.5) {
|
else if (hatY > 0.5) {
|
||||||
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
|
context.hatYAxisUsed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,6 +1344,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
int keyCode = handleRemapping(context, event);
|
int keyCode = handleRemapping(context, event);
|
||||||
|
|
||||||
|
if (prefConfig.flipFaceButtons) {
|
||||||
|
keyCode = handleFlipFaceButtons(keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1209,15 +1386,31 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.inputMap &= ~ControllerPacket.BACK_FLAG;
|
context.inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
|
if (context.hatXAxisUsed) {
|
||||||
|
// Suppress this duplicate event if we have a hat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
|
if (context.hatXAxisUsed) {
|
||||||
|
// Suppress this duplicate event if we have a hat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_UP:
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
|
if (context.hatYAxisUsed) {
|
||||||
|
// Suppress this duplicate event if we have a hat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.inputMap &= ~ControllerPacket.UP_FLAG;
|
context.inputMap &= ~ControllerPacket.UP_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
|
if (context.hatYAxisUsed) {
|
||||||
|
// Suppress this duplicate event if we have a hat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_B:
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
@@ -1301,6 +1494,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket(context);
|
sendControllerInputPacket(context);
|
||||||
|
|
||||||
|
if (context.pendingExit && context.inputMap == 0) {
|
||||||
|
// All buttons from the quit combo are lifted. Finish the activity now.
|
||||||
|
activityContext.finish();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1311,12 +1510,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
int keyCode = handleRemapping(context, event);
|
int keyCode = handleRemapping(context, event);
|
||||||
|
|
||||||
|
if (prefConfig.flipFaceButtons) {
|
||||||
|
keyCode = handleFlipFaceButtons(keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
|
context.hasMode = true;
|
||||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_START:
|
case KeyEvent.KEYCODE_BUTTON_START:
|
||||||
@@ -1328,18 +1533,35 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BACK:
|
case KeyEvent.KEYCODE_BACK:
|
||||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||||
|
context.hasSelect = true;
|
||||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
context.inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
|
if (context.hatXAxisUsed) {
|
||||||
|
// Suppress this duplicate event if we have a hat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
|
if (context.hatXAxisUsed) {
|
||||||
|
// Suppress this duplicate event if we have a hat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_UP:
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
|
if (context.hatYAxisUsed) {
|
||||||
|
// Suppress this duplicate event if we have a hat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.inputMap |= ControllerPacket.UP_FLAG;
|
context.inputMap |= ControllerPacket.UP_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
|
if (context.hatYAxisUsed) {
|
||||||
|
// Suppress this duplicate event if we have a hat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_B:
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
@@ -1385,30 +1607,50 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start+Back+LB+RB is the quit combo
|
||||||
|
if (context.inputMap == (ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG |
|
||||||
|
ControllerPacket.LB_FLAG | ControllerPacket.RB_FLAG)) {
|
||||||
|
// Wait for the combo to lift and then finish the activity
|
||||||
|
context.pendingExit = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Start+LB acts like select for controllers with one button
|
// Start+LB acts like select for controllers with one button
|
||||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
if (!context.hasSelect) {
|
||||||
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
|
||||||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
(context.inputMap == ControllerPacket.PLAY_FLAG &&
|
||||||
{
|
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||||
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
{
|
||||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||||
|
context.inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We detect select+start or start+RB as the special button combo
|
// If there is a physical select button, we'll use Start+Select as the special button combo
|
||||||
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
// otherwise we'll use Start+RB.
|
||||||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
if (!context.hasMode) {
|
||||||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
if (context.hasSelect) {
|
||||||
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG)) {
|
||||||
{
|
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG);
|
||||||
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
|
||||||
|
|
||||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
|
||||||
|
(context.inputMap == ControllerPacket.PLAY_FLAG &&
|
||||||
|
SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||||
|
{
|
||||||
|
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||||
|
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// We don't need to send repeat key down events, but the platform
|
// We don't need to send repeat key down events, but the platform
|
||||||
// sends us events that claim to be repeats but they're from different
|
// sends us events that claim to be repeats but they're from different
|
||||||
// devices, so we just send them all and deal with some duplicates.
|
// devices, so we just send them all and deal with some duplicates.
|
||||||
@@ -1479,6 +1721,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if (context != null) {
|
if (context != null) {
|
||||||
LimeLog.info("Removed controller: "+controller.getControllerId());
|
LimeLog.info("Removed controller: "+controller.getControllerId());
|
||||||
releaseControllerNumber(context);
|
releaseControllerNumber(context);
|
||||||
|
context.destroy();
|
||||||
usbDeviceContexts.remove(controller.getControllerId());
|
usbDeviceContexts.remove(controller.getControllerId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1489,8 +1732,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
usbDeviceContexts.put(controller.getControllerId(), context);
|
usbDeviceContexts.put(controller.getControllerId(), context);
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenericControllerContext {
|
static class GenericControllerContext {
|
||||||
public int id;
|
public int id;
|
||||||
|
public boolean external;
|
||||||
|
|
||||||
|
public int vendorId;
|
||||||
|
public int productId;
|
||||||
|
|
||||||
public float leftStickDeadzoneRadius;
|
public float leftStickDeadzoneRadius;
|
||||||
public float rightStickDeadzoneRadius;
|
public float rightStickDeadzoneRadius;
|
||||||
@@ -1511,6 +1758,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public boolean mouseEmulationActive;
|
public boolean mouseEmulationActive;
|
||||||
public Timer mouseEmulationTimer;
|
public Timer mouseEmulationTimer;
|
||||||
public short mouseEmulationLastInputMap;
|
public short mouseEmulationLastInputMap;
|
||||||
|
|
||||||
|
public void destroy() {
|
||||||
|
if (mouseEmulationTimer != null) {
|
||||||
|
mouseEmulationTimer.cancel();
|
||||||
|
mouseEmulationTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputDeviceContext extends GenericControllerContext {
|
class InputDeviceContext extends GenericControllerContext {
|
||||||
@@ -1530,17 +1784,22 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
public int hatXAxis = -1;
|
public int hatXAxis = -1;
|
||||||
public int hatYAxis = -1;
|
public int hatYAxis = -1;
|
||||||
|
public boolean hatXAxisUsed, hatYAxisUsed;
|
||||||
|
|
||||||
public boolean isDualShock4;
|
public boolean isNonStandardDualShock4;
|
||||||
public boolean isXboxController;
|
public boolean usesLinuxGamepadStandardFaceButtons;
|
||||||
public boolean isXboxBtController;
|
public boolean isNonStandardXboxBtController;
|
||||||
public boolean isServal;
|
public boolean isServal;
|
||||||
public boolean backIsStart;
|
public boolean backIsStart;
|
||||||
public boolean modeIsSelect;
|
public boolean modeIsSelect;
|
||||||
|
public boolean searchIsMode;
|
||||||
public boolean ignoreBack;
|
public boolean ignoreBack;
|
||||||
public boolean hasJoystickAxes;
|
public boolean hasJoystickAxes;
|
||||||
|
public boolean pendingExit;
|
||||||
|
|
||||||
public int emulatingButtonFlags = 0;
|
public int emulatingButtonFlags = 0;
|
||||||
|
public boolean hasSelect;
|
||||||
|
public boolean hasMode;
|
||||||
|
|
||||||
// Used for OUYA bumper state tracking since they force all buttons
|
// Used for OUYA bumper state tracking since they force all buttons
|
||||||
// up when the OUYA button goes down. We watch the last time we get
|
// up when the OUYA button goes down. We watch the last time we get
|
||||||
@@ -1551,9 +1810,25 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public long lastRbUpTime = 0;
|
public long lastRbUpTime = 0;
|
||||||
|
|
||||||
public long startDownTime = 0;
|
public long startDownTime = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
super.destroy();
|
||||||
|
|
||||||
|
if (vibrator != null) {
|
||||||
|
vibrator.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UsbDeviceContext extends GenericControllerContext {
|
class UsbDeviceContext extends GenericControllerContext {
|
||||||
public AbstractController device;
|
public AbstractController device;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
super.destroy();
|
||||||
|
|
||||||
|
// Nothing for now
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,290 +8,290 @@ import android.view.KeyEvent;
|
|||||||
* @author Cameron Gutman
|
* @author Cameron Gutman
|
||||||
*/
|
*/
|
||||||
public class KeyboardTranslator {
|
public class KeyboardTranslator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GFE's prefix for every key code
|
* GFE's prefix for every key code
|
||||||
*/
|
*/
|
||||||
private static final short KEY_PREFIX = (short) 0x80;
|
private static final short KEY_PREFIX = (short) 0x80;
|
||||||
|
|
||||||
public static final int VK_0 = 48;
|
public static final int VK_0 = 48;
|
||||||
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_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_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_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;
|
||||||
public static final int VK_PAGE_UP = 33;
|
public static final int VK_PAGE_UP = 33;
|
||||||
public static final int VK_PAGE_DOWN = 34;
|
public static final int VK_PAGE_DOWN = 34;
|
||||||
public static final int VK_PLUS = 521;
|
public static final int VK_PLUS = 521;
|
||||||
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_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;
|
||||||
public static final int VK_TAB = 9;
|
public static final int VK_TAB = 9;
|
||||||
public static final int VK_LEFT = 37;
|
public static final int VK_LEFT = 37;
|
||||||
public static final int VK_RIGHT = 39;
|
public static final int VK_RIGHT = 39;
|
||||||
public static final int VK_UP = 38;
|
public static final int VK_UP = 38;
|
||||||
public static final int VK_DOWN = 40;
|
public static final int VK_DOWN = 40;
|
||||||
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) {
|
public static boolean needsShift(int keycode) {
|
||||||
switch (keycode)
|
switch (keycode)
|
||||||
{
|
{
|
||||||
case KeyEvent.KEYCODE_AT:
|
case KeyEvent.KEYCODE_AT:
|
||||||
case KeyEvent.KEYCODE_POUND:
|
case KeyEvent.KEYCODE_POUND:
|
||||||
case KeyEvent.KEYCODE_PLUS:
|
case KeyEvent.KEYCODE_PLUS:
|
||||||
case KeyEvent.KEYCODE_STAR:
|
case KeyEvent.KEYCODE_STAR:
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false;
|
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
|
||||||
* @return a GFE keycode for the given keycode
|
* @return a GFE keycode for the given keycode
|
||||||
*/
|
*/
|
||||||
public static short translate(int keycode) {
|
public static short translate(int keycode) {
|
||||||
int translated;
|
int translated;
|
||||||
|
|
||||||
// This is a poor man's mapping between Android key codes
|
// This is a poor man's mapping between Android key codes
|
||||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
// 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;
|
||||||
}
|
}
|
||||||
else if (keycode >= KeyEvent.KEYCODE_A &&
|
else if (keycode >= KeyEvent.KEYCODE_A &&
|
||||||
keycode <= KeyEvent.KEYCODE_Z) {
|
keycode <= KeyEvent.KEYCODE_Z) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
||||||
}
|
}
|
||||||
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
||||||
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
||||||
}
|
}
|
||||||
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
||||||
keycode <= KeyEvent.KEYCODE_F12) {
|
keycode <= KeyEvent.KEYCODE_F12) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch (keycode) {
|
switch (keycode) {
|
||||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||||
translated = 0xA4;
|
translated = 0xA4;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||||
translated = 0xA5;
|
translated = 0xA5;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_BACKSLASH:
|
case KeyEvent.KEYCODE_BACKSLASH:
|
||||||
translated = 0xdc;
|
translated = 0xdc;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CAPS_LOCK:
|
case KeyEvent.KEYCODE_CAPS_LOCK:
|
||||||
translated = VK_CAPS_LOCK;
|
translated = VK_CAPS_LOCK;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CLEAR:
|
case KeyEvent.KEYCODE_CLEAR:
|
||||||
translated = VK_CLEAR;
|
translated = VK_CLEAR;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_COMMA:
|
case KeyEvent.KEYCODE_COMMA:
|
||||||
translated = 0xbc;
|
translated = 0xbc;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||||
translated = 0xA2;
|
translated = 0xA2;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||||
translated = 0xA3;
|
translated = 0xA3;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DEL:
|
case KeyEvent.KEYCODE_DEL:
|
||||||
translated = VK_BACK_SPACE;
|
translated = VK_BACK_SPACE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_ENTER:
|
case KeyEvent.KEYCODE_ENTER:
|
||||||
translated = 0x0d;
|
translated = 0x0d;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_PLUS:
|
case KeyEvent.KEYCODE_PLUS:
|
||||||
case KeyEvent.KEYCODE_EQUALS:
|
case KeyEvent.KEYCODE_EQUALS:
|
||||||
translated = 0xbb;
|
translated = 0xbb;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_ESCAPE:
|
case KeyEvent.KEYCODE_ESCAPE:
|
||||||
translated = VK_ESCAPE;
|
translated = VK_ESCAPE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||||
translated = 0x2e;
|
translated = 0x2e;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_INSERT:
|
case KeyEvent.KEYCODE_INSERT:
|
||||||
translated = 0x2d;
|
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:
|
||||||
translated = 0x5b;
|
translated = 0x5b;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_META_RIGHT:
|
case KeyEvent.KEYCODE_META_RIGHT:
|
||||||
translated = 0x5c;
|
translated = 0x5c;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_MINUS:
|
case KeyEvent.KEYCODE_MINUS:
|
||||||
translated = 0xbd;
|
translated = 0xbd;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_MOVE_END:
|
case KeyEvent.KEYCODE_MOVE_END:
|
||||||
translated = VK_END;
|
translated = VK_END;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_MOVE_HOME:
|
case KeyEvent.KEYCODE_MOVE_HOME:
|
||||||
translated = VK_HOME;
|
translated = VK_HOME;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUM_LOCK:
|
case KeyEvent.KEYCODE_NUM_LOCK:
|
||||||
translated = VK_NUM_LOCK;
|
translated = VK_NUM_LOCK;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_PAGE_DOWN:
|
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||||
translated = VK_PAGE_DOWN;
|
translated = VK_PAGE_DOWN;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_PAGE_UP:
|
case KeyEvent.KEYCODE_PAGE_UP:
|
||||||
translated = VK_PAGE_UP;
|
translated = VK_PAGE_UP;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_PERIOD:
|
case KeyEvent.KEYCODE_PERIOD:
|
||||||
translated = 0xbe;
|
translated = 0xbe;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
||||||
translated = 0xdd;
|
translated = 0xdd;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
||||||
translated = VK_SCROLL_LOCK;
|
translated = VK_SCROLL_LOCK;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SEMICOLON:
|
case KeyEvent.KEYCODE_SEMICOLON:
|
||||||
translated = 0xba;
|
translated = 0xba;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||||
translated = 0xA0;
|
translated = 0xA0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||||
translated = 0xA1;
|
translated = 0xA1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SLASH:
|
case KeyEvent.KEYCODE_SLASH:
|
||||||
translated = 0xbf;
|
translated = 0xbf;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SPACE:
|
case KeyEvent.KEYCODE_SPACE:
|
||||||
translated = VK_SPACE;
|
translated = VK_SPACE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SYSRQ:
|
case KeyEvent.KEYCODE_SYSRQ:
|
||||||
// Android defines this as SysRq/PrntScrn
|
// Android defines this as SysRq/PrntScrn
|
||||||
translated = VK_PRINTSCREEN;
|
translated = VK_PRINTSCREEN;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_TAB:
|
case KeyEvent.KEYCODE_TAB:
|
||||||
translated = VK_TAB;
|
translated = VK_TAB;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
translated = VK_LEFT;
|
translated = VK_LEFT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
translated = VK_RIGHT;
|
translated = VK_RIGHT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DPAD_UP:
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
translated = VK_UP;
|
translated = VK_UP;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
translated = VK_DOWN;
|
translated = VK_DOWN;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_GRAVE:
|
case KeyEvent.KEYCODE_GRAVE:
|
||||||
translated = VK_BACK_QUOTE;
|
translated = VK_BACK_QUOTE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_APOSTROPHE:
|
case KeyEvent.KEYCODE_APOSTROPHE:
|
||||||
translated = 0xde;
|
translated = 0xde;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_BREAK:
|
case KeyEvent.KEYCODE_BREAK:
|
||||||
translated = VK_PAUSE;
|
translated = VK_PAUSE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||||
translated = 0x6F;
|
translated = 0x6F;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||||
translated = 0x6A;
|
translated = 0x6A;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||||
translated = 0x6D;
|
translated = 0x6D;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||||
translated = 0x6B;
|
translated = 0x6B;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||||
translated = 0x6E;
|
translated = 0x6E;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_AT:
|
case KeyEvent.KEYCODE_AT:
|
||||||
translated = 2 + VK_0;
|
translated = 2 + VK_0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_POUND:
|
case KeyEvent.KEYCODE_POUND:
|
||||||
translated = 3 + VK_0;
|
translated = 3 + VK_0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_STAR:
|
case KeyEvent.KEYCODE_STAR:
|
||||||
translated = 8 + VK_0;
|
translated = 8 + VK_0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
System.out.println("No key for "+keycode);
|
System.out.println("No key for "+keycode);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (short) ((KEY_PREFIX << 8) | translated);
|
return (short) ((KEY_PREFIX << 8) | translated);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -1,17 +1,22 @@
|
|||||||
package com.limelight.binding.input.capture;
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
|
||||||
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
|
||||||
|
|
||||||
|
// We extend AndroidPointerIconCaptureProvider because we want to also get the
|
||||||
|
// pointer icon hiding behavior over our stream view just in case pointer capture
|
||||||
|
// is unavailable on this system (ex: DeX, ChromeOS)
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider {
|
||||||
private View targetView;
|
private View targetView;
|
||||||
|
|
||||||
public AndroidNativePointerCaptureProvider(View targetView) {
|
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
|
||||||
|
super(activity, targetView);
|
||||||
this.targetView = targetView;
|
this.targetView = targetView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +36,6 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
|||||||
targetView.releasePointerCapture();
|
targetView.releasePointerCapture();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCapturingActive() {
|
|
||||||
return targetView.hasPointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
||||||
|
|||||||
+5
-30
@@ -7,55 +7,30 @@ import android.os.Build;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.PointerIcon;
|
import android.view.PointerIcon;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
||||||
private ViewGroup rootViewGroup;
|
private View targetView;
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
public AndroidPointerIconCaptureProvider(Activity activity) {
|
public AndroidPointerIconCaptureProvider(Activity activity, View targetView) {
|
||||||
this.context = activity;
|
this.context = activity;
|
||||||
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
this.targetView = targetView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
public static boolean isCaptureProviderSupported() {
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPointerIconOnAllViews(PointerIcon icon) {
|
|
||||||
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
|
|
||||||
View view = rootViewGroup.getChildAt(i);
|
|
||||||
view.setPointerIcon(icon);
|
|
||||||
}
|
|
||||||
rootViewGroup.setPointerIcon(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableCapture() {
|
public void enableCapture() {
|
||||||
super.enableCapture();
|
super.enableCapture();
|
||||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableCapture() {
|
public void disableCapture() {
|
||||||
super.disableCapture();
|
super.disableCapture();
|
||||||
setPointerIconOnAllViews(null);
|
targetView.setPointerIcon(null);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
|
|
||||||
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public class InputCaptureManager {
|
|||||||
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||||
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
||||||
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, activity.findViewById(R.id.surfaceView));
|
||||||
}
|
}
|
||||||
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
|
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
|
||||||
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
|
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
|
||||||
@@ -28,7 +28,7 @@ public class InputCaptureManager {
|
|||||||
// Android N's native capture can't capture over system UI elements
|
// Android N's native capture can't capture over system UI elements
|
||||||
// so we want to only use it if there's no other option.
|
// so we want to only use it if there's no other option.
|
||||||
LimeLog.info("Using Android N+ pointer hiding");
|
LimeLog.info("Using Android N+ pointer hiding");
|
||||||
return new AndroidPointerIconCaptureProvider(activity);
|
return new AndroidPointerIconCaptureProvider(activity, activity.findViewById(R.id.surfaceView));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LimeLog.info("Mouse capture not available");
|
LimeLog.info("Mouse capture not available");
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
// All mouse events should use relative axes, even if they are zero. This avoids triggering
|
||||||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
// cursor jumps if we get an event with no associated motion, like ACTION_DOWN or ACTION_UP.
|
||||||
|
return event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
|
||||||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.limelight.binding.input.driver;
|
|||||||
public abstract class AbstractController {
|
public abstract class AbstractController {
|
||||||
|
|
||||||
private final int deviceId;
|
private final int deviceId;
|
||||||
|
private final int vendorId;
|
||||||
|
private final int productId;
|
||||||
|
|
||||||
private UsbDriverListener listener;
|
private UsbDriverListener listener;
|
||||||
|
|
||||||
@@ -15,6 +17,14 @@ public abstract class AbstractController {
|
|||||||
return deviceId;
|
return deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getVendorId() {
|
||||||
|
return vendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProductId() {
|
||||||
|
return productId;
|
||||||
|
}
|
||||||
|
|
||||||
protected void setButtonFlag(int buttonFlag, int data) {
|
protected void setButtonFlag(int buttonFlag, int data) {
|
||||||
if (data != 0) {
|
if (data != 0) {
|
||||||
buttonFlags |= buttonFlag;
|
buttonFlags |= buttonFlag;
|
||||||
@@ -32,9 +42,11 @@ public abstract class AbstractController {
|
|||||||
public abstract boolean start();
|
public abstract boolean start();
|
||||||
public abstract void stop();
|
public abstract void stop();
|
||||||
|
|
||||||
public AbstractController(int deviceId, UsbDriverListener listener) {
|
public AbstractController(int deviceId, UsbDriverListener listener, int vendorId, int productId) {
|
||||||
this.deviceId = deviceId;
|
this.deviceId = deviceId;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
|
this.vendorId = vendorId;
|
||||||
|
this.productId = productId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public abstract class AbstractXboxController extends AbstractController {
|
|||||||
protected UsbEndpoint inEndpt, outEndpt;
|
protected UsbEndpoint inEndpt, outEndpt;
|
||||||
|
|
||||||
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
super(deviceId, listener);
|
super(deviceId, listener, device.getVendorId(), device.getProductId());
|
||||||
this.device = device;
|
this.device = device;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isRecognizedInputDevice(UsbDevice device) {
|
public 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.
|
||||||
@@ -191,10 +191,32 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean kernelSupportsXboxOne() {
|
||||||
|
String kernelVersion = System.getProperty("os.version");
|
||||||
|
LimeLog.info("Kernel Version: "+kernelVersion);
|
||||||
|
|
||||||
|
if (kernelVersion == null) {
|
||||||
|
// We'll assume this is some newer version of Android
|
||||||
|
// that doesn't let you read the kernel version this way.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (kernelVersion.startsWith("2.") || kernelVersion.startsWith("3.")) {
|
||||||
|
// These are old kernels that definitely don't support Xbox One controllers properly
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (kernelVersion.startsWith("4.4.") || kernelVersion.startsWith("4.9.")) {
|
||||||
|
// These aren't guaranteed to have backported kernel patches for proper Xbox One
|
||||||
|
// support (though some devices will).
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The next AOSP common kernel is 4.14 which has working Xbox One controller support
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
||||||
// We always bind to XB1 controllers but only bind to XB360 controllers
|
return ((!kernelSupportsXboxOne() || !isRecognizedInputDevice(device) || claimAllAvailable) && XboxOneController.canClaimDevice(device)) ||
|
||||||
// if we know the kernel isn't already driving this device.
|
|
||||||
return XboxOneController.canClaimDevice(device) ||
|
|
||||||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean handleRead(ByteBuffer buffer) {
|
protected boolean handleRead(ByteBuffer buffer) {
|
||||||
if (buffer.limit() < 14) {
|
if (buffer.remaining() < 14) {
|
||||||
LimeLog.severe("Read too small: "+buffer.limit());
|
LimeLog.severe("Read too small: "+buffer.remaining());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||||
|
private static final byte[] ONE_S_INIT = {0x05, 0x20, 0x00, 0x0f, 0x06};
|
||||||
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
|
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
|
||||||
0x00, 0x00, 0x00, (byte)0x80, 0x00};
|
0x00, 0x00, 0x00, (byte)0x80, 0x00};
|
||||||
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
|
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
|
||||||
@@ -38,6 +39,8 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
|
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
|
||||||
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
|
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
|
||||||
new InitPacket(0x0000, 0x0000, FW2015_INIT),
|
new InitPacket(0x0000, 0x0000, FW2015_INIT),
|
||||||
|
new InitPacket(0x045e, 0x02ea, ONE_S_INIT),
|
||||||
|
new InitPacket(0x045e, 0x0b00, ONE_S_INIT),
|
||||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
|
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
|
||||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
|
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
|
||||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
|
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
|
||||||
@@ -98,11 +101,21 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
switch (buffer.get())
|
switch (buffer.get())
|
||||||
{
|
{
|
||||||
case 0x20:
|
case 0x20:
|
||||||
|
if (buffer.remaining() < 17) {
|
||||||
|
LimeLog.severe("XBone button/axis read too small: "+buffer.remaining());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
buffer.position(buffer.position()+3);
|
buffer.position(buffer.position()+3);
|
||||||
processButtons(buffer);
|
processButtons(buffer);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case 0x07:
|
case 0x07:
|
||||||
|
if (buffer.remaining() < 4) {
|
||||||
|
LimeLog.severe("XBone mode read too small: "+buffer.remaining());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// The Xbox One S controller needs acks for mode reports otherwise
|
// The Xbox One S controller needs acks for mode reports otherwise
|
||||||
// it retransmits them forever.
|
// it retransmits them forever.
|
||||||
if (buffer.get() == 0x30) {
|
if (buffer.get() == 0x30) {
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package com.limelight.binding.input.touch;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
public class AbsoluteTouchContext implements TouchContext {
|
||||||
|
private int lastTouchDownX = 0;
|
||||||
|
private int lastTouchDownY = 0;
|
||||||
|
private long lastTouchDownTime = 0;
|
||||||
|
private int lastTouchUpX = 0;
|
||||||
|
private int lastTouchUpY = 0;
|
||||||
|
private long lastTouchUpTime = 0;
|
||||||
|
private int lastTouchLocationX = 0;
|
||||||
|
private int lastTouchLocationY = 0;
|
||||||
|
private boolean cancelled;
|
||||||
|
private boolean confirmedLongPress;
|
||||||
|
private boolean confirmedTap;
|
||||||
|
private Timer longPressTimer;
|
||||||
|
private Timer tapDownTimer;
|
||||||
|
private float accumulatedScrollDelta;
|
||||||
|
|
||||||
|
private final NvConnection conn;
|
||||||
|
private final int actionIndex;
|
||||||
|
private final View targetView;
|
||||||
|
|
||||||
|
private static final int SCROLL_SPEED_DIVISOR = 20;
|
||||||
|
|
||||||
|
private static final int LONG_PRESS_TIME_THRESHOLD = 650;
|
||||||
|
private static final int LONG_PRESS_DISTANCE_THRESHOLD = 30;
|
||||||
|
|
||||||
|
private static final int DOUBLE_TAP_TIME_THRESHOLD = 250;
|
||||||
|
private static final int DOUBLE_TAP_DISTANCE_THRESHOLD = 60;
|
||||||
|
|
||||||
|
private static final int TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD = 100;
|
||||||
|
private static final int TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD = 20;
|
||||||
|
|
||||||
|
public AbsoluteTouchContext(NvConnection conn, int actionIndex, View view)
|
||||||
|
{
|
||||||
|
this.conn = conn;
|
||||||
|
this.actionIndex = actionIndex;
|
||||||
|
this.targetView = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getActionIndex()
|
||||||
|
{
|
||||||
|
return actionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
|
||||||
|
{
|
||||||
|
if (!isNewFinger) {
|
||||||
|
// We don't handle finger transitions for absolute mode
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchLocationX = lastTouchDownX = eventX;
|
||||||
|
lastTouchLocationY = lastTouchDownY = eventY;
|
||||||
|
lastTouchDownTime = SystemClock.uptimeMillis();
|
||||||
|
cancelled = confirmedTap = confirmedLongPress = false;
|
||||||
|
accumulatedScrollDelta = 0;
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
// Start the timers
|
||||||
|
startTapDownTimer();
|
||||||
|
startLongPressTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean distanceExceeds(int deltaX, int deltaY, double limit) {
|
||||||
|
return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) > limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePosition(int eventX, int eventY) {
|
||||||
|
// We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT.
|
||||||
|
// Normalize these to the view size. We can't just drop them because we won't always get an event
|
||||||
|
// right at the boundary of the view, so dropping them would result in our cursor never really
|
||||||
|
// reaching the sides of the screen.
|
||||||
|
eventX = Math.min(Math.max(eventX, 0), targetView.getWidth());
|
||||||
|
eventY = Math.min(Math.max(eventY, 0), targetView.getHeight());
|
||||||
|
|
||||||
|
conn.sendMousePosition((short)eventX, (short)eventY, (short)targetView.getWidth(), (short)targetView.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
// Cancel the timers
|
||||||
|
cancelLongPressTimer();
|
||||||
|
cancelTapDownTimer();
|
||||||
|
|
||||||
|
// Raise the mouse buttons that we currently have down
|
||||||
|
if (confirmedLongPress) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else if (confirmedTap) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we get here, this means that the tap completed within the touch down
|
||||||
|
// deadzone time. We'll need to send the touch down and up events now at the
|
||||||
|
// original touch down position.
|
||||||
|
tapConfirmed();
|
||||||
|
try {
|
||||||
|
// FIXME: Sleeping on the main thread sucks
|
||||||
|
Thread.sleep(50);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchLocationX = lastTouchUpX = eventX;
|
||||||
|
lastTouchLocationY = lastTouchUpY = eventY;
|
||||||
|
lastTouchUpTime = SystemClock.uptimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void startLongPressTimer() {
|
||||||
|
longPressTimer = new Timer(true);
|
||||||
|
longPressTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (AbsoluteTouchContext.this) {
|
||||||
|
// Check if someone cancelled us
|
||||||
|
if (longPressTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncancellable now
|
||||||
|
longPressTimer = null;
|
||||||
|
|
||||||
|
// This timer should have already expired, but cancel it just in case
|
||||||
|
cancelTapDownTimer();
|
||||||
|
|
||||||
|
// Switch from a left click to a right click after a long press
|
||||||
|
confirmedLongPress = true;
|
||||||
|
if (confirmedTap) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, LONG_PRESS_TIME_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void cancelLongPressTimer() {
|
||||||
|
if (longPressTimer != null) {
|
||||||
|
longPressTimer.cancel();
|
||||||
|
longPressTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void startTapDownTimer() {
|
||||||
|
tapDownTimer = new Timer(true);
|
||||||
|
tapDownTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (AbsoluteTouchContext.this) {
|
||||||
|
// Check if someone cancelled us
|
||||||
|
if (tapDownTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncancellable now
|
||||||
|
tapDownTimer = null;
|
||||||
|
|
||||||
|
// Start our tap
|
||||||
|
tapConfirmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void cancelTapDownTimer() {
|
||||||
|
if (tapDownTimer != null) {
|
||||||
|
tapDownTimer.cancel();
|
||||||
|
tapDownTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tapConfirmed() {
|
||||||
|
if (confirmedTap || confirmedLongPress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmedTap = true;
|
||||||
|
cancelTapDownTimer();
|
||||||
|
|
||||||
|
// Left button down at original position
|
||||||
|
if (lastTouchDownTime - lastTouchUpTime > DOUBLE_TAP_TIME_THRESHOLD ||
|
||||||
|
distanceExceeds(lastTouchDownX - lastTouchUpX, lastTouchDownY - lastTouchUpY, DOUBLE_TAP_DISTANCE_THRESHOLD)) {
|
||||||
|
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
||||||
|
updatePosition(lastTouchDownX, lastTouchDownY);
|
||||||
|
}
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (cancelled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
if (distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, LONG_PRESS_DISTANCE_THRESHOLD)) {
|
||||||
|
// Moved too far since touch down. Cancel the long press timer.
|
||||||
|
cancelLongPressTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore motion within the deadzone period after touch down
|
||||||
|
if (confirmedTap || distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD)) {
|
||||||
|
tapConfirmed();
|
||||||
|
updatePosition(eventX, eventY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (actionIndex == 1) {
|
||||||
|
accumulatedScrollDelta += (eventY - lastTouchLocationY) / (float)SCROLL_SPEED_DIVISOR;
|
||||||
|
if ((short)accumulatedScrollDelta != 0) {
|
||||||
|
conn.sendMouseHighResScroll((short)accumulatedScrollDelta);
|
||||||
|
accumulatedScrollDelta -= (short)accumulatedScrollDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchLocationX = eventX;
|
||||||
|
lastTouchLocationY = eventY;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelTouch() {
|
||||||
|
cancelled = true;
|
||||||
|
|
||||||
|
// Cancel the timers
|
||||||
|
cancelLongPressTimer();
|
||||||
|
cancelTapDownTimer();
|
||||||
|
|
||||||
|
// Raise the mouse buttons
|
||||||
|
if (confirmedLongPress) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else if (confirmedTap) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPointerCount(int pointerCount) {
|
||||||
|
if (actionIndex == 0 && pointerCount > 1) {
|
||||||
|
cancelTouch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
-19
@@ -1,5 +1,6 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input.touch;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
@@ -8,7 +9,7 @@ import com.limelight.nvstream.input.MouseButtonPacket;
|
|||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class TouchContext {
|
public class RelativeTouchContext implements TouchContext {
|
||||||
private int lastTouchX = 0;
|
private int lastTouchX = 0;
|
||||||
private int lastTouchY = 0;
|
private int lastTouchY = 0;
|
||||||
private int originalTouchX = 0;
|
private int originalTouchX = 0;
|
||||||
@@ -17,9 +18,12 @@ public class TouchContext {
|
|||||||
private boolean cancelled;
|
private boolean cancelled;
|
||||||
private boolean confirmedMove;
|
private boolean confirmedMove;
|
||||||
private boolean confirmedDrag;
|
private boolean confirmedDrag;
|
||||||
|
private boolean confirmedScroll;
|
||||||
private Timer dragTimer;
|
private Timer dragTimer;
|
||||||
private double distanceMoved;
|
private double distanceMoved;
|
||||||
private double xFactor, yFactor;
|
private double xFactor, yFactor;
|
||||||
|
private int pointerCount;
|
||||||
|
private int maxPointerCountInGesture;
|
||||||
|
|
||||||
private final NvConnection conn;
|
private final NvConnection conn;
|
||||||
private final int actionIndex;
|
private final int actionIndex;
|
||||||
@@ -32,8 +36,10 @@ public class TouchContext {
|
|||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex,
|
private static final int SCROLL_SPEED_DIVISOR = 20;
|
||||||
int referenceWidth, int referenceHeight, View view)
|
|
||||||
|
public RelativeTouchContext(NvConnection conn, int actionIndex,
|
||||||
|
int referenceWidth, int referenceHeight, View view)
|
||||||
{
|
{
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.actionIndex = actionIndex;
|
this.actionIndex = actionIndex;
|
||||||
@@ -42,6 +48,7 @@ public class TouchContext {
|
|||||||
this.targetView = view;
|
this.targetView = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getActionIndex()
|
public int getActionIndex()
|
||||||
{
|
{
|
||||||
return actionIndex;
|
return actionIndex;
|
||||||
@@ -57,8 +64,18 @@ public class TouchContext {
|
|||||||
|
|
||||||
private boolean isTap()
|
private boolean isTap()
|
||||||
{
|
{
|
||||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
if (confirmedDrag || confirmedMove || confirmedScroll) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this input wasn't the last finger down, do not report
|
||||||
|
// a tap. This ensures we don't report duplicate taps for each
|
||||||
|
// finger on a multi-finger tap gesture
|
||||||
|
if (actionIndex + 1 != maxPointerCountInGesture) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long timeDelta = SystemClock.uptimeMillis() - originalTouchTime;
|
||||||
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +89,8 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
@Override
|
||||||
|
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
|
||||||
{
|
{
|
||||||
// Get the view dimensions to scale inputs on this touch
|
// Get the view dimensions to scale inputs on this touch
|
||||||
xFactor = referenceWidth / (double)targetView.getWidth();
|
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||||
@@ -80,18 +98,23 @@ public class TouchContext {
|
|||||||
|
|
||||||
originalTouchX = lastTouchX = eventX;
|
originalTouchX = lastTouchX = eventX;
|
||||||
originalTouchY = lastTouchY = eventY;
|
originalTouchY = lastTouchY = eventY;
|
||||||
originalTouchTime = System.currentTimeMillis();
|
|
||||||
cancelled = confirmedDrag = confirmedMove = false;
|
|
||||||
distanceMoved = 0;
|
|
||||||
|
|
||||||
if (actionIndex == 0) {
|
if (isNewFinger) {
|
||||||
// Start the timer for engaging a drag
|
maxPointerCountInGesture = pointerCount;
|
||||||
startDragTimer();
|
originalTouchTime = SystemClock.uptimeMillis();
|
||||||
|
cancelled = confirmedDrag = confirmedMove = confirmedScroll = false;
|
||||||
|
distanceMoved = 0;
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
// Start the timer for engaging a drag
|
||||||
|
startDragTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void touchUpEvent(int eventX, int eventY)
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
@@ -128,7 +151,7 @@ public class TouchContext {
|
|||||||
dragTimer.schedule(new TimerTask() {
|
dragTimer.schedule(new TimerTask() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
synchronized (TouchContext.this) {
|
synchronized (RelativeTouchContext.this) {
|
||||||
// Check if someone already set move
|
// Check if someone already set move
|
||||||
if (confirmedMove) {
|
if (confirmedMove) {
|
||||||
return;
|
return;
|
||||||
@@ -179,20 +202,33 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkForConfirmedScroll() {
|
||||||
|
// Enter scrolling mode if we've already left the tap zone
|
||||||
|
// and we have 2 fingers on screen. Leave scroll mode if
|
||||||
|
// we no longer have 2 fingers on screen
|
||||||
|
confirmedScroll = (actionIndex == 0 && pointerCount == 2 && confirmedMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean touchMoveEvent(int eventX, int eventY)
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
|
if (cancelled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||||
{
|
{
|
||||||
|
checkForConfirmedMove(eventX, eventY);
|
||||||
|
checkForConfirmedScroll();
|
||||||
|
|
||||||
// We only send moves and drags for the primary touch point
|
// We only send moves and drags for the primary touch point
|
||||||
if (actionIndex == 0) {
|
if (actionIndex == 0) {
|
||||||
checkForConfirmedMove(eventX, eventY);
|
|
||||||
|
|
||||||
int deltaX = eventX - lastTouchX;
|
int deltaX = eventX - lastTouchX;
|
||||||
int deltaY = eventY - lastTouchY;
|
int deltaY = eventY - lastTouchY;
|
||||||
|
|
||||||
// Scale the deltas based on the factors passed to our constructor
|
// Scale the deltas based on the factors passed to our constructor
|
||||||
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
|
deltaX = (int) Math.round((double) Math.abs(deltaX) * xFactor);
|
||||||
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
|
deltaY = (int) Math.round((double) Math.abs(deltaY) * yFactor);
|
||||||
|
|
||||||
// Fix up the signs
|
// Fix up the signs
|
||||||
if (eventX < lastTouchX) {
|
if (eventX < lastTouchX) {
|
||||||
@@ -202,6 +238,16 @@ public class TouchContext {
|
|||||||
deltaY = -deltaY;
|
deltaY = -deltaY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pointerCount == 2) {
|
||||||
|
if (confirmedScroll) {
|
||||||
|
deltaY /= SCROLL_SPEED_DIVISOR;
|
||||||
|
|
||||||
|
conn.sendMouseHighResScroll((short) deltaY);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
conn.sendMouseMove((short) deltaX, (short) deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
||||||
// non-zero to update lastTouch that way devices that report small touch events often
|
// non-zero to update lastTouch that way devices that report small touch events often
|
||||||
// will work correctly
|
// will work correctly
|
||||||
@@ -211,8 +257,6 @@ public class TouchContext {
|
|||||||
if (deltaY != 0) {
|
if (deltaY != 0) {
|
||||||
lastTouchY = eventY;
|
lastTouchY = eventY;
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lastTouchX = eventX;
|
lastTouchX = eventX;
|
||||||
@@ -223,6 +267,7 @@ public class TouchContext {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void cancelTouch() {
|
public void cancelTouch() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|
||||||
@@ -235,7 +280,17 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isCancelled() {
|
public boolean isCancelled() {
|
||||||
return cancelled;
|
return cancelled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPointerCount(int pointerCount) {
|
||||||
|
this.pointerCount = pointerCount;
|
||||||
|
|
||||||
|
if (pointerCount > maxPointerCountInGesture) {
|
||||||
|
maxPointerCountInGesture = pointerCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.limelight.binding.input.touch;
|
||||||
|
|
||||||
|
public interface TouchContext {
|
||||||
|
int getActionIndex();
|
||||||
|
void setPointerCount(int pointerCount);
|
||||||
|
boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger);
|
||||||
|
boolean touchMoveEvent(int eventX, int eventY);
|
||||||
|
void touchUpEvent(int eventX, int eventY);
|
||||||
|
void cancelTouch();
|
||||||
|
boolean isCancelled();
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import android.content.Context;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -210,7 +211,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, 100);
|
radius_complete = getPercent(getCorrectWidth() / 2, 100) - 2 * getDefaultStrokeWidth();
|
||||||
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);
|
||||||
|
|
||||||
@@ -270,7 +271,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
||||||
// them to make precise movements.
|
// them to make precise movements.
|
||||||
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
||||||
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
SystemClock.uptimeMillis() - timeLastClick > timeoutDeadzone ||
|
||||||
movement_radius > radius_dead_zone) ?
|
movement_radius > radius_dead_zone) ?
|
||||||
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
|
|
||||||
@@ -293,12 +294,12 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
movement_radius = getMovementRadius(relative_x, relative_y);
|
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||||
movement_angle = getAngle(relative_x, relative_y);
|
movement_angle = getAngle(relative_x, relative_y);
|
||||||
|
|
||||||
// chop radius if out of outer circle and already pressed
|
// pass touch event to parent if out of outer circle
|
||||||
|
if (movement_radius > radius_complete && !isPressed())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// chop radius if out of outer circle or near the edge
|
||||||
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||||
// not pressed already, so ignore event from outer circle
|
|
||||||
if (!isPressed()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
movement_radius = radius_complete - radius_analog_stick;
|
movement_radius = radius_complete - radius_analog_stick;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +312,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
// check for double click
|
// check for double click
|
||||||
if (lastClickState == CLICK_STATE.SINGLE &&
|
if (lastClickState == CLICK_STATE.SINGLE &&
|
||||||
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
timeLastClick + timeoutDoubleClick > SystemClock.uptimeMillis()) {
|
||||||
click_state = CLICK_STATE.DOUBLE;
|
click_state = CLICK_STATE.DOUBLE;
|
||||||
notifyOnDoubleClick();
|
notifyOnDoubleClick();
|
||||||
} else {
|
} else {
|
||||||
@@ -319,7 +320,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
notifyOnClick();
|
notifyOnClick();
|
||||||
}
|
}
|
||||||
// reset last click timestamp
|
// reset last click timestamp
|
||||||
timeLastClick = System.currentTimeMillis();
|
timeLastClick = SystemClock.uptimeMillis();
|
||||||
// set item pressed and update
|
// set item pressed and update
|
||||||
setPressed(true);
|
setPressed(true);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.content.Context;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ public class DigitalButton extends VirtualControllerElement {
|
|||||||
private TimerLongClickTimerTask longClickTimerTask = null;
|
private TimerLongClickTimerTask longClickTimerTask = null;
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
private final Paint paint = new Paint();
|
||||||
|
private final RectF rect = new RectF();
|
||||||
|
|
||||||
private int layer;
|
private int layer;
|
||||||
private DigitalButton movingButton = null;
|
private DigitalButton movingButton = null;
|
||||||
@@ -144,14 +146,18 @@ public class DigitalButton extends VirtualControllerElement {
|
|||||||
// set transparent background
|
// set transparent background
|
||||||
canvas.drawColor(Color.TRANSPARENT);
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
paint.setTextSize(getPercent(getWidth(), 30));
|
paint.setTextSize(getPercent(getWidth(), 25));
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
|
||||||
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
rect.left = rect.top = paint.getStrokeWidth();
|
||||||
|
rect.right = getWidth() - rect.left;
|
||||||
|
rect.bottom = getHeight() - rect.top;
|
||||||
|
|
||||||
|
canvas.drawOval(rect, paint);
|
||||||
|
|
||||||
if (icon != -1) {
|
if (icon != -1) {
|
||||||
Drawable d = getResources().getDrawable(icon);
|
Drawable d = getResources().getDrawable(icon);
|
||||||
|
|||||||
+18
-6
@@ -22,7 +22,7 @@ import java.util.Timer;
|
|||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class VirtualController {
|
public class VirtualController {
|
||||||
public class ControllerInputContext {
|
public static class ControllerInputContext {
|
||||||
public short inputMap = 0x0000;
|
public short inputMap = 0x0000;
|
||||||
public byte leftTrigger = 0x00;
|
public byte leftTrigger = 0x00;
|
||||||
public byte rightTrigger = 0x00;
|
public byte rightTrigger = 0x00;
|
||||||
@@ -34,7 +34,8 @@ public class VirtualController {
|
|||||||
|
|
||||||
public enum ControllerMode {
|
public enum ControllerMode {
|
||||||
Active,
|
Active,
|
||||||
Configuration
|
MoveButtons,
|
||||||
|
ResizeButtons
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
@@ -72,13 +73,16 @@ public class VirtualController {
|
|||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
String message;
|
String message;
|
||||||
|
|
||||||
if (currentMode == ControllerMode.Configuration) {
|
if (currentMode == ControllerMode.Active){
|
||||||
|
currentMode = ControllerMode.MoveButtons;
|
||||||
|
message = "Entering configuration mode (Move buttons)";
|
||||||
|
} else if (currentMode == ControllerMode.MoveButtons) {
|
||||||
|
currentMode = ControllerMode.ResizeButtons;
|
||||||
|
message = "Entering configuration mode (Resize buttons)";
|
||||||
|
} else {
|
||||||
currentMode = ControllerMode.Active;
|
currentMode = ControllerMode.Active;
|
||||||
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
||||||
message = "Exiting configuration mode";
|
message = "Exiting configuration mode";
|
||||||
} else {
|
|
||||||
currentMode = ControllerMode.Configuration;
|
|
||||||
message = "Entering configuration mode";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||||
@@ -90,6 +94,7 @@ public class VirtualController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hide() {
|
public void hide() {
|
||||||
@@ -120,6 +125,13 @@ public class VirtualController {
|
|||||||
elements.clear();
|
elements.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOpacity(int opacity) {
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
element.setOpacity(opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
||||||
elements.add(element);
|
elements.add(element);
|
||||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
||||||
|
|||||||
+115
-73
@@ -24,6 +24,11 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
return (int) (((float) total / (float) 100) * (float) percent);
|
return (int) (((float) total / (float) 100) * (float) percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The default controls are specified using a grid of 128*72 cells at 16:9
|
||||||
|
private static int screenScale(int units, int height) {
|
||||||
|
return (int) (((float) height / (float) 72) * (float) units);
|
||||||
|
}
|
||||||
|
|
||||||
private static DigitalPad createDigitalPad(
|
private static DigitalPad createDigitalPad(
|
||||||
final VirtualController controller,
|
final VirtualController controller,
|
||||||
final Context context) {
|
final Context context) {
|
||||||
@@ -145,149 +150,186 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
return new RightAnalogStick(controller, context);
|
return new RightAnalogStick(controller, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int BUTTON_BASE_X = 65;
|
|
||||||
private static final int BUTTON_BASE_Y = 5;
|
private static final int TRIGGER_L_BASE_X = 1;
|
||||||
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
private static final int TRIGGER_R_BASE_X = 92;
|
||||||
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
private static final int TRIGGER_DISTANCE = 23;
|
||||||
|
private static final int TRIGGER_BASE_Y = 31;
|
||||||
|
private static final int TRIGGER_WIDTH = 12;
|
||||||
|
private static final int TRIGGER_HEIGHT = 9;
|
||||||
|
|
||||||
|
// Face buttons are defined based on the Y button (button number 9)
|
||||||
|
private static final int BUTTON_BASE_X = 106;
|
||||||
|
private static final int BUTTON_BASE_Y = 1;
|
||||||
|
private static final int BUTTON_SIZE = 10;
|
||||||
|
|
||||||
|
private static final int DPAD_BASE_X = 4;
|
||||||
|
private static final int DPAD_BASE_Y = 41;
|
||||||
|
private static final int DPAD_SIZE = 30;
|
||||||
|
|
||||||
|
private static final int ANALOG_L_BASE_X = 6;
|
||||||
|
private static final int ANALOG_L_BASE_Y = 4;
|
||||||
|
private static final int ANALOG_R_BASE_X = 98;
|
||||||
|
private static final int ANALOG_R_BASE_Y = 42;
|
||||||
|
private static final int ANALOG_SIZE = 26;
|
||||||
|
|
||||||
|
private static final int L3_R3_BASE_Y = 60;
|
||||||
|
|
||||||
|
private static final int START_X = 83;
|
||||||
|
private static final int BACK_X = 34;
|
||||||
|
private static final int START_BACK_Y = 64;
|
||||||
|
private static final int START_BACK_WIDTH = 12;
|
||||||
|
private static final int START_BACK_HEIGHT = 7;
|
||||||
|
|
||||||
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);
|
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
||||||
|
|
||||||
|
// Displace controls on the right by this amount of pixels to account for different aspect ratios
|
||||||
|
int rightDisplacement = screen.widthPixels - screen.heightPixels * 16 / 9;
|
||||||
|
|
||||||
|
int height = screen.heightPixels;
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
||||||
if (!config.onlyL3R3)
|
if (!config.onlyL3R3)
|
||||||
{
|
{
|
||||||
controller.addElement(createDigitalPad(controller, context),
|
controller.addElement(createDigitalPad(controller, context),
|
||||||
getPercent(5, screen.widthPixels),
|
screenScale(DPAD_BASE_X, height),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
screenScale(DPAD_BASE_Y, height),
|
||||||
getPercent(30, screen.widthPixels),
|
screenScale(DPAD_SIZE, height),
|
||||||
getPercent(40, screen.heightPixels)
|
screenScale(DPAD_SIZE, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_A,
|
VirtualControllerElement.EID_A,
|
||||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
!config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
!config.flipFaceButtons ? "A" : "B", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(BUTTON_BASE_Y + 2 * BUTTON_SIZE, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_B,
|
VirtualControllerElement.EID_B,
|
||||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
config.flipFaceButtons ? "A" : "B", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
screenScale(BUTTON_BASE_X + BUTTON_SIZE, height) + rightDisplacement,
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_X,
|
VirtualControllerElement.EID_X,
|
||||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
!config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
!config.flipFaceButtons ? "X" : "Y", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
screenScale(BUTTON_BASE_X - BUTTON_SIZE, height) + rightDisplacement,
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_Y,
|
VirtualControllerElement.EID_Y,
|
||||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
config.flipFaceButtons ? "X" : "Y", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(BUTTON_BASE_Y, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createLeftTrigger(
|
controller.addElement(createLeftTrigger(
|
||||||
0, "LT", -1, controller, context),
|
1, "LT", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
screenScale(TRIGGER_L_BASE_X, height),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
screenScale(TRIGGER_BASE_Y, height),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createRightTrigger(
|
controller.addElement(createRightTrigger(
|
||||||
0, "RT", -1, controller, context),
|
1, "RT", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
screenScale(TRIGGER_BASE_Y, height),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_LB,
|
VirtualControllerElement.EID_LB,
|
||||||
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
screenScale(TRIGGER_L_BASE_X + TRIGGER_DISTANCE, height),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
screenScale(TRIGGER_BASE_Y, height),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_RB,
|
VirtualControllerElement.EID_RB,
|
||||||
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(TRIGGER_R_BASE_X, height) + rightDisplacement,
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
screenScale(TRIGGER_BASE_Y, height),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createLeftStick(controller, context),
|
controller.addElement(createLeftStick(controller, context),
|
||||||
getPercent(5, screen.widthPixels),
|
screenScale(ANALOG_L_BASE_X, height),
|
||||||
getPercent(50, screen.heightPixels),
|
screenScale(ANALOG_L_BASE_Y, height),
|
||||||
getPercent(40, screen.widthPixels),
|
screenScale(ANALOG_SIZE, height),
|
||||||
getPercent(50, screen.heightPixels)
|
screenScale(ANALOG_SIZE, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createRightStick(controller, context),
|
controller.addElement(createRightStick(controller, context),
|
||||||
getPercent(55, screen.widthPixels),
|
screenScale(ANALOG_R_BASE_X, height) + rightDisplacement,
|
||||||
getPercent(50, screen.heightPixels),
|
screenScale(ANALOG_R_BASE_Y, height),
|
||||||
getPercent(40, screen.widthPixels),
|
screenScale(ANALOG_SIZE, height),
|
||||||
getPercent(50, screen.heightPixels)
|
screenScale(ANALOG_SIZE, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_BACK,
|
VirtualControllerElement.EID_BACK,
|
||||||
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||||
getPercent(40, screen.widthPixels),
|
screenScale(BACK_X, height),
|
||||||
getPercent(90, screen.heightPixels),
|
screenScale(START_BACK_Y, height),
|
||||||
getPercent(10, screen.widthPixels),
|
screenScale(START_BACK_WIDTH, height),
|
||||||
getPercent(10, screen.heightPixels)
|
screenScale(START_BACK_HEIGHT, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_START,
|
VirtualControllerElement.EID_START,
|
||||||
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||||
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
|
screenScale(START_X, height) + rightDisplacement,
|
||||||
getPercent(90, screen.heightPixels),
|
screenScale(START_BACK_Y, height),
|
||||||
getPercent(10, screen.widthPixels),
|
screenScale(START_BACK_WIDTH, height),
|
||||||
getPercent(10, screen.heightPixels)
|
screenScale(START_BACK_HEIGHT, height)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_LSB,
|
VirtualControllerElement.EID_LSB,
|
||||||
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
||||||
getPercent(2, screen.widthPixels),
|
screenScale(TRIGGER_L_BASE_X, height),
|
||||||
getPercent(80, screen.heightPixels),
|
screenScale(L3_R3_BASE_Y, height),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_RSB,
|
VirtualControllerElement.EID_RSB,
|
||||||
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
||||||
getPercent(89, screen.widthPixels),
|
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
|
||||||
getPercent(80, screen.heightPixels),
|
screenScale(L3_R3_BASE_Y, height),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
controller.setOpacity(config.oscOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void saveProfile(final VirtualController controller,
|
public static void saveProfile(final VirtualController controller,
|
||||||
@@ -306,7 +348,7 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
prefEditor.apply();
|
prefEditor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||||
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
||||||
|
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
for (VirtualControllerElement element : controller.getElements()) {
|
||||||
@@ -324,5 +366,5 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-31
@@ -43,7 +43,8 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
|
|
||||||
private int normalColor = 0xF0888888;
|
private int normalColor = 0xF0888888;
|
||||||
protected int pressedColor = 0xF00000FF;
|
protected int pressedColor = 0xF00000FF;
|
||||||
private int configNormalColor = 0xF0FF0000;
|
private int configMoveColor = 0xF0FF0000;
|
||||||
|
private int configResizeColor = 0xF0FF00FF;
|
||||||
private int configSelectedColor = 0xF000FF00;
|
private int configSelectedColor = 0xF000FF00;
|
||||||
|
|
||||||
protected int startSize_x;
|
protected int startSize_x;
|
||||||
@@ -112,34 +113,34 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
protected void actionShowNormalColorChooser() {
|
protected void actionShowNormalColorChooser() {
|
||||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCancel(AmbilWarnaDialog dialog)
|
public void onCancel(AmbilWarnaDialog dialog)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||||
normalColor = color;
|
normalColor = color;
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
colorDialog.show();
|
colorDialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void actionShowPressedColorChooser() {
|
protected void actionShowPressedColorChooser() {
|
||||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCancel(AmbilWarnaDialog dialog) {
|
public void onCancel(AmbilWarnaDialog dialog) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||||
pressedColor = color;
|
pressedColor = color;
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
colorDialog.show();
|
colorDialog.show();
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
protected void actionEnableMove() {
|
protected void actionEnableMove() {
|
||||||
@@ -156,8 +157,12 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected int getDefaultColor() {
|
protected int getDefaultColor() {
|
||||||
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
|
||||||
configNormalColor : normalColor;
|
return configMoveColor;
|
||||||
|
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
|
||||||
|
return configResizeColor;
|
||||||
|
else
|
||||||
|
return normalColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getDefaultStrokeWidth() {
|
protected int getDefaultStrokeWidth() {
|
||||||
@@ -195,11 +200,11 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
case 2: { // set default color
|
case 2: { // set default color
|
||||||
actionShowNormalColorChooser();
|
actionShowNormalColorChooser();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 3: { // set pressed color
|
case 3: { // set pressed color
|
||||||
actionShowPressedColorChooser();
|
actionShowPressedColorChooser();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -230,7 +235,10 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
startSize_x = getWidth();
|
startSize_x = getWidth();
|
||||||
startSize_y = getHeight();
|
startSize_y = getHeight();
|
||||||
|
|
||||||
actionEnableMove();
|
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
|
||||||
|
actionEnableMove();
|
||||||
|
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
|
||||||
|
actionEnableResize();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -287,6 +295,15 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setOpacity(int opacity) {
|
||||||
|
int hexOpacity = opacity * 255 / 100;
|
||||||
|
this.normalColor = (hexOpacity << 24) | (normalColor & 0x00FFFFFF);
|
||||||
|
this.pressedColor = (hexOpacity << 24) | (pressedColor & 0x00FFFFFF);
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
protected final float getPercent(float value, float percent) {
|
protected final float getPercent(float value, float percent) {
|
||||||
return value / 100 * percent;
|
return value / 100 * percent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,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.os.SystemClock;
|
||||||
import android.util.Range;
|
import android.util.Range;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private boolean adaptivePlayback, directSubmit;
|
private boolean adaptivePlayback, directSubmit;
|
||||||
|
private boolean lowLatency;
|
||||||
private boolean constrainedHighProfile;
|
private boolean constrainedHighProfile;
|
||||||
private boolean refFrameInvalidationAvc, refFrameInvalidationHevc;
|
private boolean refFrameInvalidationAvc, refFrameInvalidationHevc;
|
||||||
private boolean refFrameInvalidationActive;
|
private boolean refFrameInvalidationActive;
|
||||||
@@ -60,6 +62,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private boolean legacyFrameDropRendering = false;
|
private boolean legacyFrameDropRendering = false;
|
||||||
private PerfOverlayListener perfListener;
|
private PerfOverlayListener perfListener;
|
||||||
|
|
||||||
|
private MediaFormat inputFormat;
|
||||||
|
private MediaFormat outputFormat;
|
||||||
|
private MediaFormat configuredFormat;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
private SeqParameterSet savedSps;
|
private SeqParameterSet savedSps;
|
||||||
|
|
||||||
@@ -104,8 +110,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork)) {
|
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());
|
||||||
|
|
||||||
// HDR implies HEVC forced on, since HEVCMain10HDR10 is required for HDR
|
// HDR implies HEVC forced on, since HEVCMain10HDR10 is required for HDR.
|
||||||
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_ON || requestedHdr) {
|
// > 4K streaming also requires HEVC, so force it on there too.
|
||||||
|
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_ON || requestedHdr ||
|
||||||
|
prefs.width > 4096 || prefs.height > 4096) {
|
||||||
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
|
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -161,7 +169,6 @@ 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);
|
|
||||||
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
|
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
|
||||||
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
|
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
|
||||||
|
|
||||||
@@ -190,8 +197,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return avcDecoder != null;
|
return avcDecoder != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean is49FpsBlacklisted() {
|
public boolean isBlacklistedForFrameRate(int frameRate) {
|
||||||
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedFor49Fps(avcDecoder.getName());
|
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedForFrameRate(avcDecoder.getName(), frameRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enableLegacyFrameDropRendering() {
|
public void enableLegacyFrameDropRendering() {
|
||||||
@@ -245,6 +252,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (width > 4096 || height > 4096) {
|
||||||
|
LimeLog.severe("> 4K streaming only supported on HEVC");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// These fixups only apply to H264 decoders
|
// These fixups only apply to H264 decoders
|
||||||
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(selectedDecoderName);
|
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(selectedDecoderName);
|
||||||
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(selectedDecoderName);
|
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(selectedDecoderName);
|
||||||
@@ -264,6 +276,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refFrameInvalidationActive = refFrameInvalidationAvc;
|
refFrameInvalidationActive = refFrameInvalidationAvc;
|
||||||
|
|
||||||
|
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(avcDecoder, mimeType);
|
||||||
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder, mimeType);
|
||||||
}
|
}
|
||||||
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
|
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
|
||||||
mimeType = "video/hevc";
|
mimeType = "video/hevc";
|
||||||
@@ -275,6 +290,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refFrameInvalidationActive = refFrameInvalidationHevc;
|
refFrameInvalidationActive = refFrameInvalidationHevc;
|
||||||
|
|
||||||
|
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(hevcDecoder, mimeType);
|
||||||
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(hevcDecoder, mimeType);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Unknown format
|
// Unknown format
|
||||||
@@ -293,6 +311,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
|
MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
|
||||||
|
|
||||||
|
// Avoid setting KEY_FRAME_RATE on Lollipop and earlier to reduce compatibility risk
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
// We use prefs.fps instead of redrawRate here because the low latency hack in Game.java
|
||||||
|
// may leave us with an odd redrawRate value like 59 or 49 which might cause the decoder
|
||||||
|
// to puke. To be safe, we'll use the unmodified value.
|
||||||
|
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, prefs.fps);
|
||||||
|
}
|
||||||
|
|
||||||
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
|
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
|
||||||
// so we don't fill these pre-KitKat
|
// so we don't fill these pre-KitKat
|
||||||
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
@@ -300,16 +326,39 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && lowLatency) {
|
||||||
// Operate at maximum rate to lower latency as much as possible on
|
videoFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
|
||||||
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
|
|
||||||
// but that will actually result in the decoder crashing if it can't satisfy
|
|
||||||
// our (ludicrous) operating rate requirement.
|
|
||||||
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
|
|
||||||
}
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
// Set the Qualcomm vendor low latency extension if the Android R option is unavailable
|
||||||
|
if (MediaCodecHelper.decoderSupportsQcomVendorLowLatency(selectedDecoderName)) {
|
||||||
|
// MediaCodec supports vendor-defined format keys using the "vendor.<extension name>.<parameter name>" syntax.
|
||||||
|
// These allow access to functionality that is not exposed through documented MediaFormat.KEY_* values.
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/common/inc/vidc_vendor_extensions.h;l=67
|
||||||
|
//
|
||||||
|
// Examples of Qualcomm's vendor extensions for Snapdragon 845:
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/vdec/src/omx_vdec_extensions.hpp
|
||||||
|
// https://cs.android.com/android/_/android/platform/hardware/qcom/sm8150/media/+/0621ceb1c1b19564999db8293574a0e12952ff6c
|
||||||
|
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MediaCodecHelper.decoderSupportsMaxOperatingRate(selectedDecoderName)) {
|
||||||
|
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configuredFormat = videoFormat;
|
||||||
|
LimeLog.info("Configuring with format: "+configuredFormat);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
|
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// This will contain the actual accepted input format attributes
|
||||||
|
inputFormat = videoDecoder.getInputFormat();
|
||||||
|
LimeLog.info("Input format: "+inputFormat);
|
||||||
|
}
|
||||||
|
|
||||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||||
|
|
||||||
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@@ -334,6 +383,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
legacyInputBuffers = videoDecoder.getInputBuffers();
|
legacyInputBuffers = videoDecoder.getInputBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return -5;
|
return -5;
|
||||||
@@ -368,7 +418,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
//
|
//
|
||||||
if (initialException != null) {
|
if (initialException != null) {
|
||||||
// This isn't the first time we've had an exception processing video
|
// This isn't the first time we've had an exception processing video
|
||||||
if (System.currentTimeMillis() - initialExceptionTimestamp >= EXCEPTION_REPORT_DELAY_MS) {
|
if (SystemClock.uptimeMillis() - initialExceptionTimestamp >= EXCEPTION_REPORT_DELAY_MS) {
|
||||||
// It's been over 3 seconds and we're still getting exceptions. Throw the original now.
|
// It's been over 3 seconds and we're still getting exceptions. Throw the original now.
|
||||||
if (!reportedCrash) {
|
if (!reportedCrash) {
|
||||||
reportedCrash = true;
|
reportedCrash = true;
|
||||||
@@ -385,7 +435,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
else {
|
else {
|
||||||
initialException = new RendererException(this, e);
|
initialException = new RendererException(this, e);
|
||||||
}
|
}
|
||||||
initialExceptionTimestamp = System.currentTimeMillis();
|
initialExceptionTimestamp = SystemClock.uptimeMillis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,7 +495,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
break;
|
break;
|
||||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
LimeLog.info("Output format changed");
|
LimeLog.info("Output format changed");
|
||||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
outputFormat = videoDecoder.getOutputFormat();
|
||||||
|
LimeLog.info("New output format: " + outputFormat);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -594,7 +645,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastFrameNumber == 0) {
|
if (lastFrameNumber == 0) {
|
||||||
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
activeWindowVideoStats.measurementStartTimestamp = SystemClock.uptimeMillis();
|
||||||
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
||||||
// 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.
|
||||||
@@ -606,7 +657,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
lastFrameNumber = frameNumber;
|
lastFrameNumber = frameNumber;
|
||||||
|
|
||||||
// Flip stats windows roughly every second
|
// Flip stats windows roughly every second
|
||||||
if (System.currentTimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) {
|
if (SystemClock.uptimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) {
|
||||||
if (prefs.enablePerfOverlay) {
|
if (prefs.enablePerfOverlay) {
|
||||||
VideoStats lastTwo = new VideoStats();
|
VideoStats lastTwo = new VideoStats();
|
||||||
lastTwo.add(lastWindowVideoStats);
|
lastTwo.add(lastWindowVideoStats);
|
||||||
@@ -639,7 +690,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
globalVideoStats.add(activeWindowVideoStats);
|
globalVideoStats.add(activeWindowVideoStats);
|
||||||
lastWindowVideoStats.copy(activeWindowVideoStats);
|
lastWindowVideoStats.copy(activeWindowVideoStats);
|
||||||
activeWindowVideoStats.clear();
|
activeWindowVideoStats.clear();
|
||||||
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
activeWindowVideoStats.measurementStartTimestamp = SystemClock.uptimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
activeWindowVideoStats.totalFramesReceived++;
|
activeWindowVideoStats.totalFramesReceived++;
|
||||||
@@ -683,17 +734,17 @@ 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 <= 720 && initialHeight <= 480) {
|
if (initialWidth <= 720 && initialHeight <= 480 && refreshRate <= 60) {
|
||||||
// Max 5 buffered frames at 720x480x60
|
// Max 5 buffered frames at 720x480x60
|
||||||
LimeLog.info("Patching level_idc to 31");
|
LimeLog.info("Patching level_idc to 31");
|
||||||
sps.levelIdc = 31;
|
sps.levelIdc = 31;
|
||||||
}
|
}
|
||||||
else if (initialWidth <= 1280 && initialHeight <= 720) {
|
else if (initialWidth <= 1280 && initialHeight <= 720 && refreshRate <= 60) {
|
||||||
// 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.levelIdc = 32;
|
sps.levelIdc = 32;
|
||||||
}
|
}
|
||||||
else if (initialWidth <= 1920 && initialHeight <= 1080) {
|
else if (initialWidth <= 1920 && initialHeight <= 1080 && refreshRate <= 60) {
|
||||||
// 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.levelIdc = 42;
|
sps.levelIdc = 42;
|
||||||
@@ -718,12 +769,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 on old devices.
|
||||||
sps.vuiParams.videoSignalTypePresentFlag = false;
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
sps.vuiParams.colourDescriptionPresentFlag = false;
|
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||||
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||||
|
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||||
|
}
|
||||||
|
|
||||||
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
|
// Some older devices used to choke on a bitstream restrictions, so we won't provide them
|
||||||
|
// unless explicitly whitelisted. For newer devices, leave the bitstream restrictions present.
|
||||||
|
if (needsSpsBitstreamFixup || isExynos4 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||||
|
|
||||||
@@ -1014,18 +1069,39 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
|
||||||
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
||||||
str += "AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper()+"\n";
|
str += "AVC supported width range: "+avcWidthRange+"\n";
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
try {
|
||||||
|
Range<Double> avcFpsRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
|
||||||
|
str += "AVC achievable FPS range: "+avcFpsRange+"\n";
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
str += "AVC achievable FPS range: UNSUPPORTED!\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
|
||||||
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
||||||
str += "HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper()+"\n";
|
str += "HEVC supported width range: "+hevcWidthRange+"\n";
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
try {
|
||||||
|
Range<Double> hevcFpsRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
|
||||||
|
str += "HEVC achievable FPS range: " + hevcFpsRange + "\n";
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
str += "HEVC achievable FPS range: UNSUPPORTED!\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
str += "Configured format: "+renderer.configuredFormat+"\n";
|
||||||
|
str += "Input format: "+renderer.inputFormat+"\n";
|
||||||
|
str += "Output format: "+renderer.outputFormat+"\n";
|
||||||
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
|
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
|
||||||
str += "GL Renderer: "+renderer.glRenderer+"\n";
|
str += "GL Renderer: "+renderer.glRenderer+"\n";
|
||||||
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
||||||
str += "Foreground: "+renderer.foreground+"\n";
|
str += "Foreground: "+renderer.foreground+"\n";
|
||||||
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
||||||
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
|
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
|
||||||
|
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n";
|
||||||
|
str += "Low latency mode: "+renderer.lowLatency+"\n";
|
||||||
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\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.prefs.bitrate+" Kbps \n";
|
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
|||||||
package com.limelight.binding.video;
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
class VideoStats {
|
class VideoStats {
|
||||||
|
|
||||||
long decoderTimeMs;
|
long decoderTimeMs;
|
||||||
@@ -24,7 +26,7 @@ class VideoStats {
|
|||||||
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert other.measurementStartTimestamp <= this.measurementStartTimestamp;
|
assert other.measurementStartTimestamp >= this.measurementStartTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void copy(VideoStats other) {
|
void copy(VideoStats other) {
|
||||||
@@ -50,7 +52,7 @@ class VideoStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VideoStatsFps getFps() {
|
VideoStatsFps getFps() {
|
||||||
float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000;
|
float elapsed = (SystemClock.uptimeMillis() - this.measurementStartTimestamp) / (float) 1000;
|
||||||
|
|
||||||
VideoStatsFps fps = new VideoStatsFps();
|
VideoStatsFps fps = new VideoStatsFps();
|
||||||
if (elapsed > 0) {
|
if (elapsed > 0) {
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.Inet4Address;
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
@@ -22,14 +26,21 @@ 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.NetHelper;
|
||||||
import com.limelight.utils.ServerHelper;
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.net.NetworkCapabilities;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
@@ -54,6 +65,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private ComputerManagerListener listener = null;
|
private ComputerManagerListener listener = null;
|
||||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
private boolean pollingActive = false;
|
private boolean pollingActive = false;
|
||||||
|
private final Lock defaultNetworkLock = new ReentrantLock();
|
||||||
|
|
||||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||||
@@ -125,6 +137,18 @@ public class ComputerManagerService extends Service {
|
|||||||
dbManager.updateComputer(existingComputer);
|
dbManager.updateComputer(existingComputer);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
try {
|
||||||
|
// If the active address is a site-local address (RFC 1918),
|
||||||
|
// then use STUN to populate the external address field if
|
||||||
|
// it's not set already.
|
||||||
|
if (details.remoteAddress == null) {
|
||||||
|
InetAddress addr = InetAddress.getByName(details.activeAddress);
|
||||||
|
if (addr.isSiteLocalAddress()) {
|
||||||
|
populateExternalAddress(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException ignored) {}
|
||||||
|
|
||||||
dbManager.updateComputer(details);
|
dbManager.updateComputer(details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +177,7 @@ public class ComputerManagerService extends Service {
|
|||||||
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
||||||
offlineCount++;
|
offlineCount++;
|
||||||
} else {
|
} else {
|
||||||
tuple.lastSuccessfulPollMs = System.currentTimeMillis();
|
tuple.lastSuccessfulPollMs = SystemClock.elapsedRealtime();
|
||||||
offlineCount = 0;
|
offlineCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +208,7 @@ public class ComputerManagerService extends Service {
|
|||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
// Enforce the poll data TTL
|
// Enforce the poll data TTL
|
||||||
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
if (SystemClock.elapsedRealtime() - 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;
|
||||||
}
|
}
|
||||||
@@ -294,6 +318,66 @@ public class ComputerManagerService extends Service {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void populateExternalAddress(ComputerDetails details) {
|
||||||
|
boolean boundToNetwork = false;
|
||||||
|
boolean activeNetworkIsVpn = NetHelper.isActiveNetworkVpn(this);
|
||||||
|
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
|
||||||
|
// Check if we're currently connected to a VPN which may send our
|
||||||
|
// STUN request from an unexpected interface
|
||||||
|
if (activeNetworkIsVpn) {
|
||||||
|
// Acquire the default network lock since we could be changing global process state
|
||||||
|
defaultNetworkLock.lock();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// On Lollipop or later, we can bind our process to the underlying interface
|
||||||
|
// to ensure our STUN request goes out on that interface or not at all (which is
|
||||||
|
// preferable to getting a VPN endpoint address back).
|
||||||
|
Network[] networks = connMgr.getAllNetworks();
|
||||||
|
for (Network net : networks) {
|
||||||
|
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(net);
|
||||||
|
if (netCaps != null) {
|
||||||
|
if (!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
||||||
|
!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||||
|
// This network looks like an underlying multicast-capable transport,
|
||||||
|
// so let's guess that it's probably where our mDNS response came from.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (connMgr.bindProcessToNetwork(net)) {
|
||||||
|
boundToNetwork = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ConnectivityManager.setProcessDefaultNetwork(net)) {
|
||||||
|
boundToNetwork = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the STUN request if we're not on a VPN or if we bound to a network
|
||||||
|
if (!activeNetworkIsVpn || boundToNetwork) {
|
||||||
|
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unbind from the network
|
||||||
|
if (boundToNetwork) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
connMgr.bindProcessToNetwork(null);
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
ConnectivityManager.setProcessDefaultNetwork(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock the network state
|
||||||
|
if (activeNetworkIsVpn) {
|
||||||
|
defaultNetworkLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MdnsDiscoveryListener createDiscoveryListener() {
|
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||||
return new MdnsDiscoveryListener() {
|
return new MdnsDiscoveryListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -308,7 +392,7 @@ public class ComputerManagerService extends Service {
|
|||||||
// our WAN address, which is also very likely the WAN address
|
// our WAN address, which is also very likely the WAN address
|
||||||
// of the PC. We can use this later to connect remotely.
|
// of the PC. We can use this later to connect remotely.
|
||||||
if (computer.getLocalAddress() instanceof Inet4Address) {
|
if (computer.getLocalAddress() instanceof Inet4Address) {
|
||||||
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
populateExternalAddress(details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (computer.getIpv6Address() != null) {
|
if (computer.getIpv6Address() != null) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.content.Context;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -13,9 +15,14 @@ import com.limelight.grid.assets.DiskAssetLoader;
|
|||||||
import com.limelight.grid.assets.MemoryAssetLoader;
|
import com.limelight.grid.assets.MemoryAssetLoader;
|
||||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
@@ -23,15 +30,63 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
private static final int SMALL_WIDTH_DP = 100;
|
private static final int SMALL_WIDTH_DP = 100;
|
||||||
private static final int LARGE_WIDTH_DP = 150;
|
private static final int LARGE_WIDTH_DP = 150;
|
||||||
|
|
||||||
private final CachedAppAssetLoader loader;
|
private final ComputerDetails computer;
|
||||||
|
private final String uniqueId;
|
||||||
|
private final boolean showHiddenApps;
|
||||||
|
|
||||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
private CachedAppAssetLoader loader;
|
||||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
|
private Set<Integer> hiddenAppIds = new HashSet<>();
|
||||||
|
private ArrayList<AppView.AppObject> allApps = new ArrayList<>();
|
||||||
|
|
||||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId, boolean showHiddenApps) {
|
||||||
|
super(context, getLayoutIdForPreferences(prefs));
|
||||||
|
|
||||||
|
this.computer = computer;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
this.showHiddenApps = showHiddenApps;
|
||||||
|
|
||||||
|
updateLayoutWithPreferences(context, prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateHiddenApps(Set<Integer> newHiddenAppIds, boolean hideImmediately) {
|
||||||
|
this.hiddenAppIds.clear();
|
||||||
|
this.hiddenAppIds.addAll(newHiddenAppIds);
|
||||||
|
|
||||||
|
if (hideImmediately) {
|
||||||
|
// Reconstruct the itemList with the new hidden app set
|
||||||
|
itemList.clear();
|
||||||
|
for (AppView.AppObject app : allApps) {
|
||||||
|
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
|
||||||
|
|
||||||
|
if (!app.isHidden || showHiddenApps) {
|
||||||
|
itemList.add(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Just update the isHidden state to show the correct UI indication
|
||||||
|
for (AppView.AppObject app : allApps) {
|
||||||
|
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
||||||
|
if (prefs.smallIconMode) {
|
||||||
|
return R.layout.app_grid_item_small;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return R.layout.app_grid_item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
|
||||||
|
int dpi = context.getResources().getDisplayMetrics().densityDpi;
|
||||||
int dp;
|
int dp;
|
||||||
|
|
||||||
if (small) {
|
if (prefs.smallIconMode) {
|
||||||
dp = SMALL_WIDTH_DP;
|
dp = SMALL_WIDTH_DP;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -45,10 +100,19 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
}
|
}
|
||||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||||
|
|
||||||
|
if (loader != null) {
|
||||||
|
// Cancel operations on the old loader
|
||||||
|
cancelQueuedOperations();
|
||||||
|
}
|
||||||
|
|
||||||
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));
|
new DiskAssetLoader(context),
|
||||||
|
BitmapFactory.decodeResource(context.getResources(), R.drawable.no_app_image));
|
||||||
|
|
||||||
|
// This will trigger the view to reload with the new layout
|
||||||
|
setLayoutId(getLayoutIdForPreferences(prefs));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
public void cancelQueuedOperations() {
|
||||||
@@ -57,8 +121,8 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
loader.freeCacheMemory();
|
loader.freeCacheMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sortList() {
|
private static void sortList(List<AppView.AppObject> list) {
|
||||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
Collections.sort(list, new Comparator<AppView.AppObject>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||||
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
||||||
@@ -67,43 +131,54 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addApp(AppView.AppObject app) {
|
public void addApp(AppView.AppObject app) {
|
||||||
// Queue a request to fetch this bitmap into cache
|
// Update hidden state
|
||||||
loader.queueCacheLoad(app.app);
|
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
|
||||||
|
|
||||||
// Add the app to our sorted list
|
// Always add the app to the all apps list
|
||||||
itemList.add(app);
|
allApps.add(app);
|
||||||
sortList();
|
sortList(allApps);
|
||||||
|
|
||||||
|
// Add the app to the adapter data if it's not hidden
|
||||||
|
if (showHiddenApps || !app.isHidden) {
|
||||||
|
// Queue a request to fetch this bitmap into cache
|
||||||
|
loader.queueCacheLoad(app.app);
|
||||||
|
|
||||||
|
// Add the app to our sorted list
|
||||||
|
itemList.add(app);
|
||||||
|
sortList(itemList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeApp(AppView.AppObject app) {
|
public void removeApp(AppView.AppObject app) {
|
||||||
itemList.remove(app);
|
itemList.remove(app);
|
||||||
|
allApps.remove(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
public void clear() {
|
||||||
|
super.clear();
|
||||||
|
allApps.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, AppView.AppObject obj) {
|
||||||
// Let the cached asset loader handle it
|
// Let the cached asset loader handle it
|
||||||
loader.populateImageView(obj.app, imgView, prgView);
|
loader.populateImageView(obj.app, imgView, txtView);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
|
|
||||||
// Select the text view so it starts marquee mode
|
|
||||||
txtView.setSelected(true);
|
|
||||||
|
|
||||||
// Return false to use the app's toString method
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
|
||||||
if (obj.isRunning) {
|
if (obj.isRunning) {
|
||||||
// Show the play button overlay
|
// Show the play button overlay
|
||||||
overlayView.setImageResource(R.drawable.ic_play);
|
overlayView.setImageResource(R.drawable.ic_play);
|
||||||
return true;
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlayView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No overlay
|
if (obj.isHidden) {
|
||||||
return false;
|
parentView.setAlpha(0.40f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parentView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,26 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
protected final int layoutId;
|
private int layoutId;
|
||||||
protected final ArrayList<T> itemList = new ArrayList<>();
|
final ArrayList<T> itemList = new ArrayList<>();
|
||||||
protected final LayoutInflater inflater;
|
private final LayoutInflater inflater;
|
||||||
|
|
||||||
public GenericGridAdapter(Context context, int layoutId) {
|
GenericGridAdapter(Context context, int layoutId) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.layoutId = layoutId;
|
this.layoutId = layoutId;
|
||||||
|
|
||||||
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setLayoutId(int layoutId) {
|
||||||
|
if (layoutId != this.layoutId) {
|
||||||
|
this.layoutId = layoutId;
|
||||||
|
|
||||||
|
// Force the view to be redrawn with the new layout
|
||||||
|
notifyDataSetInvalidated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
itemList.clear();
|
itemList.clear();
|
||||||
}
|
}
|
||||||
@@ -45,9 +54,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
|
public abstract void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, T obj);
|
||||||
public abstract boolean populateTextView(TextView txtView, T obj);
|
|
||||||
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
||||||
@@ -60,22 +67,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
TextView txtView = convertView.findViewById(R.id.grid_text);
|
TextView txtView = convertView.findViewById(R.id.grid_text);
|
||||||
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
||||||
|
|
||||||
if (imgView != null) {
|
populateView(convertView, imgView, prgView, txtView, overlayView, itemList.get(i));
|
||||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
|
||||||
imgView.setImageBitmap(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!populateTextView(txtView, itemList.get(i))) {
|
|
||||||
txtView.setText(itemList.get(i).toString());
|
|
||||||
}
|
|
||||||
if (overlayView != null) {
|
|
||||||
if (!populateOverlayView(overlayView, itemList.get(i))) {
|
|
||||||
overlayView.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
overlayView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertView;
|
return convertView;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,24 @@ 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 com.limelight.nvstream.http.PairingManager;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
||||||
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||||
|
|
||||||
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
public PcGridAdapter(Context context, PreferenceConfiguration prefs) {
|
||||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
|
super(context, getLayoutIdForPreferences(prefs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
||||||
|
return R.layout.pc_grid_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
|
||||||
|
// This will trigger the view to reload with the new layout
|
||||||
|
setLayoutId(getLayoutIdForPreferences(prefs));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addComputer(PcView.ComputerObject computer) {
|
public void addComputer(PcView.ComputerObject computer) {
|
||||||
@@ -39,7 +49,8 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
public void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, PcView.ComputerObject obj) {
|
||||||
|
imgView.setImageResource(R.drawable.ic_computer);
|
||||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||||
imgView.setAlpha(1.0f);
|
imgView.setAlpha(1.0f);
|
||||||
}
|
}
|
||||||
@@ -54,12 +65,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
prgView.setVisibility(View.INVISIBLE);
|
prgView.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
imgView.setImageResource(R.drawable.ic_computer);
|
txtView.setText(obj.details.name);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
|
||||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||||
txtView.setAlpha(1.0f);
|
txtView.setAlpha(1.0f);
|
||||||
}
|
}
|
||||||
@@ -67,16 +73,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
txtView.setAlpha(0.4f);
|
txtView.setAlpha(0.4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return false to use the computer's toString method
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
|
||||||
if (obj.details.state == ComputerDetails.State.OFFLINE) {
|
if (obj.details.state == ComputerDetails.State.OFFLINE) {
|
||||||
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
||||||
overlayView.setAlpha(0.4f);
|
overlayView.setAlpha(0.4f);
|
||||||
return true;
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
// We must check if the status is exactly online and unpaired
|
// We must check if the status is exactly online and unpaired
|
||||||
// to avoid colliding with the loading spinner when status is unknown
|
// to avoid colliding with the loading spinner when status is unknown
|
||||||
@@ -84,8 +84,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
|
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
|
||||||
overlayView.setImageResource(R.drawable.ic_lock);
|
overlayView.setImageResource(R.drawable.ic_lock);
|
||||||
overlayView.setAlpha(1.0f);
|
overlayView.setAlpha(1.0f);
|
||||||
return true;
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlayView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import android.graphics.drawable.BitmapDrawable;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
@@ -52,15 +55,17 @@ public class CachedAppAssetLoader {
|
|||||||
private final MemoryAssetLoader memoryLoader;
|
private final MemoryAssetLoader memoryLoader;
|
||||||
private final DiskAssetLoader diskLoader;
|
private final DiskAssetLoader diskLoader;
|
||||||
private final Bitmap placeholderBitmap;
|
private final Bitmap placeholderBitmap;
|
||||||
|
private final Bitmap noAppImageBitmap;
|
||||||
|
|
||||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||||
DiskAssetLoader diskLoader) {
|
DiskAssetLoader diskLoader, Bitmap noAppImageBitmap) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.scalingDivider = scalingDivider;
|
this.scalingDivider = scalingDivider;
|
||||||
this.networkLoader = networkLoader;
|
this.networkLoader = networkLoader;
|
||||||
this.memoryLoader = memoryLoader;
|
this.memoryLoader = memoryLoader;
|
||||||
this.diskLoader = diskLoader;
|
this.diskLoader = diskLoader;
|
||||||
|
this.noAppImageBitmap = noAppImageBitmap;
|
||||||
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +92,7 @@ public class CachedAppAssetLoader {
|
|||||||
memoryLoader.clearCache();
|
memoryLoader.clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
private ScaledBitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
||||||
// Try 3 times
|
// Try 3 times
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
// Check again whether we've been cancelled or the image view is gone
|
// Check again whether we've been cancelled or the image view is gone
|
||||||
@@ -108,7 +113,7 @@ public class CachedAppAssetLoader {
|
|||||||
// If there's a task associated with this load, we should return the bitmap
|
// If there's a task associated with this load, we should return the bitmap
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
return bmp;
|
return bmp;
|
||||||
}
|
}
|
||||||
@@ -130,29 +135,29 @@ public class CachedAppAssetLoader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
private class LoaderTask extends AsyncTask<LoaderTuple, Void, ScaledBitmap> {
|
||||||
private final WeakReference<ImageView> imageViewRef;
|
private final WeakReference<ImageView> imageViewRef;
|
||||||
private final WeakReference<ProgressBar> progressViewRef;
|
private final WeakReference<TextView> textViewRef;
|
||||||
private final boolean diskOnly;
|
private final boolean diskOnly;
|
||||||
|
|
||||||
private LoaderTuple tuple;
|
private LoaderTuple tuple;
|
||||||
|
|
||||||
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
public LoaderTask(ImageView imageView, TextView textView, boolean diskOnly) {
|
||||||
this.imageViewRef = new WeakReference<>(imageView);
|
this.imageViewRef = new WeakReference<>(imageView);
|
||||||
this.progressViewRef = new WeakReference<>(prgView);
|
this.textViewRef = new WeakReference<>(textView);
|
||||||
this.diskOnly = diskOnly;
|
this.diskOnly = diskOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
protected ScaledBitmap doInBackground(LoaderTuple... params) {
|
||||||
tuple = params[0];
|
tuple = params[0];
|
||||||
|
|
||||||
// Check whether it has been cancelled or the views are gone
|
// Check whether it has been cancelled or the views are gone
|
||||||
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
if (isCancelled() || imageViewRef.get() == null || textViewRef.get() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
if (bmp == null) {
|
if (bmp == null) {
|
||||||
if (!diskOnly) {
|
if (!diskOnly) {
|
||||||
// Try to load the asset from the network
|
// Try to load the asset from the network
|
||||||
@@ -181,44 +186,61 @@ public class CachedAppAssetLoader {
|
|||||||
|
|
||||||
// If the current loader task for this view isn't us, do nothing
|
// If the current loader task for this view isn't us, do nothing
|
||||||
final ImageView imageView = imageViewRef.get();
|
final ImageView imageView = imageViewRef.get();
|
||||||
final ProgressBar prgView = progressViewRef.get();
|
final TextView textView = textViewRef.get();
|
||||||
if (getLoaderTask(imageView) == this) {
|
if (getLoaderTask(imageView) == this) {
|
||||||
// Now display the progress bar since we have to hit the network
|
// Set off another loader task on the network executor. This time our AsyncDrawable
|
||||||
if (prgView != null) {
|
// will use the app image placeholder bitmap, rather than an empty bitmap.
|
||||||
prgView.setVisibility(View.VISIBLE);
|
LoaderTask task = new LoaderTask(imageView, textView, false);
|
||||||
}
|
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
|
||||||
|
|
||||||
// Set off another loader task on the network executor
|
|
||||||
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
|
||||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
|
||||||
imageView.setVisibility(View.VISIBLE);
|
|
||||||
imageView.setImageDrawable(asyncDrawable);
|
imageView.setImageDrawable(asyncDrawable);
|
||||||
|
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
textView.setVisibility(View.VISIBLE);
|
||||||
task.executeOnExecutor(networkExecutor, tuple);
|
task.executeOnExecutor(networkExecutor, tuple);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Bitmap bitmap) {
|
protected void onPostExecute(final ScaledBitmap bitmap) {
|
||||||
// Do nothing if cancelled
|
// Do nothing if cancelled
|
||||||
if (isCancelled()) {
|
if (isCancelled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
final ImageView imageView = imageViewRef.get();
|
||||||
final ProgressBar prgView = progressViewRef.get();
|
final TextView textView = textViewRef.get();
|
||||||
if (getLoaderTask(imageView) == this) {
|
if (getLoaderTask(imageView) == this) {
|
||||||
// Set the bitmap
|
// Fade in the box art
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
imageView.setImageBitmap(bitmap);
|
// Show the text if it's a placeholder
|
||||||
}
|
textView.setVisibility(isBitmapPlaceholder(bitmap) ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
// Hide the progress bar
|
if (imageView.getVisibility() == View.VISIBLE) {
|
||||||
if (prgView != null) {
|
// Fade out the placeholder first
|
||||||
prgView.setVisibility(View.INVISIBLE);
|
Animation fadeOutAnimation = AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadeout);
|
||||||
}
|
fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animation animation) {}
|
||||||
|
|
||||||
// Show the view
|
@Override
|
||||||
imageView.setVisibility(View.VISIBLE);
|
public void onAnimationEnd(Animation animation) {
|
||||||
|
// Fade in the new box art
|
||||||
|
imageView.setImageBitmap(bitmap.bitmap);
|
||||||
|
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(Animation animation) {}
|
||||||
|
});
|
||||||
|
imageView.startAnimation(fadeOutAnimation);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// View is invisible already, so just fade in the new art
|
||||||
|
imageView.setImageBitmap(bitmap.bitmap);
|
||||||
|
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,7 +318,13 @@ public class CachedAppAssetLoader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
|
private boolean isBitmapPlaceholder(ScaledBitmap bitmap) {
|
||||||
|
return (bitmap == null) ||
|
||||||
|
(bitmap.originalWidth == 130 && bitmap.originalHeight == 180) || // GFE 2.0
|
||||||
|
(bitmap.originalWidth == 628 && bitmap.originalHeight == 888); // GFE 3.0
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean populateImageView(NvApp app, ImageView imgView, TextView textView) {
|
||||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
// If there's already a task in progress for this view,
|
// If there's already a task in progress for this view,
|
||||||
@@ -306,22 +334,26 @@ public class CachedAppAssetLoader {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the progress bar always on initial load
|
// Always set the name text so we have it if needed later
|
||||||
prgView.setVisibility(View.INVISIBLE);
|
textView.setText(app.getAppName());
|
||||||
|
|
||||||
// First, try the memory cache in the current context
|
// First, try the memory cache in the current context
|
||||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
ScaledBitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
// Show the bitmap immediately
|
// Show the bitmap immediately
|
||||||
imgView.setVisibility(View.VISIBLE);
|
imgView.setVisibility(View.VISIBLE);
|
||||||
imgView.setImageBitmap(bmp);
|
imgView.setImageBitmap(bmp.bitmap);
|
||||||
|
|
||||||
|
// Show the text if it's a placeholder bitmap
|
||||||
|
textView.setVisibility(isBitmapPlaceholder(bmp) ? View.VISIBLE : View.GONE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's not in memory, create an async task to load it. This task will be attached
|
// If it's not in memory, create an async task to load it. This task will be attached
|
||||||
// via AsyncDrawable to this view.
|
// via AsyncDrawable to this view.
|
||||||
final LoaderTask task = new LoaderTask(imgView, prgView, true);
|
final LoaderTask task = new LoaderTask(imgView, textView, true);
|
||||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
||||||
|
textView.setVisibility(View.INVISIBLE);
|
||||||
imgView.setVisibility(View.INVISIBLE);
|
imgView.setVisibility(View.INVISIBLE);
|
||||||
imgView.setImageDrawable(asyncDrawable);
|
imgView.setImageDrawable(asyncDrawable);
|
||||||
|
|
||||||
@@ -330,7 +362,7 @@ public class CachedAppAssetLoader {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LoaderTuple {
|
public static class LoaderTuple {
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
public final NvApp app;
|
public final NvApp app;
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class DiskAssetLoader {
|
|||||||
return inSampleSize;
|
return inSampleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
||||||
|
|
||||||
// Don't bother with anything if it doesn't exist
|
// Don't bother with anything if it doesn't exist
|
||||||
@@ -110,27 +110,33 @@ public class DiskAssetLoader {
|
|||||||
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
||||||
|
return new ScaledBitmap(decodeOnlyOptions.outWidth, decodeOnlyOptions.outHeight, bmp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// On P, we can get a bitmap back in one step with ImageDecoder
|
// On P, we can get a bitmap back in one step with ImageDecoder
|
||||||
|
final ScaledBitmap scaledBitmap = new ScaledBitmap();
|
||||||
try {
|
try {
|
||||||
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
scaledBitmap.bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
||||||
|
scaledBitmap.originalWidth = imageInfo.getSize().getWidth();
|
||||||
|
scaledBitmap.originalHeight = imageInfo.getSize().getHeight();
|
||||||
|
|
||||||
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
||||||
if (isLowRamDevice) {
|
if (isLowRamDevice) {
|
||||||
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return scaledBitmap;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bmp;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getFile(String computerUuid, int appId) {
|
public File getFile(String computerUuid, int appId) {
|
||||||
|
|||||||
@@ -1,37 +1,74 @@
|
|||||||
package com.limelight.grid.assets;
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.util.LruCache;
|
import android.util.LruCache;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
public class MemoryAssetLoader {
|
public class MemoryAssetLoader {
|
||||||
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
||||||
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
private static final LruCache<String, ScaledBitmap> memoryCache = new LruCache<String, ScaledBitmap>(maxMemory / 16) {
|
||||||
@Override
|
@Override
|
||||||
protected int sizeOf(String key, Bitmap bitmap) {
|
protected int sizeOf(String key, ScaledBitmap bitmap) {
|
||||||
// Sizeof returns kilobytes
|
// Sizeof returns kilobytes
|
||||||
return bitmap.getByteCount() / 1024;
|
return bitmap.bitmap.getByteCount() / 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void entryRemoved(boolean evicted, String key, ScaledBitmap oldValue, ScaledBitmap newValue) {
|
||||||
|
super.entryRemoved(evicted, key, oldValue, newValue);
|
||||||
|
|
||||||
|
if (evicted) {
|
||||||
|
// Keep a soft reference around to the bitmap as long as we can
|
||||||
|
evictionCache.put(key, new SoftReference<>(oldValue));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
private static final HashMap<String, SoftReference<ScaledBitmap>> evictionCache = new HashMap<>();
|
||||||
|
|
||||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
final String key = constructKey(tuple);
|
||||||
|
|
||||||
|
ScaledBitmap bmp = memoryCache.get(key);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
LimeLog.info("LRU cache hit for tuple: "+tuple);
|
||||||
|
return bmp;
|
||||||
}
|
}
|
||||||
return bmp;
|
|
||||||
|
SoftReference<ScaledBitmap> bmpRef = evictionCache.get(key);
|
||||||
|
if (bmpRef != null) {
|
||||||
|
bmp = bmpRef.get();
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Eviction cache hit for tuple: "+tuple);
|
||||||
|
|
||||||
|
// Put this entry back into the LRU cache
|
||||||
|
evictionCache.remove(key);
|
||||||
|
memoryCache.put(key, bmp);
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The data is gone, so remove the dangling SoftReference now
|
||||||
|
evictionCache.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, ScaledBitmap bitmap) {
|
||||||
memoryCache.put(constructKey(tuple), bitmap);
|
memoryCache.put(constructKey(tuple), bitmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
|
// We must evict first because that will push all items into the eviction cache
|
||||||
memoryCache.evictAll();
|
memoryCache.evictAll();
|
||||||
|
evictionCache.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
public class ScaledBitmap {
|
||||||
|
public int originalWidth;
|
||||||
|
public int originalHeight;
|
||||||
|
|
||||||
|
public Bitmap bitmap;
|
||||||
|
|
||||||
|
public ScaledBitmap() {}
|
||||||
|
|
||||||
|
public ScaledBitmap(int originalWidth, int originalHeight, Bitmap bitmap) {
|
||||||
|
this.originalWidth = originalWidth;
|
||||||
|
this.originalHeight = originalHeight;
|
||||||
|
this.bitmap = bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,20 +5,19 @@ import java.security.cert.X509Certificate;
|
|||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
public class ConnectionContext {
|
public class ConnectionContext {
|
||||||
public String serverAddress;
|
public String serverAddress;
|
||||||
public X509Certificate serverCert;
|
public X509Certificate serverCert;
|
||||||
public StreamConfiguration streamConfig;
|
public StreamConfiguration streamConfig;
|
||||||
public NvConnectionListener connListener;
|
public NvConnectionListener connListener;
|
||||||
public SecretKey riKey;
|
public SecretKey riKey;
|
||||||
public int riKeyId;
|
public int riKeyId;
|
||||||
|
|
||||||
// This is the version quad from the appversion tag of /serverinfo
|
// This is the version quad from the appversion tag of /serverinfo
|
||||||
public String serverAppVersion;
|
public String serverAppVersion;
|
||||||
public String serverGfeVersion;
|
public String serverGfeVersion;
|
||||||
|
|
||||||
public int negotiatedWidth, negotiatedHeight;
|
public int negotiatedWidth, negotiatedHeight;
|
||||||
public int negotiatedFps;
|
public boolean negotiatedHdr;
|
||||||
public boolean negotiatedHdr;
|
|
||||||
|
|
||||||
public int videoCapabilities;
|
public int videoCapabilities;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,320 +26,347 @@ import com.limelight.nvstream.input.MouseButtonPacket;
|
|||||||
import com.limelight.nvstream.jni.MoonBridge;
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
public class NvConnection {
|
public class NvConnection {
|
||||||
// Context parameters
|
// Context parameters
|
||||||
private String host;
|
private String host;
|
||||||
private LimelightCryptoProvider cryptoProvider;
|
private LimelightCryptoProvider cryptoProvider;
|
||||||
private String uniqueId;
|
private String uniqueId;
|
||||||
private ConnectionContext context;
|
private ConnectionContext context;
|
||||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||||
private final boolean isMonkey;
|
private final boolean isMonkey;
|
||||||
|
|
||||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
||||||
{
|
{
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.cryptoProvider = cryptoProvider;
|
this.cryptoProvider = cryptoProvider;
|
||||||
this.uniqueId = uniqueId;
|
this.uniqueId = uniqueId;
|
||||||
|
|
||||||
this.context = new ConnectionContext();
|
this.context = new ConnectionContext();
|
||||||
this.context.streamConfig = config;
|
this.context.streamConfig = config;
|
||||||
this.context.serverCert = serverCert;
|
this.context.serverCert = serverCert;
|
||||||
try {
|
|
||||||
// This is unique per connection
|
|
||||||
this.context.riKey = generateRiAesKey();
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context.riKeyId = generateRiKeyId();
|
|
||||||
this.isMonkey = ActivityManager.isUserAMonkey();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
|
|
||||||
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
|
||||||
|
|
||||||
// RI keys are 128 bits
|
|
||||||
keyGen.init(128);
|
|
||||||
|
|
||||||
return keyGen.generateKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int generateRiKeyId() {
|
|
||||||
return new SecureRandom().nextInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
// This is unique per connection
|
||||||
// Interrupt any pending connection. This is thread-safe.
|
this.context.riKey = generateRiAesKey();
|
||||||
MoonBridge.interruptConnection();
|
context.riKeyId = generateRiKeyId();
|
||||||
|
|
||||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||||
// we must not invoke that functionality in parallel.
|
}
|
||||||
synchronized (MoonBridge.class) {
|
|
||||||
MoonBridge.stopConnection();
|
private static SecretKey generateRiAesKey() {
|
||||||
MoonBridge.cleanupBridge();
|
try {
|
||||||
}
|
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||||
|
|
||||||
// Now a pending connection can be processed
|
// RI keys are 128 bits
|
||||||
connectionAllowed.release();
|
keyGen.init(128);
|
||||||
}
|
|
||||||
|
|
||||||
private boolean startApp() throws XmlPullParserException, IOException
|
|
||||||
{
|
|
||||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
|
||||||
|
|
||||||
String serverInfo = h.getServerInfo();
|
return keyGen.generateKey();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
context.serverAppVersion = h.getServerVersion(serverInfo);
|
e.printStackTrace();
|
||||||
if (context.serverAppVersion == null) {
|
throw new RuntimeException(e);
|
||||||
context.connListener.displayMessage("Server version malformed");
|
}
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
private static int generateRiKeyId() {
|
||||||
|
return new SecureRandom().nextInt();
|
||||||
|
}
|
||||||
|
|
||||||
// May be missing for older servers
|
public void stop() {
|
||||||
context.serverGfeVersion = h.getGfeVersion(serverInfo);
|
// Interrupt any pending connection. This is thread-safe.
|
||||||
|
MoonBridge.interruptConnection();
|
||||||
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
|
||||||
context.connListener.displayMessage("Device not paired with computer");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.negotiatedHdr = context.streamConfig.getEnableHdr();
|
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||||
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
|
// we must not invoke that functionality in parallel.
|
||||||
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
|
synchronized (MoonBridge.class) {
|
||||||
context.negotiatedHdr = false;
|
MoonBridge.stopConnection();
|
||||||
}
|
MoonBridge.cleanupBridge();
|
||||||
|
}
|
||||||
//
|
|
||||||
// Decide on negotiated stream parameters now
|
|
||||||
//
|
|
||||||
|
|
||||||
// Check for a supported stream resolution
|
|
||||||
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
|
||||||
// Client wants 4K but the server can't do it
|
|
||||||
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
|
||||||
|
|
||||||
// Lower resolution to 1080p
|
|
||||||
context.negotiatedWidth = 1920;
|
|
||||||
context.negotiatedHeight = 1080;
|
|
||||||
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Take what the client wanted
|
|
||||||
context.negotiatedWidth = context.streamConfig.getWidth();
|
|
||||||
context.negotiatedHeight = context.streamConfig.getHeight();
|
|
||||||
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Video stream format will be decided during the RTSP handshake
|
|
||||||
//
|
|
||||||
|
|
||||||
NvApp app = context.streamConfig.getApp();
|
|
||||||
|
|
||||||
// If the client did not provide an exact app ID, do a lookup with the applist
|
|
||||||
if (!context.streamConfig.getApp().isInitialized()) {
|
|
||||||
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
|
|
||||||
app = h.getAppByName(context.streamConfig.getApp().getAppName());
|
|
||||||
if (app == null) {
|
|
||||||
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a game running, resume it
|
|
||||||
if (h.getCurrentGame(serverInfo) != 0) {
|
|
||||||
try {
|
|
||||||
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
|
|
||||||
if (!h.resumeApp(context)) {
|
|
||||||
context.connListener.displayMessage("Failed to resume existing session");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return quitAndLaunch(h, context);
|
|
||||||
}
|
|
||||||
} catch (GfeHttpResponseException e) {
|
|
||||||
if (e.getErrorCode() == 470) {
|
|
||||||
// This is the error you get when you try to resume a session that's not yours.
|
|
||||||
// Because this is fairly common, we'll display a more detailed message.
|
|
||||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
|
||||||
" so it cannot be resumed. End streaming on the original " +
|
|
||||||
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if (e.getErrorCode() == 525) {
|
|
||||||
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
|
|
||||||
"quit the session and start streaming again.");
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Resumed existing game session");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return launchNotRunningApp(h, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
|
// Now a pending connection can be processed
|
||||||
XmlPullParserException {
|
connectionAllowed.release();
|
||||||
try {
|
}
|
||||||
if (!h.quitApp()) {
|
|
||||||
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
|
private boolean startApp() throws XmlPullParserException, IOException
|
||||||
return false;
|
{
|
||||||
}
|
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||||
} catch (GfeHttpResponseException e) {
|
|
||||||
if (e.getErrorCode() == 599) {
|
|
||||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
|
||||||
" so it cannot be quit. End streaming on the original " +
|
|
||||||
"device or the PC itself. (Error code: "+e.getErrorCode()+")");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return launchNotRunningApp(h, context);
|
String serverInfo = h.getServerInfo();
|
||||||
}
|
|
||||||
|
context.serverAppVersion = h.getServerVersion(serverInfo);
|
||||||
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
|
if (context.serverAppVersion == null) {
|
||||||
throws IOException, XmlPullParserException {
|
context.connListener.displayMessage("Server version malformed");
|
||||||
// Launch the app since it's not running
|
return false;
|
||||||
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
|
}
|
||||||
context.connListener.displayMessage("Failed to launch application");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Launched new game session");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
|
// May be missing for older servers
|
||||||
{
|
context.serverGfeVersion = h.getGfeVersion(serverInfo);
|
||||||
new Thread(new Runnable() {
|
|
||||||
public void run() {
|
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
||||||
context.connListener = connectionListener;
|
context.connListener.displayMessage("Device not paired with computer");
|
||||||
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
String appName = context.streamConfig.getApp().getAppName();
|
context.negotiatedHdr = context.streamConfig.getEnableHdr();
|
||||||
|
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
|
||||||
|
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
|
||||||
|
context.negotiatedHdr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Decide on negotiated stream parameters now
|
||||||
|
//
|
||||||
|
|
||||||
|
// Check for a supported stream resolution
|
||||||
|
if ((context.streamConfig.getWidth() > 4096 || context.streamConfig.getHeight() > 4096) &&
|
||||||
|
(h.getServerCodecModeSupport(serverInfo) & 0x200) == 0) {
|
||||||
|
context.connListener.displayMessage("Your host PC does not support streaming at resolutions above 4K.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if ((context.streamConfig.getWidth() > 4096 || context.streamConfig.getHeight() > 4096) &&
|
||||||
|
!context.streamConfig.getHevcSupported()) {
|
||||||
|
context.connListener.displayMessage("Your streaming device must support H.265 to stream at resolutions above 4K.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
||||||
|
// Client wants 4K but the server can't do it
|
||||||
|
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
||||||
|
|
||||||
|
// Lower resolution to 1080p
|
||||||
|
context.negotiatedWidth = 1920;
|
||||||
|
context.negotiatedHeight = 1080;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Take what the client wanted
|
||||||
|
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||||
|
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Video stream format will be decided during the RTSP handshake
|
||||||
|
//
|
||||||
|
|
||||||
|
NvApp app = context.streamConfig.getApp();
|
||||||
|
|
||||||
|
// If the client did not provide an exact app ID, do a lookup with the applist
|
||||||
|
if (!context.streamConfig.getApp().isInitialized()) {
|
||||||
|
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
|
||||||
|
app = h.getAppByName(context.streamConfig.getApp().getAppName());
|
||||||
|
if (app == null) {
|
||||||
|
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a game running, resume it
|
||||||
|
if (h.getCurrentGame(serverInfo) != 0) {
|
||||||
|
try {
|
||||||
|
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
|
||||||
|
if (!h.resumeApp(context)) {
|
||||||
|
context.connListener.displayMessage("Failed to resume existing session");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return quitAndLaunch(h, context);
|
||||||
|
}
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 470) {
|
||||||
|
// This is the error you get when you try to resume a session that's not yours.
|
||||||
|
// Because this is fairly common, we'll display a more detailed message.
|
||||||
|
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||||
|
" so it cannot be resumed. End streaming on the original " +
|
||||||
|
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (e.getErrorCode() == 525) {
|
||||||
|
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
|
||||||
|
"quit the session and start streaming again.");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Resumed existing game session");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return launchNotRunningApp(h, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context.serverAddress = host;
|
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
|
||||||
context.connListener.stageStarting(appName);
|
XmlPullParserException {
|
||||||
|
try {
|
||||||
|
if (!h.quitApp()) {
|
||||||
|
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 599) {
|
||||||
|
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||||
|
" so it cannot be quit. End streaming on the original " +
|
||||||
|
"device or the PC itself. (Error code: "+e.getErrorCode()+")");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
return launchNotRunningApp(h, context);
|
||||||
if (!startApp()) {
|
}
|
||||||
context.connListener.stageFailed(appName, 0);
|
|
||||||
return;
|
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
|
||||||
}
|
throws IOException, XmlPullParserException {
|
||||||
context.connListener.stageComplete(appName);
|
// Launch the app since it's not running
|
||||||
} catch (XmlPullParserException | IOException e) {
|
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
|
||||||
e.printStackTrace();
|
context.connListener.displayMessage("Failed to launch application");
|
||||||
context.connListener.displayMessage(e.getMessage());
|
return false;
|
||||||
context.connListener.stageFailed(appName, 0);
|
}
|
||||||
return;
|
|
||||||
}
|
LimeLog.info("Launched new game session");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
ByteBuffer ib = ByteBuffer.allocate(16);
|
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
|
||||||
ib.putInt(context.riKeyId);
|
{
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
context.connListener = connectionListener;
|
||||||
|
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
|
||||||
|
|
||||||
// Acquire the connection semaphore to ensure we only have one
|
String appName = context.streamConfig.getApp().getAppName();
|
||||||
// connection going at once.
|
|
||||||
try {
|
|
||||||
connectionAllowed.acquire();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
context.connListener.displayMessage(e.getMessage());
|
|
||||||
context.connListener.stageFailed(appName, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
context.serverAddress = host;
|
||||||
// we must not invoke that functionality in parallel.
|
context.connListener.stageStarting(appName);
|
||||||
synchronized (MoonBridge.class) {
|
|
||||||
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
|
||||||
int ret = MoonBridge.startConnection(context.serverAddress,
|
|
||||||
context.serverAppVersion, context.serverGfeVersion,
|
|
||||||
context.negotiatedWidth, context.negotiatedHeight,
|
|
||||||
context.negotiatedFps, context.streamConfig.getBitrate(),
|
|
||||||
context.streamConfig.getMaxPacketSize(),
|
|
||||||
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
|
|
||||||
context.streamConfig.getHevcSupported(),
|
|
||||||
context.negotiatedHdr,
|
|
||||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
|
||||||
context.streamConfig.getClientRefreshRateX100(),
|
|
||||||
context.riKey.getEncoded(), ib.array(),
|
|
||||||
context.videoCapabilities);
|
|
||||||
if (ret != 0) {
|
|
||||||
// LiStartConnection() failed, so the caller is not expected
|
|
||||||
// to stop the connection themselves. We need to release their
|
|
||||||
// semaphore count for them.
|
|
||||||
connectionAllowed.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendMouseMove(final short deltaX, final short deltaY)
|
|
||||||
{
|
|
||||||
if (!isMonkey) {
|
|
||||||
MoonBridge.sendMouseMove(deltaX, deltaY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendMouseButtonDown(final byte mouseButton)
|
|
||||||
{
|
|
||||||
if (!isMonkey) {
|
|
||||||
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendMouseButtonUp(final byte mouseButton)
|
|
||||||
{
|
|
||||||
if (!isMonkey) {
|
|
||||||
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendControllerInput(final short controllerNumber,
|
|
||||||
final short activeGamepadMask, final short buttonFlags,
|
|
||||||
final byte leftTrigger, final byte rightTrigger,
|
|
||||||
final short leftStickX, final short leftStickY,
|
|
||||||
final short rightStickX, final short rightStickY)
|
|
||||||
{
|
|
||||||
if (!isMonkey) {
|
|
||||||
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
|
|
||||||
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendControllerInput(final short buttonFlags,
|
|
||||||
final byte leftTrigger, final byte rightTrigger,
|
|
||||||
final short leftStickX, final short leftStickY,
|
|
||||||
final short rightStickX, final short rightStickY)
|
|
||||||
{
|
|
||||||
if (!isMonkey) {
|
|
||||||
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
|
|
||||||
leftStickY, rightStickX, rightStickY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
|
|
||||||
if (!isMonkey) {
|
|
||||||
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendMouseScroll(final byte scrollClicks) {
|
|
||||||
if (!isMonkey) {
|
|
||||||
MoonBridge.sendMouseScroll(scrollClicks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
try {
|
||||||
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
if (!startApp()) {
|
||||||
}
|
context.connListener.stageFailed(appName, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.connListener.stageComplete(appName);
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
context.connListener.displayMessage(e.getMessage());
|
||||||
|
context.connListener.stageFailed(appName, 0, e.getErrorCode());
|
||||||
|
return;
|
||||||
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
context.connListener.displayMessage(e.getMessage());
|
||||||
|
context.connListener.stageFailed(appName, MoonBridge.ML_PORT_FLAG_TCP_47984 | MoonBridge.ML_PORT_FLAG_TCP_47989, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer ib = ByteBuffer.allocate(16);
|
||||||
|
ib.putInt(context.riKeyId);
|
||||||
|
|
||||||
|
// Acquire the connection semaphore to ensure we only have one
|
||||||
|
// connection going at once.
|
||||||
|
try {
|
||||||
|
connectionAllowed.acquire();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
context.connListener.displayMessage(e.getMessage());
|
||||||
|
context.connListener.stageFailed(appName, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||||
|
// we must not invoke that functionality in parallel.
|
||||||
|
synchronized (MoonBridge.class) {
|
||||||
|
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||||
|
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||||
|
context.serverAppVersion, context.serverGfeVersion,
|
||||||
|
context.negotiatedWidth, context.negotiatedHeight,
|
||||||
|
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
||||||
|
context.streamConfig.getMaxPacketSize(),
|
||||||
|
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration().toInt(),
|
||||||
|
context.streamConfig.getHevcSupported(),
|
||||||
|
context.negotiatedHdr,
|
||||||
|
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||||
|
context.streamConfig.getClientRefreshRateX100(),
|
||||||
|
context.riKey.getEncoded(), ib.array(),
|
||||||
|
context.videoCapabilities);
|
||||||
|
if (ret != 0) {
|
||||||
|
// LiStartConnection() failed, so the caller is not expected
|
||||||
|
// to stop the connection themselves. We need to release their
|
||||||
|
// semaphore count for them.
|
||||||
|
connectionAllowed.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseMove(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseButtonDown(final byte mouseButton)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseButtonUp(final byte mouseButton)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendControllerInput(final short controllerNumber,
|
||||||
|
final short activeGamepadMask, final short buttonFlags,
|
||||||
|
final byte leftTrigger, final byte rightTrigger,
|
||||||
|
final short leftStickX, final short leftStickY,
|
||||||
|
final short rightStickX, final short rightStickY)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
|
||||||
|
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendControllerInput(final short buttonFlags,
|
||||||
|
final byte leftTrigger, final byte rightTrigger,
|
||||||
|
final short leftStickX, final short leftStickY,
|
||||||
|
final short rightStickX, final short rightStickY)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
|
||||||
|
leftStickY, rightStickX, rightStickY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseScroll(final byte scrollClicks) {
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseScroll(scrollClicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseHighResScroll(final short scrollAmount) {
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseHighResScroll(scrollAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
||||||
|
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package com.limelight.nvstream;
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
public interface NvConnectionListener {
|
public interface NvConnectionListener {
|
||||||
void stageStarting(String stage);
|
void stageStarting(String stage);
|
||||||
void stageComplete(String stage);
|
void stageComplete(String stage);
|
||||||
void stageFailed(String stage, long errorCode);
|
void stageFailed(String stage, int portFlags, int errorCode);
|
||||||
|
|
||||||
void connectionStarted();
|
void connectionStarted();
|
||||||
void connectionTerminated(long errorCode);
|
void connectionTerminated(int errorCode);
|
||||||
void connectionStatusUpdate(int connectionStatus);
|
void connectionStatusUpdate(int connectionStatus);
|
||||||
|
|
||||||
void displayMessage(String message);
|
void displayMessage(String message);
|
||||||
void displayTransientMessage(String message);
|
void displayTransientMessage(String message);
|
||||||
|
|
||||||
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,230 +4,211 @@ import com.limelight.nvstream.http.NvApp;
|
|||||||
import com.limelight.nvstream.jni.MoonBridge;
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
public class StreamConfiguration {
|
public class StreamConfiguration {
|
||||||
public static final int INVALID_APP_ID = 0;
|
public static final int INVALID_APP_ID = 0;
|
||||||
|
|
||||||
public static final int STREAM_CFG_LOCAL = 0;
|
public static final int STREAM_CFG_LOCAL = 0;
|
||||||
public static final int STREAM_CFG_REMOTE = 1;
|
public static final int STREAM_CFG_REMOTE = 1;
|
||||||
public static final int STREAM_CFG_AUTO = 2;
|
public static final int STREAM_CFG_AUTO = 2;
|
||||||
|
|
||||||
|
private NvApp app;
|
||||||
|
private int width, height;
|
||||||
|
private int refreshRate;
|
||||||
|
private int launchRefreshRate;
|
||||||
|
private int clientRefreshRateX100;
|
||||||
|
private int bitrate;
|
||||||
|
private boolean sops;
|
||||||
|
private boolean enableAdaptiveResolution;
|
||||||
|
private boolean playLocalAudio;
|
||||||
|
private int maxPacketSize;
|
||||||
|
private int remote;
|
||||||
|
private MoonBridge.AudioConfiguration audioConfiguration;
|
||||||
|
private boolean supportsHevc;
|
||||||
|
private int hevcBitratePercentageMultiplier;
|
||||||
|
private boolean enableHdr;
|
||||||
|
private int attachedGamepadMask;
|
||||||
|
|
||||||
private static final int CHANNEL_COUNT_STEREO = 2;
|
public static class Builder {
|
||||||
private static final int CHANNEL_COUNT_5_1 = 6;
|
private StreamConfiguration config = new StreamConfiguration();
|
||||||
|
|
||||||
private static final int CHANNEL_MASK_STEREO = 0x3;
|
public StreamConfiguration.Builder setApp(NvApp app) {
|
||||||
private static final int CHANNEL_MASK_5_1 = 0xFC;
|
config.app = app;
|
||||||
|
return this;
|
||||||
private NvApp app;
|
}
|
||||||
private int width, height;
|
|
||||||
private int refreshRate;
|
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
|
||||||
private int clientRefreshRateX100;
|
config.remote = remote;
|
||||||
private int bitrate;
|
return this;
|
||||||
private boolean sops;
|
}
|
||||||
private boolean enableAdaptiveResolution;
|
|
||||||
private boolean playLocalAudio;
|
public StreamConfiguration.Builder setResolution(int width, int height) {
|
||||||
private int maxPacketSize;
|
config.width = width;
|
||||||
private int remote;
|
config.height = height;
|
||||||
private int audioChannelMask;
|
return this;
|
||||||
private int audioChannelCount;
|
}
|
||||||
private int audioConfiguration;
|
|
||||||
private boolean supportsHevc;
|
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
|
||||||
private int hevcBitratePercentageMultiplier;
|
config.refreshRate = refreshRate;
|
||||||
private boolean enableHdr;
|
return this;
|
||||||
private int attachedGamepadMask;
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public StreamConfiguration.Builder setLaunchRefreshRate(int refreshRate) {
|
||||||
private StreamConfiguration config = new StreamConfiguration();
|
config.launchRefreshRate = refreshRate;
|
||||||
|
return this;
|
||||||
public StreamConfiguration.Builder setApp(NvApp app) {
|
}
|
||||||
config.app = app;
|
|
||||||
return this;
|
public StreamConfiguration.Builder setBitrate(int bitrate) {
|
||||||
}
|
config.bitrate = bitrate;
|
||||||
|
return this;
|
||||||
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
|
}
|
||||||
config.remote = remote;
|
|
||||||
return this;
|
public StreamConfiguration.Builder setEnableSops(boolean enable) {
|
||||||
}
|
config.sops = enable;
|
||||||
|
return this;
|
||||||
public StreamConfiguration.Builder setResolution(int width, int height) {
|
}
|
||||||
config.width = width;
|
|
||||||
config.height = height;
|
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
|
||||||
return this;
|
config.enableAdaptiveResolution = enable;
|
||||||
}
|
return this;
|
||||||
|
}
|
||||||
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
|
|
||||||
config.refreshRate = refreshRate;
|
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
|
||||||
return this;
|
config.playLocalAudio = enable;
|
||||||
}
|
return this;
|
||||||
|
}
|
||||||
public StreamConfiguration.Builder setBitrate(int bitrate) {
|
|
||||||
config.bitrate = bitrate;
|
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
|
||||||
return this;
|
config.maxPacketSize = maxPacketSize;
|
||||||
}
|
return this;
|
||||||
|
}
|
||||||
public StreamConfiguration.Builder setEnableSops(boolean enable) {
|
|
||||||
config.sops = enable;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
|
|
||||||
config.enableAdaptiveResolution = enable;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
|
|
||||||
config.playLocalAudio = enable;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
|
|
||||||
config.maxPacketSize = maxPacketSize;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
|
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
|
||||||
config.hevcBitratePercentageMultiplier = multiplier;
|
config.hevcBitratePercentageMultiplier = multiplier;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
|
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
|
||||||
config.enableHdr = enableHdr;
|
config.enableHdr = enableHdr;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
|
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
|
||||||
config.attachedGamepadMask = attachedGamepadMask;
|
config.attachedGamepadMask = attachedGamepadMask;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
|
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
|
||||||
config.attachedGamepadMask = 0;
|
config.attachedGamepadMask = 0;
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
if (gamepadCount > i) {
|
if (gamepadCount > i) {
|
||||||
config.attachedGamepadMask |= 1 << i;
|
config.attachedGamepadMask |= 1 << i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
|
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
|
||||||
config.clientRefreshRateX100 = refreshRateX100;
|
config.clientRefreshRateX100 = refreshRateX100;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamConfiguration.Builder setAudioConfiguration(int audioConfig) {
|
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
|
||||||
if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) {
|
config.audioConfiguration = audioConfig;
|
||||||
config.audioChannelCount = CHANNEL_COUNT_STEREO;
|
return this;
|
||||||
config.audioChannelMask = CHANNEL_MASK_STEREO;
|
}
|
||||||
}
|
|
||||||
else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) {
|
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
|
||||||
config.audioChannelCount = CHANNEL_COUNT_5_1;
|
config.supportsHevc = supportsHevc;
|
||||||
config.audioChannelMask = CHANNEL_MASK_5_1;
|
return this;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
throw new IllegalArgumentException("Invalid audio configuration");
|
public StreamConfiguration build() {
|
||||||
}
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StreamConfiguration() {
|
||||||
|
// Set default attributes
|
||||||
|
this.app = new NvApp("Steam");
|
||||||
|
this.width = 1280;
|
||||||
|
this.height = 720;
|
||||||
|
this.refreshRate = 60;
|
||||||
|
this.launchRefreshRate = 60;
|
||||||
|
this.bitrate = 10000;
|
||||||
|
this.maxPacketSize = 1024;
|
||||||
|
this.remote = STREAM_CFG_AUTO;
|
||||||
|
this.sops = true;
|
||||||
|
this.enableAdaptiveResolution = false;
|
||||||
|
this.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
|
||||||
|
this.supportsHevc = false;
|
||||||
|
this.enableHdr = false;
|
||||||
|
this.attachedGamepadMask = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRefreshRate() {
|
||||||
|
return refreshRate;
|
||||||
|
}
|
||||||
|
|
||||||
config.audioConfiguration = audioConfig;
|
public int getLaunchRefreshRate() {
|
||||||
|
return launchRefreshRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBitrate() {
|
||||||
|
return bitrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxPacketSize() {
|
||||||
|
return maxPacketSize;
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
public NvApp getApp() {
|
||||||
}
|
return app;
|
||||||
|
}
|
||||||
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
|
|
||||||
config.supportsHevc = supportsHevc;
|
public boolean getSops() {
|
||||||
return this;
|
return sops;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamConfiguration build() {
|
public boolean getAdaptiveResolutionEnabled() {
|
||||||
return config;
|
return enableAdaptiveResolution;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public boolean getPlayLocalAudio() {
|
||||||
private StreamConfiguration() {
|
return playLocalAudio;
|
||||||
// Set default attributes
|
}
|
||||||
this.app = new NvApp("Steam");
|
|
||||||
this.width = 1280;
|
public int getRemote() {
|
||||||
this.height = 720;
|
return remote;
|
||||||
this.refreshRate = 60;
|
}
|
||||||
this.bitrate = 10000;
|
|
||||||
this.maxPacketSize = 1024;
|
|
||||||
this.remote = STREAM_CFG_AUTO;
|
|
||||||
this.sops = true;
|
|
||||||
this.enableAdaptiveResolution = false;
|
|
||||||
this.audioChannelCount = CHANNEL_COUNT_STEREO;
|
|
||||||
this.audioChannelMask = CHANNEL_MASK_STEREO;
|
|
||||||
this.supportsHevc = false;
|
|
||||||
this.enableHdr = false;
|
|
||||||
this.attachedGamepadMask = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getWidth() {
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getHeight() {
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRefreshRate() {
|
|
||||||
return refreshRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getBitrate() {
|
|
||||||
return bitrate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxPacketSize() {
|
|
||||||
return maxPacketSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public NvApp getApp() {
|
public MoonBridge.AudioConfiguration getAudioConfiguration() {
|
||||||
return app;
|
return audioConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getSops() {
|
public boolean getHevcSupported() {
|
||||||
return sops;
|
return supportsHevc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getAdaptiveResolutionEnabled() {
|
|
||||||
return enableAdaptiveResolution;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getPlayLocalAudio() {
|
|
||||||
return playLocalAudio;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRemote() {
|
|
||||||
return remote;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAudioChannelCount() {
|
|
||||||
return audioChannelCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAudioChannelMask() {
|
|
||||||
return audioChannelMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAudioConfiguration() {
|
public int getHevcBitratePercentageMultiplier() {
|
||||||
return audioConfiguration;
|
return hevcBitratePercentageMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getHevcSupported() {
|
|
||||||
return supportsHevc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getHevcBitratePercentageMultiplier() {
|
public boolean getEnableHdr() {
|
||||||
return hevcBitratePercentageMultiplier;
|
return enableHdr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getEnableHdr() {
|
public int getAttachedGamepadMask() {
|
||||||
return enableHdr;
|
return attachedGamepadMask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getAttachedGamepadMask() {
|
public int getClientRefreshRateX100() {
|
||||||
return attachedGamepadMask;
|
return clientRefreshRateX100;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getClientRefreshRateX100() {
|
|
||||||
return clientRefreshRateX100;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
package com.limelight.nvstream.av;
|
package com.limelight.nvstream.av;
|
||||||
|
|
||||||
public class ByteBufferDescriptor {
|
public class ByteBufferDescriptor {
|
||||||
public byte[] data;
|
public byte[] data;
|
||||||
public int offset;
|
public int offset;
|
||||||
public int length;
|
public int length;
|
||||||
|
|
||||||
public ByteBufferDescriptor nextDescriptor;
|
public ByteBufferDescriptor nextDescriptor;
|
||||||
|
|
||||||
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
||||||
{
|
{
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.offset = offset;
|
this.offset = offset;
|
||||||
this.length = length;
|
this.length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
||||||
{
|
{
|
||||||
this.data = desc.data;
|
this.data = desc.data;
|
||||||
this.offset = desc.offset;
|
this.offset = desc.offset;
|
||||||
this.length = desc.length;
|
this.length = desc.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reinitialize(byte[] data, int offset, int length)
|
public void reinitialize(byte[] data, int offset, int length)
|
||||||
{
|
{
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.offset = offset;
|
this.offset = offset;
|
||||||
this.length = length;
|
this.length = length;
|
||||||
this.nextDescriptor = null;
|
this.nextDescriptor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void print()
|
public void print()
|
||||||
{
|
{
|
||||||
print(offset, length);
|
print(offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void print(int length)
|
public void print(int length)
|
||||||
{
|
{
|
||||||
print(this.offset, length);
|
print(this.offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void print(int offset, int length)
|
public void print(int offset, int length)
|
||||||
{
|
{
|
||||||
for (int i = offset; i < offset+length;) {
|
for (int i = offset; i < offset+length;) {
|
||||||
if (i + 8 <= offset+length) {
|
if (i + 8 <= offset+length) {
|
||||||
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
|
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
|
||||||
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
|
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
|
||||||
i += 8;
|
i += 8;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
System.out.printf("%x: %02x \n", i, data[i]);
|
System.out.printf("%x: %02x \n", i, data[i]);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
System.out.println();
|
System.out.println();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package com.limelight.nvstream.av.audio;
|
package com.limelight.nvstream.av.audio;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
public interface AudioRenderer {
|
public interface AudioRenderer {
|
||||||
int setup(int audioConfiguration);
|
int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame);
|
||||||
|
|
||||||
void start();
|
void start();
|
||||||
|
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
void playDecodedAudio(short[] audioData);
|
void playDecodedAudio(short[] audioData);
|
||||||
|
|
||||||
void cleanup();
|
void cleanup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package com.limelight.nvstream.av.video;
|
package com.limelight.nvstream.av.video;
|
||||||
|
|
||||||
public abstract class VideoDecoderRenderer {
|
public abstract class VideoDecoderRenderer {
|
||||||
public abstract int setup(int format, int width, int height, int redrawRate);
|
public abstract int setup(int format, int width, int height, int redrawRate);
|
||||||
|
|
||||||
public abstract void start();
|
public abstract void start();
|
||||||
|
|
||||||
public abstract void stop();
|
public abstract void stop();
|
||||||
|
|
||||||
// This is called once for each frame-start NALU. This means it will be called several times
|
// This is called once for each frame-start NALU. This means it will be called several times
|
||||||
// for an IDR frame which contains several parameter sets and the I-frame data.
|
// for an IDR frame which contains several parameter sets and the I-frame data.
|
||||||
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||||
int frameNumber, long receiveTimeMs);
|
int frameNumber, long receiveTimeMs);
|
||||||
|
|
||||||
public abstract void cleanup();
|
public abstract void cleanup();
|
||||||
|
|
||||||
public abstract int getCapabilities();
|
public abstract int getCapabilities();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,82 +4,82 @@ import java.security.cert.X509Certificate;
|
|||||||
|
|
||||||
|
|
||||||
public class ComputerDetails {
|
public class ComputerDetails {
|
||||||
public enum State {
|
public enum State {
|
||||||
ONLINE, OFFLINE, UNKNOWN
|
ONLINE, OFFLINE, UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persistent attributes
|
// Persistent attributes
|
||||||
public String uuid;
|
public String uuid;
|
||||||
public String name;
|
public String name;
|
||||||
public String localAddress;
|
public String localAddress;
|
||||||
public String remoteAddress;
|
public String remoteAddress;
|
||||||
public String manualAddress;
|
public String manualAddress;
|
||||||
public String ipv6Address;
|
public String ipv6Address;
|
||||||
public String macAddress;
|
public String macAddress;
|
||||||
public X509Certificate serverCert;
|
public X509Certificate serverCert;
|
||||||
|
|
||||||
// Transient attributes
|
// Transient attributes
|
||||||
public State state;
|
public State state;
|
||||||
public String activeAddress;
|
public String activeAddress;
|
||||||
public PairingManager.PairState pairState;
|
public PairingManager.PairState pairState;
|
||||||
public int runningGameId;
|
public int runningGameId;
|
||||||
public String rawAppList;
|
public String rawAppList;
|
||||||
|
|
||||||
public ComputerDetails() {
|
public ComputerDetails() {
|
||||||
// Use defaults
|
// Use defaults
|
||||||
state = State.UNKNOWN;
|
state = State.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails(ComputerDetails details) {
|
public ComputerDetails(ComputerDetails details) {
|
||||||
// Copy details from the other computer
|
// Copy details from the other computer
|
||||||
update(details);
|
update(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(ComputerDetails details) {
|
public void update(ComputerDetails details) {
|
||||||
this.state = details.state;
|
this.state = details.state;
|
||||||
this.name = details.name;
|
this.name = details.name;
|
||||||
this.uuid = details.uuid;
|
this.uuid = details.uuid;
|
||||||
if (details.activeAddress != null) {
|
if (details.activeAddress != null) {
|
||||||
this.activeAddress = details.activeAddress;
|
this.activeAddress = details.activeAddress;
|
||||||
}
|
}
|
||||||
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
||||||
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
||||||
this.localAddress = details.localAddress;
|
this.localAddress = details.localAddress;
|
||||||
}
|
}
|
||||||
if (details.remoteAddress != null) {
|
if (details.remoteAddress != null) {
|
||||||
this.remoteAddress = details.remoteAddress;
|
this.remoteAddress = details.remoteAddress;
|
||||||
}
|
}
|
||||||
if (details.manualAddress != null) {
|
if (details.manualAddress != null) {
|
||||||
this.manualAddress = details.manualAddress;
|
this.manualAddress = details.manualAddress;
|
||||||
}
|
}
|
||||||
if (details.ipv6Address != null) {
|
if (details.ipv6Address != null) {
|
||||||
this.ipv6Address = details.ipv6Address;
|
this.ipv6Address = details.ipv6Address;
|
||||||
}
|
}
|
||||||
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
|
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
|
||||||
this.macAddress = details.macAddress;
|
this.macAddress = details.macAddress;
|
||||||
}
|
}
|
||||||
if (details.serverCert != null) {
|
if (details.serverCert != null) {
|
||||||
this.serverCert = details.serverCert;
|
this.serverCert = details.serverCert;
|
||||||
}
|
}
|
||||||
this.pairState = details.pairState;
|
this.pairState = details.pairState;
|
||||||
this.runningGameId = details.runningGameId;
|
this.runningGameId = details.runningGameId;
|
||||||
this.rawAppList = details.rawAppList;
|
this.rawAppList = details.rawAppList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder str = new StringBuilder();
|
StringBuilder str = new StringBuilder();
|
||||||
str.append("State: ").append(state).append("\n");
|
str.append("Name: ").append(name).append("\n");
|
||||||
str.append("Active Address: ").append(activeAddress).append("\n");
|
str.append("State: ").append(state).append("\n");
|
||||||
str.append("Name: ").append(name).append("\n");
|
str.append("Active Address: ").append(activeAddress).append("\n");
|
||||||
str.append("UUID: ").append(uuid).append("\n");
|
str.append("UUID: ").append(uuid).append("\n");
|
||||||
str.append("Local Address: ").append(localAddress).append("\n");
|
str.append("Local Address: ").append(localAddress).append("\n");
|
||||||
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
||||||
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
|
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
|
||||||
str.append("Manual Address: ").append(manualAddress).append("\n");
|
str.append("Manual Address: ").append(manualAddress).append("\n");
|
||||||
str.append("MAC Address: ").append(macAddress).append("\n");
|
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||||
str.append("Pair State: ").append(pairState).append("\n");
|
str.append("Pair State: ").append(pairState).append("\n");
|
||||||
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||||
return str.toString();
|
return str.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,26 @@ package com.limelight.nvstream.http;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class GfeHttpResponseException extends IOException {
|
public class GfeHttpResponseException extends IOException {
|
||||||
private static final long serialVersionUID = 1543508830807804222L;
|
private static final long serialVersionUID = 1543508830807804222L;
|
||||||
|
|
||||||
private int errorCode;
|
private int errorCode;
|
||||||
private String errorMsg;
|
private String errorMsg;
|
||||||
|
|
||||||
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
||||||
this.errorCode = errorCode;
|
this.errorCode = errorCode;
|
||||||
this.errorMsg = errorMsg;
|
this.errorMsg = errorMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getErrorCode() {
|
public int getErrorCode() {
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getErrorMessage() {
|
public String getErrorMessage() {
|
||||||
return errorMsg;
|
return errorMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
|
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import java.security.cert.X509Certificate;
|
|||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
|
||||||
public interface LimelightCryptoProvider {
|
public interface LimelightCryptoProvider {
|
||||||
X509Certificate getClientCertificate();
|
X509Certificate getClientCertificate();
|
||||||
RSAPrivateKey getClientPrivateKey();
|
RSAPrivateKey getClientPrivateKey();
|
||||||
byte[] getPemEncodedClientCertificate();
|
byte[] getPemEncodedClientCertificate();
|
||||||
String encodeBase64String(byte[] data);
|
String encodeBase64String(byte[] data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,59 +3,68 @@ package com.limelight.nvstream.http;
|
|||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
public class NvApp {
|
public class NvApp {
|
||||||
private String appName = "";
|
private String appName = "";
|
||||||
private int appId;
|
private int appId;
|
||||||
private boolean initialized;
|
private boolean initialized;
|
||||||
private boolean hdrSupported;
|
private boolean hdrSupported;
|
||||||
|
|
||||||
public NvApp() {}
|
public NvApp() {}
|
||||||
|
|
||||||
public NvApp(String appName) {
|
public NvApp(String appName) {
|
||||||
this.appName = appName;
|
this.appName = appName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NvApp(String appName, int appId, boolean hdrSupported) {
|
public NvApp(String appName, int appId, boolean hdrSupported) {
|
||||||
this.appName = appName;
|
this.appName = appName;
|
||||||
this.appId = appId;
|
this.appId = appId;
|
||||||
this.hdrSupported = hdrSupported;
|
this.hdrSupported = hdrSupported;
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAppName(String appName) {
|
public void setAppName(String appName) {
|
||||||
this.appName = appName;
|
this.appName = appName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAppId(String appId) {
|
public void setAppId(String appId) {
|
||||||
try {
|
try {
|
||||||
this.appId = Integer.parseInt(appId);
|
this.appId = Integer.parseInt(appId);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
LimeLog.warning("Malformed app ID: "+appId);
|
LimeLog.warning("Malformed app ID: "+appId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAppId(int appId) {
|
public void setAppId(int appId) {
|
||||||
this.appId = appId;
|
this.appId = appId;
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHdrSupported(boolean hdrSupported) {
|
public void setHdrSupported(boolean hdrSupported) {
|
||||||
this.hdrSupported = hdrSupported;
|
this.hdrSupported = hdrSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAppName() {
|
public String getAppName() {
|
||||||
return this.appName;
|
return this.appName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getAppId() {
|
public int getAppId() {
|
||||||
return this.appId;
|
return this.appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isHdrSupported() {
|
public boolean isHdrSupported() {
|
||||||
return this.hdrSupported;
|
return this.hdrSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isInitialized() {
|
public boolean isInitialized() {
|
||||||
return this.initialized;
|
return this.initialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append("Name: ").append(appName).append("\n");
|
||||||
|
str.append("HDR: ").append(hdrSupported ? "Yes" : "No").append("\n");
|
||||||
|
str.append("ID: ").append(appId).append("\n");
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,327 +14,329 @@ import java.security.*;
|
|||||||
import java.security.cert.*;
|
import java.security.cert.*;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class PairingManager {
|
public class PairingManager {
|
||||||
|
|
||||||
private NvHTTP http;
|
private NvHTTP http;
|
||||||
|
|
||||||
private PrivateKey pk;
|
private PrivateKey pk;
|
||||||
private X509Certificate cert;
|
private X509Certificate cert;
|
||||||
private SecretKey aesKey;
|
private SecretKey aesKey;
|
||||||
private byte[] pemCertBytes;
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
private X509Certificate serverCert;
|
private X509Certificate serverCert;
|
||||||
|
|
||||||
public enum PairState {
|
public enum PairState {
|
||||||
NOT_PAIRED,
|
NOT_PAIRED,
|
||||||
PAIRED,
|
PAIRED,
|
||||||
PIN_WRONG,
|
PIN_WRONG,
|
||||||
FAILED,
|
FAILED,
|
||||||
ALREADY_IN_PROGRESS
|
ALREADY_IN_PROGRESS
|
||||||
}
|
}
|
||||||
|
|
||||||
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
||||||
this.http = http;
|
this.http = http;
|
||||||
this.cert = cryptoProvider.getClientCertificate();
|
this.cert = cryptoProvider.getClientCertificate();
|
||||||
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
||||||
this.pk = cryptoProvider.getClientPrivateKey();
|
this.pk = cryptoProvider.getClientPrivateKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||||
private static String bytesToHex(byte[] bytes) {
|
private static String bytesToHex(byte[] bytes) {
|
||||||
char[] hexChars = new char[bytes.length * 2];
|
char[] hexChars = new char[bytes.length * 2];
|
||||||
for ( int j = 0; j < bytes.length; j++ ) {
|
for ( int j = 0; j < bytes.length; j++ ) {
|
||||||
int v = bytes[j] & 0xFF;
|
int v = bytes[j] & 0xFF;
|
||||||
hexChars[j * 2] = hexArray[v >>> 4];
|
hexChars[j * 2] = hexArray[v >>> 4];
|
||||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||||
}
|
}
|
||||||
return new String(hexChars);
|
return new String(hexChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] hexToBytes(String s) {
|
private static byte[] hexToBytes(String s) {
|
||||||
int len = s.length();
|
int len = s.length();
|
||||||
byte[] data = new byte[len / 2];
|
if (len % 2 != 0) {
|
||||||
for (int i = 0; i < len; i += 2) {
|
throw new IllegalArgumentException("Illegal string length: "+len);
|
||||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
}
|
||||||
+ Character.digit(s.charAt(i+1), 16));
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
|
||||||
{
|
|
||||||
String certText = NvHTTP.getXmlString(text, "plaincert");
|
|
||||||
if (certText != null) {
|
|
||||||
byte[] certBytes = hexToBytes(certText);
|
|
||||||
|
|
||||||
try {
|
byte[] data = new byte[len / 2];
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
for (int i = 0; i < len; i += 2) {
|
||||||
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||||
} catch (CertificateException e) {
|
+ Character.digit(s.charAt(i+1), 16));
|
||||||
e.printStackTrace();
|
}
|
||||||
return null;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
||||||
return null;
|
{
|
||||||
}
|
String certText = NvHTTP.getXmlString(text, "plaincert");
|
||||||
}
|
if (certText != null) {
|
||||||
|
byte[] certBytes = hexToBytes(certText);
|
||||||
private byte[] generateRandomBytes(int length)
|
|
||||||
{
|
|
||||||
byte[] rand = new byte[length];
|
|
||||||
new SecureRandom().nextBytes(rand);
|
|
||||||
return rand;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
|
||||||
byte[] saltedPin = new byte[salt.length + pin.length()];
|
|
||||||
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
|
||||||
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
|
||||||
return saltedPin;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
|
||||||
try {
|
|
||||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
|
||||||
sig.initVerify(cert.getPublicKey());
|
|
||||||
sig.update(data);
|
|
||||||
return sig.verify(signature);
|
|
||||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] signData(byte[] data, PrivateKey key) {
|
|
||||||
try {
|
|
||||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
|
||||||
sig.initSign(key);
|
|
||||||
sig.update(data);
|
|
||||||
byte[] signature = new byte[256];
|
|
||||||
sig.sign(signature, 0, signature.length);
|
|
||||||
return signature;
|
|
||||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
|
||||||
try {
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
|
||||||
|
|
||||||
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
|
||||||
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
|
||||||
byte[] fullDecrypted = new byte[blockRoundedSize];
|
|
||||||
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
try {
|
||||||
cipher.doFinal(blockRoundedEncrypted, 0,
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
blockRoundedSize, fullDecrypted);
|
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
return fullDecrypted;
|
} catch (CertificateException e) {
|
||||||
} catch (GeneralSecurityException e) {
|
e.printStackTrace();
|
||||||
e.printStackTrace();
|
throw new RuntimeException(e);
|
||||||
throw new RuntimeException(e);
|
}
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
|
return null;
|
||||||
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
}
|
||||||
try {
|
}
|
||||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
|
||||||
|
private byte[] generateRandomBytes(int length)
|
||||||
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
{
|
||||||
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
byte[] rand = new byte[length];
|
||||||
|
new SecureRandom().nextBytes(rand);
|
||||||
|
return rand;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
||||||
|
byte[] saltedPin = new byte[salt.length + pin.length()];
|
||||||
|
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
||||||
|
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
||||||
|
return saltedPin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||||
|
sig.initVerify(cert.getPublicKey());
|
||||||
|
sig.update(data);
|
||||||
|
return sig.verify(signature);
|
||||||
|
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] signData(byte[] data, PrivateKey key) {
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||||
|
sig.initSign(key);
|
||||||
|
sig.update(data);
|
||||||
|
byte[] signature = new byte[256];
|
||||||
|
sig.sign(signature, 0, signature.length);
|
||||||
|
return signature;
|
||||||
|
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
||||||
|
try {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||||
|
|
||||||
|
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
||||||
|
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
||||||
|
byte[] fullDecrypted = new byte[blockRoundedSize];
|
||||||
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||||
return cipher.doFinal(blockRoundedData);
|
cipher.doFinal(blockRoundedEncrypted, 0,
|
||||||
} catch (GeneralSecurityException e) {
|
blockRoundedSize, fullDecrypted);
|
||||||
e.printStackTrace();
|
return fullDecrypted;
|
||||||
throw new RuntimeException(e);
|
} catch (GeneralSecurityException e) {
|
||||||
}
|
e.printStackTrace();
|
||||||
}
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
}
|
||||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
|
||||||
return new SecretKeySpec(aesTruncated, "AES");
|
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
||||||
}
|
try {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||||
private static byte[] concatBytes(byte[] a, byte[] b) {
|
|
||||||
byte[] c = new byte[a.length + b.length];
|
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
||||||
System.arraycopy(a, 0, c, 0, a.length);
|
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
||||||
System.arraycopy(b, 0, c, a.length, b.length);
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String generatePinString() {
|
|
||||||
Random r = new Random();
|
|
||||||
return String.format((Locale)null, "%d%d%d%d",
|
|
||||||
r.nextInt(10), r.nextInt(10),
|
|
||||||
r.nextInt(10), r.nextInt(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate getPairedCert() {
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||||
return serverCert;
|
return cipher.doFinal(blockRoundedData);
|
||||||
}
|
} catch (GeneralSecurityException e) {
|
||||||
|
e.printStackTrace();
|
||||||
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
throw new RuntimeException(e);
|
||||||
PairingHashAlgorithm hashAlgo;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||||
|
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||||
|
return new SecretKeySpec(aesTruncated, "AES");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||||
|
byte[] c = new byte[a.length + b.length];
|
||||||
|
System.arraycopy(a, 0, c, 0, a.length);
|
||||||
|
System.arraycopy(b, 0, c, a.length, b.length);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String generatePinString() {
|
||||||
|
SecureRandom r = new SecureRandom();
|
||||||
|
return String.format((Locale)null, "%d%d%d%d",
|
||||||
|
r.nextInt(10), r.nextInt(10),
|
||||||
|
r.nextInt(10), r.nextInt(10));
|
||||||
|
}
|
||||||
|
|
||||||
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
public X509Certificate getPairedCert() {
|
||||||
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
return serverCert;
|
||||||
if (serverMajorVersion >= 7) {
|
}
|
||||||
// Gen 7+ uses SHA-256 hashing
|
|
||||||
hashAlgo = new Sha256PairingHash();
|
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
||||||
}
|
PairingHashAlgorithm hashAlgo;
|
||||||
else {
|
|
||||||
// Prior to Gen 7, SHA-1 is used
|
|
||||||
hashAlgo = new Sha1PairingHash();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a salt for hashing the PIN
|
|
||||||
byte[] salt = generateRandomBytes(16);
|
|
||||||
|
|
||||||
// Combine the salt and pin, then create an AES key from them
|
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||||
byte[] saltAndPin = saltPin(salt, pin);
|
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||||
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
if (serverMajorVersion >= 7) {
|
||||||
|
// Gen 7+ uses SHA-256 hashing
|
||||||
// Send the salt and get the server cert. This doesn't have a read timeout
|
hashAlgo = new Sha256PairingHash();
|
||||||
// because the user must enter the PIN before the server responds
|
}
|
||||||
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
else {
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
// Prior to Gen 7, SHA-1 is used
|
||||||
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
hashAlgo = new Sha1PairingHash();
|
||||||
false);
|
}
|
||||||
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
|
||||||
return PairState.FAILED;
|
// Generate a salt for hashing the PIN
|
||||||
}
|
byte[] salt = generateRandomBytes(16);
|
||||||
|
|
||||||
// Save this cert for retrieval later
|
// Combine the salt and pin, then create an AES key from them
|
||||||
serverCert = extractPlainCert(getCert);
|
byte[] saltAndPin = saltPin(salt, pin);
|
||||||
if (serverCert == null) {
|
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||||
// Attempting to pair while another device is pairing will cause GFE
|
|
||||||
// to give an empty cert in the response.
|
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||||
return PairState.ALREADY_IN_PROGRESS;
|
// because the user must enter the PIN before the server responds
|
||||||
}
|
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
||||||
|
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||||
|
false);
|
||||||
|
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
||||||
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
// Require this cert for TLS to this host
|
// Save this cert for retrieval later
|
||||||
http.setServerCert(serverCert);
|
serverCert = extractPlainCert(getCert);
|
||||||
|
if (serverCert == null) {
|
||||||
// Generate a random challenge and encrypt it with our AES key
|
// Attempting to pair while another device is pairing will cause GFE
|
||||||
byte[] randomChallenge = generateRandomBytes(16);
|
// to give an empty cert in the response.
|
||||||
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
|
return PairState.ALREADY_IN_PROGRESS;
|
||||||
// Send the encrypted challenge to the server
|
}
|
||||||
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
|
||||||
true);
|
|
||||||
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
|
||||||
return PairState.FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode the server's response and subsequent challenge
|
|
||||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
|
||||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
|
||||||
|
|
||||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
|
||||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
|
||||||
|
|
||||||
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
|
||||||
byte[] clientSecret = generateRandomBytes(16);
|
|
||||||
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
|
||||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
|
||||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
|
||||||
true);
|
|
||||||
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
|
||||||
return PairState.FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the server's signed secret
|
|
||||||
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
|
||||||
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
|
||||||
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
|
||||||
|
|
||||||
// Ensure the authenticity of the data
|
// Require this cert for TLS to this host
|
||||||
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
http.setServerCert(serverCert);
|
||||||
// Cancel the pairing process
|
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
// Generate a random challenge and encrypt it with our AES key
|
||||||
|
byte[] randomChallenge = generateRandomBytes(16);
|
||||||
// Looks like a MITM
|
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||||
return PairState.FAILED;
|
|
||||||
}
|
// Send the encrypted challenge to the server
|
||||||
|
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
||||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
true);
|
||||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
||||||
// Cancel the pairing process
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
// Probably got the wrong PIN
|
|
||||||
return PairState.PIN_WRONG;
|
// Decode the server's response and subsequent challenge
|
||||||
}
|
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||||
|
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||||
// Send the server our signed secret
|
|
||||||
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||||
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
|
||||||
true);
|
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||||
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
byte[] clientSecret = generateRandomBytes(16);
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||||
return PairState.FAILED;
|
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||||
}
|
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||||
// Do the initial challenge (seems neccessary for us to show as paired)
|
true);
|
||||||
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
return PairState.FAILED;
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
}
|
||||||
return PairState.FAILED;
|
|
||||||
}
|
// Get the server's signed secret
|
||||||
|
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
||||||
|
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||||
|
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||||
|
|
||||||
return PairState.PAIRED;
|
// Ensure the authenticity of the data
|
||||||
}
|
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||||
|
// Cancel the pairing process
|
||||||
private interface PairingHashAlgorithm {
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
int getHashLength();
|
|
||||||
byte[] hashData(byte[] data);
|
// Looks like a MITM
|
||||||
}
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
|
||||||
public int getHashLength() {
|
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||||
return 20;
|
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||||
}
|
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||||
|
// Cancel the pairing process
|
||||||
public byte[] hashData(byte[] data) {
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
// Probably got the wrong PIN
|
||||||
return md.digest(data);
|
return PairState.PIN_WRONG;
|
||||||
}
|
}
|
||||||
catch (NoSuchAlgorithmException e) {
|
|
||||||
// Shouldn't ever happen
|
// Send the server our signed secret
|
||||||
e.printStackTrace();
|
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||||
return null;
|
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
}
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
||||||
}
|
true);
|
||||||
}
|
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
return PairState.FAILED;
|
||||||
public int getHashLength() {
|
}
|
||||||
return 32;
|
|
||||||
}
|
// Do the initial challenge (seems neccessary for us to show as paired)
|
||||||
|
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
||||||
public byte[] hashData(byte[] data) {
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
||||||
try {
|
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
return md.digest(data);
|
return PairState.FAILED;
|
||||||
}
|
}
|
||||||
catch (NoSuchAlgorithmException e) {
|
|
||||||
// Shouldn't ever happen
|
return PairState.PAIRED;
|
||||||
e.printStackTrace();
|
}
|
||||||
return null;
|
|
||||||
}
|
private interface PairingHashAlgorithm {
|
||||||
}
|
int getHashLength();
|
||||||
}
|
byte[] hashData(byte[] data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||||
|
public int getHashLength() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] hashData(byte[] data) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||||
|
return md.digest(data);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||||
|
public int getHashLength() {
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] hashData(byte[] data) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
return md.digest(data);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package com.limelight.nvstream.input;
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
public class ControllerPacket {
|
public class ControllerPacket {
|
||||||
public static final short A_FLAG = 0x1000;
|
public static final short A_FLAG = 0x1000;
|
||||||
public static final short B_FLAG = 0x2000;
|
public static final short B_FLAG = 0x2000;
|
||||||
public static final short X_FLAG = 0x4000;
|
public static final short X_FLAG = 0x4000;
|
||||||
public static final short Y_FLAG = (short)0x8000;
|
public static final short Y_FLAG = (short)0x8000;
|
||||||
public static final short UP_FLAG = 0x0001;
|
public static final short UP_FLAG = 0x0001;
|
||||||
public static final short DOWN_FLAG = 0x0002;
|
public static final short DOWN_FLAG = 0x0002;
|
||||||
public static final short LEFT_FLAG = 0x0004;
|
public static final short LEFT_FLAG = 0x0004;
|
||||||
public static final short RIGHT_FLAG = 0x0008;
|
public static final short RIGHT_FLAG = 0x0008;
|
||||||
public static final short LB_FLAG = 0x0100;
|
public static final short LB_FLAG = 0x0100;
|
||||||
public static final short RB_FLAG = 0x0200;
|
public static final short RB_FLAG = 0x0200;
|
||||||
public static final short PLAY_FLAG = 0x0010;
|
public static final short PLAY_FLAG = 0x0010;
|
||||||
public static final short BACK_FLAG = 0x0020;
|
public static final short BACK_FLAG = 0x0020;
|
||||||
public static final short LS_CLK_FLAG = 0x0040;
|
public static final short LS_CLK_FLAG = 0x0040;
|
||||||
public static final short RS_CLK_FLAG = 0x0080;
|
public static final short RS_CLK_FLAG = 0x0080;
|
||||||
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.limelight.nvstream.input;
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
public class KeyboardPacket {
|
public class KeyboardPacket {
|
||||||
public static final byte KEY_DOWN = 0x03;
|
public static final byte KEY_DOWN = 0x03;
|
||||||
public static final byte KEY_UP = 0x04;
|
public static final byte KEY_UP = 0x04;
|
||||||
|
|
||||||
public static final byte MODIFIER_SHIFT = 0x01;
|
public static final byte MODIFIER_SHIFT = 0x01;
|
||||||
public static final byte MODIFIER_CTRL = 0x02;
|
public static final byte MODIFIER_CTRL = 0x02;
|
||||||
public static final byte MODIFIER_ALT = 0x04;
|
public static final byte MODIFIER_ALT = 0x04;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.limelight.nvstream.input;
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
public class MouseButtonPacket {
|
public class MouseButtonPacket {
|
||||||
public static final byte PRESS_EVENT = 0x07;
|
public static final byte PRESS_EVENT = 0x07;
|
||||||
public static final byte RELEASE_EVENT = 0x08;
|
public static final byte RELEASE_EVENT = 0x08;
|
||||||
|
|
||||||
public static final byte BUTTON_LEFT = 0x01;
|
public static final byte BUTTON_LEFT = 0x01;
|
||||||
public static final byte BUTTON_MIDDLE = 0x02;
|
public static final byte BUTTON_MIDDLE = 0x02;
|
||||||
public static final byte BUTTON_RIGHT = 0x03;
|
public static final byte BUTTON_RIGHT = 0x03;
|
||||||
public static final byte BUTTON_X1 = 0x04;
|
public static final byte BUTTON_X1 = 0x04;
|
||||||
public static final byte BUTTON_X2 = 0x05;
|
public static final byte BUTTON_X2 = 0x05;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
|||||||
public class MoonBridge {
|
public class MoonBridge {
|
||||||
/* See documentation in Limelight.h for information about these functions and constants */
|
/* See documentation in Limelight.h for information about these functions and constants */
|
||||||
|
|
||||||
public static final int AUDIO_CONFIGURATION_STEREO = 0;
|
public static final AudioConfiguration AUDIO_CONFIGURATION_STEREO = new AudioConfiguration(2, 0x3);
|
||||||
public static final int AUDIO_CONFIGURATION_51_SURROUND = 1;
|
public static final AudioConfiguration AUDIO_CONFIGURATION_51_SURROUND = new AudioConfiguration(6, 0x3F);
|
||||||
|
public static final AudioConfiguration AUDIO_CONFIGURATION_71_SURROUND = new AudioConfiguration(8, 0x63F);
|
||||||
|
|
||||||
public static final int VIDEO_FORMAT_H264 = 0x0001;
|
public static final int VIDEO_FORMAT_H264 = 0x0001;
|
||||||
public static final int VIDEO_FORMAT_H265 = 0x0100;
|
public static final int VIDEO_FORMAT_H265 = 0x0100;
|
||||||
@@ -32,6 +33,30 @@ public class MoonBridge {
|
|||||||
public static final int CONN_STATUS_OKAY = 0;
|
public static final int CONN_STATUS_OKAY = 0;
|
||||||
public static final int CONN_STATUS_POOR = 1;
|
public static final int CONN_STATUS_POOR = 1;
|
||||||
|
|
||||||
|
public static final int ML_ERROR_GRACEFUL_TERMINATION = 0;
|
||||||
|
public static final int ML_ERROR_NO_VIDEO_TRAFFIC = -100;
|
||||||
|
public static final int ML_ERROR_NO_VIDEO_FRAME = -101;
|
||||||
|
public static final int ML_ERROR_UNEXPECTED_EARLY_TERMINATION = -102;
|
||||||
|
|
||||||
|
public static final int ML_PORT_INDEX_TCP_47984 = 0;
|
||||||
|
public static final int ML_PORT_INDEX_TCP_47989 = 1;
|
||||||
|
public static final int ML_PORT_INDEX_TCP_48010 = 2;
|
||||||
|
public static final int ML_PORT_INDEX_UDP_47998 = 8;
|
||||||
|
public static final int ML_PORT_INDEX_UDP_47999 = 9;
|
||||||
|
public static final int ML_PORT_INDEX_UDP_48000 = 10;
|
||||||
|
public static final int ML_PORT_INDEX_UDP_48010 = 11;
|
||||||
|
|
||||||
|
public static final int ML_PORT_FLAG_ALL = 0xFFFFFFFF;
|
||||||
|
public static final int ML_PORT_FLAG_TCP_47984 = 0x0001;
|
||||||
|
public static final int ML_PORT_FLAG_TCP_47989 = 0x0002;
|
||||||
|
public static final int ML_PORT_FLAG_TCP_48010 = 0x0004;
|
||||||
|
public static final int ML_PORT_FLAG_UDP_47998 = 0x0100;
|
||||||
|
public static final int ML_PORT_FLAG_UDP_47999 = 0x0200;
|
||||||
|
public static final int ML_PORT_FLAG_UDP_48000 = 0x0400;
|
||||||
|
public static final int ML_PORT_FLAG_UDP_48010 = 0x0800;
|
||||||
|
|
||||||
|
public static final int ML_TEST_RESULT_INCONCLUSIVE = 0xFFFFFFFF;
|
||||||
|
|
||||||
private static AudioRenderer audioRenderer;
|
private static AudioRenderer audioRenderer;
|
||||||
private static VideoDecoderRenderer videoRenderer;
|
private static VideoDecoderRenderer videoRenderer;
|
||||||
private static NvConnectionListener connectionListener;
|
private static NvConnectionListener connectionListener;
|
||||||
@@ -45,6 +70,57 @@ public class MoonBridge {
|
|||||||
return slices << 24;
|
return slices << 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class AudioConfiguration {
|
||||||
|
public final int channelCount;
|
||||||
|
public final int channelMask;
|
||||||
|
|
||||||
|
public AudioConfiguration(int channelCount, int channelMask) {
|
||||||
|
this.channelCount = channelCount;
|
||||||
|
this.channelMask = channelMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an AudioConfiguration from the integer value returned by moonlight-common-c
|
||||||
|
// See CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION() and CHANNEL_MASK_FROM_AUDIO_CONFIGURATION()
|
||||||
|
// in Limelight.h
|
||||||
|
private AudioConfiguration(int audioConfiguration) {
|
||||||
|
// Check the magic byte before decoding to make sure we got something that's actually
|
||||||
|
// a MAKE_AUDIO_CONFIGURATION()-based value and not something else like an older version
|
||||||
|
// hardcoded AUDIO_CONFIGURATION value from an earlier version of moonlight-common-c.
|
||||||
|
if ((audioConfiguration & 0xFF) != 0xCA) {
|
||||||
|
throw new IllegalArgumentException("Audio configuration has invalid magic byte!");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.channelCount = (audioConfiguration >> 8) & 0xFF;
|
||||||
|
this.channelMask = (audioConfiguration >> 16) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION() in Limelight.h
|
||||||
|
public int getSurroundAudioInfo() {
|
||||||
|
return channelMask << 16 | channelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof AudioConfiguration) {
|
||||||
|
AudioConfiguration that = (AudioConfiguration)obj;
|
||||||
|
return this.toInt() == that.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the integer value expected by moonlight-common-c
|
||||||
|
// See MAKE_AUDIO_CONFIGURATION() in Limelight.h
|
||||||
|
public int toInt() {
|
||||||
|
return ((channelMask) << 16) | (channelCount << 8) | 0xCA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static int bridgeDrSetup(int videoFormat, int width, int height, int redrawRate) {
|
public static int bridgeDrSetup(int videoFormat, int width, int height, int redrawRate) {
|
||||||
if (videoRenderer != null) {
|
if (videoRenderer != null) {
|
||||||
return videoRenderer.setup(videoFormat, width, height, redrawRate);
|
return videoRenderer.setup(videoFormat, width, height, redrawRate);
|
||||||
@@ -84,9 +160,9 @@ public class MoonBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int bridgeArInit(int audioConfiguration) {
|
public static int bridgeArInit(int audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||||
if (audioRenderer != null) {
|
if (audioRenderer != null) {
|
||||||
return audioRenderer.setup(audioConfiguration);
|
return audioRenderer.setup(new AudioConfiguration(audioConfiguration), sampleRate, samplesPerFrame);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -129,9 +205,9 @@ public class MoonBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void bridgeClStageFailed(int stage, long errorCode) {
|
public static void bridgeClStageFailed(int stage, int errorCode) {
|
||||||
if (connectionListener != null) {
|
if (connectionListener != null) {
|
||||||
connectionListener.stageFailed(getStageName(stage), errorCode);
|
connectionListener.stageFailed(getStageName(stage), getPortFlagsFromStage(stage), errorCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +217,7 @@ public class MoonBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void bridgeClConnectionTerminated(long errorCode) {
|
public static void bridgeClConnectionTerminated(int errorCode) {
|
||||||
if (connectionListener != null) {
|
if (connectionListener != null) {
|
||||||
connectionListener.connectionTerminated(errorCode);
|
connectionListener.connectionTerminated(errorCode);
|
||||||
}
|
}
|
||||||
@@ -187,6 +263,8 @@ public class MoonBridge {
|
|||||||
|
|
||||||
public static native void sendMouseMove(short deltaX, short deltaY);
|
public static native void sendMouseMove(short deltaX, short deltaY);
|
||||||
|
|
||||||
|
public static native void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight);
|
||||||
|
|
||||||
public static native void sendMouseButton(byte buttonEvent, byte mouseButton);
|
public static native void sendMouseButton(byte buttonEvent, byte mouseButton);
|
||||||
|
|
||||||
public static native void sendMultiControllerInput(short controllerNumber,
|
public static native void sendMultiControllerInput(short controllerNumber,
|
||||||
@@ -204,13 +282,23 @@ public class MoonBridge {
|
|||||||
|
|
||||||
public static native void sendMouseScroll(byte scrollClicks);
|
public static native void sendMouseScroll(byte scrollClicks);
|
||||||
|
|
||||||
|
public static native void sendMouseHighResScroll(short scrollAmount);
|
||||||
|
|
||||||
public static native String getStageName(int stage);
|
public static native String getStageName(int stage);
|
||||||
|
|
||||||
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
||||||
|
|
||||||
public static native int getPendingAudioFrames();
|
public static native int getPendingAudioDuration();
|
||||||
|
|
||||||
public static native int getPendingVideoFrames();
|
public static native int getPendingVideoFrames();
|
||||||
|
|
||||||
|
public static native int testClientConnectivity(String testServerHostName, int referencePort, int testFlags);
|
||||||
|
|
||||||
|
public static native int getPortFlagsFromStage(int stage);
|
||||||
|
|
||||||
|
public static native int getPortFlagsFromTerminationErrorCode(int errorCode);
|
||||||
|
|
||||||
|
public static native String stringifyPortFlags(int portFlags, String separator);
|
||||||
|
|
||||||
public static native void init();
|
public static native void init();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,62 +4,62 @@ import java.net.Inet6Address;
|
|||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
|
||||||
public class MdnsComputer {
|
public class MdnsComputer {
|
||||||
private InetAddress localAddr;
|
private InetAddress localAddr;
|
||||||
private Inet6Address v6Addr;
|
private Inet6Address v6Addr;
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
|
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.localAddr = localAddress;
|
this.localAddr = localAddress;
|
||||||
this.v6Addr = v6Addr;
|
this.v6Addr = v6Addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InetAddress getLocalAddress() {
|
public InetAddress getLocalAddress() {
|
||||||
return localAddr;
|
return localAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Inet6Address getIpv6Address() {
|
public Inet6Address getIpv6Address() {
|
||||||
return v6Addr;
|
return v6Addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return name.hashCode();
|
return name.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (o instanceof MdnsComputer) {
|
if (o instanceof MdnsComputer) {
|
||||||
MdnsComputer other = (MdnsComputer)o;
|
MdnsComputer other = (MdnsComputer)o;
|
||||||
|
|
||||||
if (!other.name.equals(name)) {
|
if (!other.name.equals(name)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((other.localAddr != null && localAddr == null) ||
|
if ((other.localAddr != null && localAddr == null) ||
|
||||||
(other.localAddr == null && localAddr != null) ||
|
(other.localAddr == null && localAddr != null) ||
|
||||||
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
|
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((other.v6Addr != null && v6Addr == null) ||
|
if ((other.v6Addr != null && v6Addr == null) ||
|
||||||
(other.v6Addr == null && v6Addr != null) ||
|
(other.v6Addr == null && v6Addr != null) ||
|
||||||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
|
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
|
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,388 +21,388 @@ import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
|
|||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
public class MdnsDiscoveryAgent implements ServiceListener {
|
public class MdnsDiscoveryAgent implements ServiceListener {
|
||||||
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
|
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
|
||||||
|
|
||||||
private MdnsDiscoveryListener listener;
|
private MdnsDiscoveryListener listener;
|
||||||
private Thread discoveryThread;
|
private Thread discoveryThread;
|
||||||
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
|
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
|
||||||
private HashSet<String> pendingResolution = new HashSet<String>();
|
private HashSet<String> pendingResolution = new HashSet<String>();
|
||||||
|
|
||||||
// The resolver factory's instance member has a static lifetime which
|
// The resolver factory's instance member has a static lifetime which
|
||||||
// means our ref count and listener must be static also.
|
// means our ref count and listener must be static also.
|
||||||
private static int resolverRefCount = 0;
|
private static int resolverRefCount = 0;
|
||||||
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
|
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
|
||||||
private static ServiceListener nvstreamListener = new ServiceListener() {
|
private static ServiceListener nvstreamListener = new ServiceListener() {
|
||||||
@Override
|
@Override
|
||||||
public void serviceAdded(ServiceEvent event) {
|
public void serviceAdded(ServiceEvent event) {
|
||||||
HashSet<ServiceListener> localListeners;
|
HashSet<ServiceListener> localListeners;
|
||||||
|
|
||||||
// Copy the listener set into a new set so we can invoke
|
// Copy the listener set into a new set so we can invoke
|
||||||
// the callbacks without holding the listeners monitor the
|
// the callbacks without holding the listeners monitor the
|
||||||
// whole time.
|
// whole time.
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
localListeners = new HashSet<ServiceListener>(listeners);
|
localListeners = new HashSet<ServiceListener>(listeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ServiceListener listener : localListeners) {
|
for (ServiceListener listener : localListeners) {
|
||||||
listener.serviceAdded(event);
|
listener.serviceAdded(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serviceRemoved(ServiceEvent event) {
|
public void serviceRemoved(ServiceEvent event) {
|
||||||
HashSet<ServiceListener> localListeners;
|
HashSet<ServiceListener> localListeners;
|
||||||
|
|
||||||
// Copy the listener set into a new set so we can invoke
|
// Copy the listener set into a new set so we can invoke
|
||||||
// the callbacks without holding the listeners monitor the
|
// the callbacks without holding the listeners monitor the
|
||||||
// whole time.
|
// whole time.
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
localListeners = new HashSet<ServiceListener>(listeners);
|
localListeners = new HashSet<ServiceListener>(listeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ServiceListener listener : localListeners) {
|
for (ServiceListener listener : localListeners) {
|
||||||
listener.serviceRemoved(event);
|
listener.serviceRemoved(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serviceResolved(ServiceEvent event) {
|
public void serviceResolved(ServiceEvent event) {
|
||||||
HashSet<ServiceListener> localListeners;
|
HashSet<ServiceListener> localListeners;
|
||||||
|
|
||||||
// Copy the listener set into a new set so we can invoke
|
// Copy the listener set into a new set so we can invoke
|
||||||
// the callbacks without holding the listeners monitor the
|
// the callbacks without holding the listeners monitor the
|
||||||
// whole time.
|
// whole time.
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
localListeners = new HashSet<ServiceListener>(listeners);
|
localListeners = new HashSet<ServiceListener>(listeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ServiceListener listener : localListeners) {
|
for (ServiceListener listener : localListeners) {
|
||||||
listener.serviceResolved(event);
|
listener.serviceResolved(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
|
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
|
||||||
@Override
|
@Override
|
||||||
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
|
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
|
||||||
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
|
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
|
||||||
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
|
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
|
||||||
try {
|
try {
|
||||||
if (!networkInterface.isUp()) {
|
if (!networkInterface.isUp()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if (!networkInterface.supportsMulticast()) {
|
if (!networkInterface.supportsMulticast()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (networkInterface.isLoopback()) {
|
if (networkInterface.isLoopback()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// Override jmDNS's default topology discovery class with ours
|
// Override jmDNS's default topology discovery class with ours
|
||||||
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
|
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
|
||||||
@Override
|
@Override
|
||||||
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
|
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
|
||||||
return new MyNetworkTopologyDiscovery();
|
return new MyNetworkTopologyDiscovery();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JmmDNS referenceResolver() {
|
private static JmmDNS referenceResolver() {
|
||||||
synchronized (MdnsDiscoveryAgent.class) {
|
synchronized (MdnsDiscoveryAgent.class) {
|
||||||
JmmDNS instance = JmmDNS.Factory.getInstance();
|
JmmDNS instance = JmmDNS.Factory.getInstance();
|
||||||
if (++resolverRefCount == 1) {
|
if (++resolverRefCount == 1) {
|
||||||
// This will cause the listener to be invoked for known hosts immediately.
|
// This will cause the listener to be invoked for known hosts immediately.
|
||||||
// JmDNS only supports one listener per service, so we have to do this here
|
// JmDNS only supports one listener per service, so we have to do this here
|
||||||
// with a static listener.
|
// with a static listener.
|
||||||
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
|
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void dereferenceResolver() {
|
private static void dereferenceResolver() {
|
||||||
synchronized (MdnsDiscoveryAgent.class) {
|
synchronized (MdnsDiscoveryAgent.class) {
|
||||||
if (--resolverRefCount == 0) {
|
if (--resolverRefCount == 0) {
|
||||||
try {
|
try {
|
||||||
JmmDNS.Factory.close();
|
JmmDNS.Factory.close();
|
||||||
} catch (IOException e) {}
|
} catch (IOException e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleResolvedServiceInfo(ServiceInfo info) {
|
private void handleResolvedServiceInfo(ServiceInfo info) {
|
||||||
synchronized (pendingResolution) {
|
synchronized (pendingResolution) {
|
||||||
pendingResolution.remove(info.getName());
|
pendingResolution.remove(info.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handleServiceInfo(info);
|
handleServiceInfo(info);
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
// Invalid DNS response
|
// Invalid DNS response
|
||||||
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
|
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Inet6Address getLocalAddress(Inet6Address[] addresses) {
|
private Inet6Address getLocalAddress(Inet6Address[] addresses) {
|
||||||
for (Inet6Address addr : addresses) {
|
for (Inet6Address addr : addresses) {
|
||||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
// fc00::/7 - ULAs
|
// fc00::/7 - ULAs
|
||||||
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
|
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
||||||
for (Inet6Address addr : addresses) {
|
for (Inet6Address addr : addresses) {
|
||||||
if (addr.isLinkLocalAddress()) {
|
if (addr.isLinkLocalAddress()) {
|
||||||
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
|
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
|
||||||
// First try to find a link local address, so we can match the interface identifier
|
// First try to find a link local address, so we can match the interface identifier
|
||||||
// with a global address (this will work for SLAAC but not DHCPv6).
|
// with a global address (this will work for SLAAC but not DHCPv6).
|
||||||
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
||||||
|
|
||||||
// We will try once to match a SLAAC interface suffix, then
|
// We will try once to match a SLAAC interface suffix, then
|
||||||
// pick the first matching address
|
// pick the first matching address
|
||||||
for (int tries = 0; tries < 2; tries++) {
|
for (int tries = 0; tries < 2; tries++) {
|
||||||
// We assume the addresses are already sorted in descending order
|
// We assume the addresses are already sorted in descending order
|
||||||
// of preference from Bonjour.
|
// of preference from Bonjour.
|
||||||
for (Inet6Address addr : addresses) {
|
for (Inet6Address addr : addresses) {
|
||||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
|
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
|
||||||
// Link-local, site-local, and loopback aren't global
|
// Link-local, site-local, and loopback aren't global
|
||||||
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
|
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] addrBytes = addr.getAddress();
|
byte[] addrBytes = addr.getAddress();
|
||||||
|
|
||||||
// 2002::/16
|
// 2002::/16
|
||||||
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
|
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
|
||||||
// 6to4 has horrible performance
|
// 6to4 has horrible performance
|
||||||
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
|
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 2001::/32
|
// 2001::/32
|
||||||
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
|
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
|
||||||
// Teredo also has horrible performance
|
// Teredo also has horrible performance
|
||||||
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
|
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// fc00::/7
|
// fc00::/7
|
||||||
else if ((addrBytes[0] & 0xfe) == 0xfc) {
|
else if ((addrBytes[0] & 0xfe) == 0xfc) {
|
||||||
// ULAs aren't global
|
// ULAs aren't global
|
||||||
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
|
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare the final 64-bit interface identifier and skip the address
|
// Compare the final 64-bit interface identifier and skip the address
|
||||||
// if it doesn't match our link-local address.
|
// if it doesn't match our link-local address.
|
||||||
if (linkLocalAddr != null && tries == 0) {
|
if (linkLocalAddr != null && tries == 0) {
|
||||||
boolean matched = true;
|
boolean matched = true;
|
||||||
|
|
||||||
for (int i = 8; i < 16; i++) {
|
for (int i = 8; i < 16; i++) {
|
||||||
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
|
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
|
||||||
matched = false;
|
matched = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matched) {
|
if (!matched) {
|
||||||
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
|
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
|
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
|
||||||
Inet4Address v4Addrs[] = info.getInet4Addresses();
|
Inet4Address v4Addrs[] = info.getInet4Addresses();
|
||||||
Inet6Address v6Addrs[] = info.getInet6Addresses();
|
Inet6Address v6Addrs[] = info.getInet6Addresses();
|
||||||
|
|
||||||
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
|
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
|
||||||
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
|
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
|
||||||
|
|
||||||
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
|
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
|
||||||
|
|
||||||
// Add a computer object for each IPv4 address reported by the PC
|
// Add a computer object for each IPv4 address reported by the PC
|
||||||
for (Inet4Address v4Addr : v4Addrs) {
|
for (Inet4Address v4Addr : v4Addrs) {
|
||||||
synchronized (computers) {
|
synchronized (computers) {
|
||||||
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
|
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
|
||||||
if (computers.put(computer.getLocalAddress(), computer) == null) {
|
if (computers.put(computer.getLocalAddress(), computer) == null) {
|
||||||
// This was a new entry
|
// This was a new entry
|
||||||
listener.notifyComputerAdded(computer);
|
listener.notifyComputerAdded(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there were no IPv4 addresses, use IPv6 for registration
|
// If there were no IPv4 addresses, use IPv6 for registration
|
||||||
if (v4Addrs.length == 0) {
|
if (v4Addrs.length == 0) {
|
||||||
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
||||||
|
|
||||||
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
||||||
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
|
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
|
||||||
if (computers.put(v6LocalAddr != null ?
|
if (computers.put(v6LocalAddr != null ?
|
||||||
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
|
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
|
||||||
// This was a new entry
|
// This was a new entry
|
||||||
listener.notifyComputerAdded(computer);
|
listener.notifyComputerAdded(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startDiscovery(final int discoveryIntervalMs) {
|
public void startDiscovery(final int discoveryIntervalMs) {
|
||||||
// Kill any existing discovery before starting a new one
|
// Kill any existing discovery before starting a new one
|
||||||
stopDiscovery();
|
stopDiscovery();
|
||||||
|
|
||||||
// Add our listener to the set
|
// Add our listener to the set
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
listeners.add(MdnsDiscoveryAgent.this);
|
listeners.add(MdnsDiscoveryAgent.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
discoveryThread = new Thread() {
|
discoveryThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// This may result in listener callbacks so we must register
|
// This may result in listener callbacks so we must register
|
||||||
// our listener first.
|
// our listener first.
|
||||||
JmmDNS resolver = referenceResolver();
|
JmmDNS resolver = referenceResolver();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!Thread.interrupted()) {
|
while (!Thread.interrupted()) {
|
||||||
// Start an mDNS request
|
// Start an mDNS request
|
||||||
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
|
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
|
||||||
|
|
||||||
// Run service resolution again for pending machines
|
// Run service resolution again for pending machines
|
||||||
ArrayList<String> pendingNames;
|
ArrayList<String> pendingNames;
|
||||||
synchronized (pendingResolution) {
|
synchronized (pendingResolution) {
|
||||||
pendingNames = new ArrayList<String>(pendingResolution);
|
pendingNames = new ArrayList<String>(pendingResolution);
|
||||||
}
|
}
|
||||||
for (String name : pendingNames) {
|
for (String name : pendingNames) {
|
||||||
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
|
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
|
||||||
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
|
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
|
||||||
if (infos != null && infos.length != 0) {
|
if (infos != null && infos.length != 0) {
|
||||||
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
|
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
|
||||||
for (ServiceInfo svcinfo : infos) {
|
for (ServiceInfo svcinfo : infos) {
|
||||||
handleResolvedServiceInfo(svcinfo);
|
handleResolvedServiceInfo(svcinfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the next polling interval
|
// Wait for the next polling interval
|
||||||
try {
|
try {
|
||||||
Thread.sleep(discoveryIntervalMs);
|
Thread.sleep(discoveryIntervalMs);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
// Dereference the resolver
|
// Dereference the resolver
|
||||||
dereferenceResolver();
|
dereferenceResolver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
discoveryThread.setName("mDNS Discovery Thread");
|
discoveryThread.setName("mDNS Discovery Thread");
|
||||||
discoveryThread.start();
|
discoveryThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopDiscovery() {
|
public void stopDiscovery() {
|
||||||
// Remove our listener from the set
|
// Remove our listener from the set
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
listeners.remove(MdnsDiscoveryAgent.this);
|
listeners.remove(MdnsDiscoveryAgent.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's already a running thread, interrupt it
|
// If there's already a running thread, interrupt it
|
||||||
if (discoveryThread != null) {
|
if (discoveryThread != null) {
|
||||||
discoveryThread.interrupt();
|
discoveryThread.interrupt();
|
||||||
discoveryThread = null;
|
discoveryThread = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MdnsComputer> getComputerSet() {
|
public List<MdnsComputer> getComputerSet() {
|
||||||
synchronized (computers) {
|
synchronized (computers) {
|
||||||
return new ArrayList<MdnsComputer>(computers.values());
|
return new ArrayList<MdnsComputer>(computers.values());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serviceAdded(ServiceEvent event) {
|
public void serviceAdded(ServiceEvent event) {
|
||||||
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
|
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
|
||||||
|
|
||||||
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
|
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
// This machine is pending resolution
|
// This machine is pending resolution
|
||||||
synchronized (pendingResolution) {
|
synchronized (pendingResolution) {
|
||||||
pendingResolution.add(event.getInfo().getName());
|
pendingResolution.add(event.getInfo().getName());
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("mDNS: Resolved (blocking)");
|
LimeLog.info("mDNS: Resolved (blocking)");
|
||||||
handleResolvedServiceInfo(info);
|
handleResolvedServiceInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serviceRemoved(ServiceEvent event) {
|
public void serviceRemoved(ServiceEvent event) {
|
||||||
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
|
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
|
||||||
|
|
||||||
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
|
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
|
||||||
for (Inet4Address addr : v4Addrs) {
|
for (Inet4Address addr : v4Addrs) {
|
||||||
synchronized (computers) {
|
synchronized (computers) {
|
||||||
MdnsComputer computer = computers.remove(addr);
|
MdnsComputer computer = computers.remove(addr);
|
||||||
if (computer != null) {
|
if (computer != null) {
|
||||||
listener.notifyComputerRemoved(computer);
|
listener.notifyComputerRemoved(computer);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
|
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
|
||||||
for (Inet6Address addr : v6Addrs) {
|
for (Inet6Address addr : v6Addrs) {
|
||||||
synchronized (computers) {
|
synchronized (computers) {
|
||||||
MdnsComputer computer = computers.remove(addr);
|
MdnsComputer computer = computers.remove(addr);
|
||||||
if (computer != null) {
|
if (computer != null) {
|
||||||
listener.notifyComputerRemoved(computer);
|
listener.notifyComputerRemoved(computer);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serviceResolved(ServiceEvent event) {
|
public void serviceResolved(ServiceEvent event) {
|
||||||
// We handle this synchronously
|
// We handle this synchronously
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.limelight.nvstream.mdns;
|
package com.limelight.nvstream.mdns;
|
||||||
|
|
||||||
public interface MdnsDiscoveryListener {
|
public interface MdnsDiscoveryListener {
|
||||||
void notifyComputerAdded(MdnsComputer computer);
|
void notifyComputerAdded(MdnsComputer computer);
|
||||||
void notifyComputerRemoved(MdnsComputer computer);
|
void notifyComputerRemoved(MdnsComputer computer);
|
||||||
void notifyDiscoveryFailure(Exception e);
|
void notifyDiscoveryFailure(Exception e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,90 +10,91 @@ import com.limelight.LimeLog;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
public class WakeOnLanSender {
|
public class WakeOnLanSender {
|
||||||
private static final int[] PORTS_TO_TRY = new int[] {
|
private static final int[] PORTS_TO_TRY = new int[] {
|
||||||
7, 9, // Standard WOL ports
|
9, // Standard WOL port (privileged port)
|
||||||
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
|
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||||
};
|
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||||
|
};
|
||||||
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
|
||||||
DatagramSocket sock = new DatagramSocket(0);
|
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
||||||
byte[] payload = createWolPayload(computer);
|
DatagramSocket sock = new DatagramSocket(0);
|
||||||
IOException lastException = null;
|
byte[] payload = createWolPayload(computer);
|
||||||
boolean sentWolPacket = false;
|
IOException lastException = null;
|
||||||
|
boolean sentWolPacket = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try all resolved remote and local addresses and IPv4 broadcast address.
|
// Try all resolved remote and local addresses and IPv4 broadcast address.
|
||||||
// The broadcast address is required to avoid stale ARP cache entries
|
// The broadcast address is required to avoid stale ARP cache entries
|
||||||
// making the sleeping machine unreachable.
|
// making the sleeping machine unreachable.
|
||||||
for (String unresolvedAddress : new String[] {
|
for (String unresolvedAddress : new String[] {
|
||||||
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
|
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
|
||||||
}) {
|
}) {
|
||||||
if (unresolvedAddress == null) {
|
if (unresolvedAddress == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
|
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
|
||||||
// Try all the ports for each resolved address
|
// Try all the ports for each resolved address
|
||||||
for (int port : PORTS_TO_TRY) {
|
for (int port : PORTS_TO_TRY) {
|
||||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||||
dp.setAddress(resolvedAddress);
|
dp.setAddress(resolvedAddress);
|
||||||
dp.setPort(port);
|
dp.setPort(port);
|
||||||
sock.send(dp);
|
sock.send(dp);
|
||||||
sentWolPacket = true;
|
sentWolPacket = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// We may have addresses that don't resolve on this subnet,
|
// We may have addresses that don't resolve on this subnet,
|
||||||
// but don't throw and exit the whole function if that happens.
|
// but don't throw and exit the whole function if that happens.
|
||||||
// We'll throw it at the end if we didn't send a single packet.
|
// We'll throw it at the end if we didn't send a single packet.
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
lastException = e;
|
lastException = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
sock.close();
|
sock.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Propagate the DNS resolution exception if we didn't
|
// Propagate the DNS resolution exception if we didn't
|
||||||
// manage to get a single packet out to the host.
|
// manage to get a single packet out to the host.
|
||||||
if (!sentWolPacket && lastException != null) {
|
if (!sentWolPacket && lastException != null) {
|
||||||
throw lastException;
|
throw lastException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] macStringToBytes(String macAddress) {
|
private static byte[] macStringToBytes(String macAddress) {
|
||||||
byte[] macBytes = new byte[6];
|
byte[] macBytes = new byte[6];
|
||||||
@SuppressWarnings("resource")
|
@SuppressWarnings("resource")
|
||||||
Scanner scan = new Scanner(macAddress).useDelimiter(":");
|
Scanner scan = new Scanner(macAddress).useDelimiter(":");
|
||||||
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
||||||
try {
|
try {
|
||||||
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
|
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scan.close();
|
scan.close();
|
||||||
return macBytes;
|
return macBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] createWolPayload(ComputerDetails computer) {
|
private static byte[] createWolPayload(ComputerDetails computer) {
|
||||||
byte[] payload = new byte[102];
|
byte[] payload = new byte[102];
|
||||||
byte[] macAddress = macStringToBytes(computer.macAddress);
|
byte[] macAddress = macStringToBytes(computer.macAddress);
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
// 6 bytes of FF
|
// 6 bytes of FF
|
||||||
for (i = 0; i < 6; i++) {
|
for (i = 0; i < 6; i++) {
|
||||||
payload[i] = (byte)0xFF;
|
payload[i] = (byte)0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 16 repetitions of the MAC address
|
// 16 repetitions of the MAC address
|
||||||
for (int j = 0; j < 16; j++) {
|
for (int j = 0; j < 16; j++) {
|
||||||
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
|
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
|
||||||
i += macAddress.length;
|
i += macAddress.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import com.limelight.computers.ComputerManagerService;
|
|||||||
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.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
@@ -97,6 +99,7 @@ public class AddComputerManually extends Activity {
|
|||||||
private void doAddPc(String host) {
|
private void doAddPc(String host) {
|
||||||
boolean wrongSiteLocal = false;
|
boolean wrongSiteLocal = false;
|
||||||
boolean success;
|
boolean success;
|
||||||
|
int portTestResult;
|
||||||
|
|
||||||
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);
|
||||||
@@ -104,12 +107,6 @@ public class AddComputerManually extends Activity {
|
|||||||
try {
|
try {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
details.manualAddress = host;
|
details.manualAddress = host;
|
||||||
|
|
||||||
try {
|
|
||||||
NvHTTP http = new NvHTTP(host, managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(this));
|
|
||||||
details.serverCert = http.getCertificateIfTrusted();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
|
|
||||||
success = managerBinder.addComputerBlocking(details);
|
success = managerBinder.addComputerBlocking(details);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||||
@@ -120,6 +117,14 @@ public class AddComputerManually extends Activity {
|
|||||||
if (!success){
|
if (!success){
|
||||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||||
}
|
}
|
||||||
|
if (!success && !wrongSiteLocal) {
|
||||||
|
// Run the test before dismissing the spinner because it can take a few seconds.
|
||||||
|
portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER, 443,
|
||||||
|
MoonBridge.ML_PORT_FLAG_TCP_47984 | MoonBridge.ML_PORT_FLAG_TCP_47989);
|
||||||
|
} else {
|
||||||
|
// Don't bother with the test if we succeeded or the IP address was bogus
|
||||||
|
portTestResult = MoonBridge.ML_TEST_RESULT_INCONCLUSIVE;
|
||||||
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
|
||||||
@@ -127,7 +132,14 @@ public class AddComputerManually extends Activity {
|
|||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
||||||
}
|
}
|
||||||
else if (!success) {
|
else if (!success) {
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
|
String dialogText;
|
||||||
|
if (portTestResult != MoonBridge.ML_TEST_RESULT_INCONCLUSIVE && portTestResult != 0) {
|
||||||
|
dialogText = getResources().getString(R.string.nettest_text_blocked);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialogText = getResources().getString(R.string.addpc_fail);
|
||||||
|
}
|
||||||
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), dialogText, false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import android.content.pm.PackageManager;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
public class PreferenceConfiguration {
|
||||||
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||||
|
private static final String LEGACY_ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
||||||
|
|
||||||
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
||||||
static final String FPS_PREF_STRING = "list_fps";
|
static final String FPS_PREF_STRING = "list_fps";
|
||||||
@@ -19,11 +21,11 @@ public class PreferenceConfiguration {
|
|||||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||||
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
|
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
|
||||||
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
||||||
|
private static final String OSC_OPACITY_PREF_STRING = "seekbar_osc_opacity";
|
||||||
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
||||||
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
|
||||||
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
||||||
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
||||||
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
static final String AUDIO_CONFIG_PREF_STRING = "list_audio_config";
|
||||||
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";
|
||||||
@@ -38,18 +40,20 @@ public class PreferenceConfiguration {
|
|||||||
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
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_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
||||||
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
||||||
|
private static final String FLIP_FACE_BUTTONS_PREF_STRING = "checkbox_flip_face_buttons";
|
||||||
|
private static final String TOUCHSCREEN_TRACKPAD_PREF_STRING = "checkbox_touchscreen_trackpad";
|
||||||
|
private static final String LATENCY_TOAST_PREF_STRING = "checkbox_enable_post_stream_toast";
|
||||||
|
|
||||||
static final String DEFAULT_RESOLUTION = "720p";
|
static final String DEFAULT_RESOLUTION = "1280x720";
|
||||||
static final String DEFAULT_FPS = "60";
|
static final String DEFAULT_FPS = "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;
|
||||||
private static final boolean DEFAULT_HOST_AUDIO = false;
|
private static final boolean DEFAULT_HOST_AUDIO = false;
|
||||||
private static final int DEFAULT_DEADZONE = 15;
|
private static final int DEFAULT_DEADZONE = 15;
|
||||||
|
private static final int DEFAULT_OPACITY = 90;
|
||||||
public static final String DEFAULT_LANGUAGE = "default";
|
public static final String DEFAULT_LANGUAGE = "default";
|
||||||
private static final boolean DEFAULT_LIST_MODE = false;
|
|
||||||
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
||||||
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
|
|
||||||
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;
|
||||||
@@ -64,83 +68,95 @@ public class PreferenceConfiguration {
|
|||||||
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
||||||
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
||||||
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
||||||
|
private static final boolean DEFAULT_FLIP_FACE_BUTTONS = false;
|
||||||
|
private static final boolean DEFAULT_TOUCHSCREEN_TRACKPAD = true;
|
||||||
|
private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo
|
||||||
|
private static final boolean DEFAULT_LATENCY_TOAST = 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;
|
||||||
public static final int FORCE_H265_OFF = 1;
|
public static final int FORCE_H265_OFF = 1;
|
||||||
|
|
||||||
|
public static final String RES_360P = "640x360";
|
||||||
|
public static final String RES_480P = "854x480";
|
||||||
|
public static final String RES_720P = "1280x720";
|
||||||
|
public static final String RES_1080P = "1920x1080";
|
||||||
|
public static final String RES_1440P = "2560x1440";
|
||||||
|
public static final String RES_4K = "3840x2160";
|
||||||
|
public static final String RES_NATIVE = "Native";
|
||||||
|
|
||||||
public int width, height, fps;
|
public int width, height, fps;
|
||||||
public int bitrate;
|
public int bitrate;
|
||||||
public int videoFormat;
|
public int videoFormat;
|
||||||
public int deadzonePercentage;
|
public int deadzonePercentage;
|
||||||
|
public int oscOpacity;
|
||||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
public boolean smallIconMode, multiController, usbDriver, flipFaceButtons;
|
||||||
public boolean onscreenController;
|
public boolean onscreenController;
|
||||||
public boolean onlyL3R3;
|
public boolean onlyL3R3;
|
||||||
public boolean disableFrameDrop;
|
public boolean disableFrameDrop;
|
||||||
public boolean enableHdr;
|
public boolean enableHdr;
|
||||||
public boolean enablePip;
|
public boolean enablePip;
|
||||||
public boolean enablePerfOverlay;
|
public boolean enablePerfOverlay;
|
||||||
|
public boolean enableLatencyToast;
|
||||||
public boolean bindAllUsb;
|
public boolean bindAllUsb;
|
||||||
public boolean mouseEmulation;
|
public boolean mouseEmulation;
|
||||||
public boolean mouseNavButtons;
|
public boolean mouseNavButtons;
|
||||||
public boolean unlockFps;
|
public boolean unlockFps;
|
||||||
public boolean vibrateOsc;
|
public boolean vibrateOsc;
|
||||||
public boolean vibrateFallbackToDevice;
|
public boolean vibrateFallbackToDevice;
|
||||||
|
public boolean touchscreenTrackpad;
|
||||||
|
public MoonBridge.AudioConfiguration audioConfiguration;
|
||||||
|
|
||||||
private static int getHeightFromResolutionString(String resString) {
|
private static String convertFromLegacyResolutionString(String resString) {
|
||||||
if (resString.equalsIgnoreCase("360p")) {
|
if (resString.equalsIgnoreCase("360p")) {
|
||||||
return 360;
|
return RES_360P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("480p")) {
|
else if (resString.equalsIgnoreCase("480p")) {
|
||||||
return 480;
|
return RES_480P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("720p")) {
|
else if (resString.equalsIgnoreCase("720p")) {
|
||||||
return 720;
|
return RES_720P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("1080p")) {
|
else if (resString.equalsIgnoreCase("1080p")) {
|
||||||
return 1080;
|
return RES_1080P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("1440p")) {
|
else if (resString.equalsIgnoreCase("1440p")) {
|
||||||
return 1440;
|
return RES_1440P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("4K")) {
|
else if (resString.equalsIgnoreCase("4K")) {
|
||||||
return 2160;
|
return RES_4K;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should be unreachable
|
// Should be unreachable
|
||||||
return 720;
|
return RES_720P;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getWidthFromResolutionString(String resString) {
|
private static int getWidthFromResolutionString(String resString) {
|
||||||
int height = getHeightFromResolutionString(resString);
|
return Integer.parseInt(resString.split("x")[0]);
|
||||||
if (height == 480) {
|
}
|
||||||
// This isn't an exact 16:9 resolution
|
|
||||||
return 854;
|
private static int getHeightFromResolutionString(String resString) {
|
||||||
}
|
return Integer.parseInt(resString.split("x")[1]);
|
||||||
else {
|
|
||||||
return (height * 16) / 9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getResolutionString(int width, int height) {
|
private static String getResolutionString(int width, int height) {
|
||||||
switch (height) {
|
switch (height) {
|
||||||
case 360:
|
case 360:
|
||||||
return "360p";
|
return RES_360P;
|
||||||
case 480:
|
case 480:
|
||||||
return "480p";
|
return RES_480P;
|
||||||
default:
|
default:
|
||||||
case 720:
|
case 720:
|
||||||
return "720p";
|
return RES_720P;
|
||||||
case 1080:
|
case 1080:
|
||||||
return "1080p";
|
return RES_1080P;
|
||||||
case 1440:
|
case 1440:
|
||||||
return "1440p";
|
return RES_1440P;
|
||||||
case 2160:
|
case 2160:
|
||||||
return "4K";
|
return RES_4K;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +260,15 @@ public class PreferenceConfiguration {
|
|||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||||
|
|
||||||
// Migrate legacy preferences to the new locations
|
// Migrate legacy preferences to the new locations
|
||||||
|
if (prefs.contains(LEGACY_ENABLE_51_SURROUND_PREF_STRING)) {
|
||||||
|
if (prefs.getBoolean(LEGACY_ENABLE_51_SURROUND_PREF_STRING, false)) {
|
||||||
|
prefs.edit()
|
||||||
|
.remove(LEGACY_ENABLE_51_SURROUND_PREF_STRING)
|
||||||
|
.putString(AUDIO_CONFIG_PREF_STRING, "51")
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
||||||
if (str != null) {
|
if (str != null) {
|
||||||
if (str.equals("360p30")) {
|
if (str.equals("360p30")) {
|
||||||
@@ -302,21 +327,47 @@ public class PreferenceConfiguration {
|
|||||||
else {
|
else {
|
||||||
// Use the new preference location
|
// Use the new preference location
|
||||||
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||||
|
|
||||||
|
// Convert legacy resolution strings to the new style
|
||||||
|
if (!resStr.contains("x")) {
|
||||||
|
resStr = PreferenceConfiguration.convertFromLegacyResolutionString(resStr);
|
||||||
|
prefs.edit().putString(RESOLUTION_PREF_STRING, resStr).apply();
|
||||||
|
}
|
||||||
|
|
||||||
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||||
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
||||||
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!prefs.contains(SMALL_ICONS_PREF_STRING)) {
|
||||||
|
// We need to write small icon mode's default to disk for the settings page to display
|
||||||
|
// the current state of the option properly
|
||||||
|
prefs.edit().putBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)).apply();
|
||||||
|
}
|
||||||
|
|
||||||
// This must happen after the preferences migration to ensure the preferences are populated
|
// 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);
|
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
||||||
if (config.bitrate == 0) {
|
if (config.bitrate == 0) {
|
||||||
config.bitrate = getDefaultBitrate(context);
|
config.bitrate = getDefaultBitrate(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String audioConfig = prefs.getString(AUDIO_CONFIG_PREF_STRING, DEFAULT_AUDIO_CONFIG);
|
||||||
|
if (audioConfig.equals("71")) {
|
||||||
|
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_71_SURROUND;
|
||||||
|
}
|
||||||
|
else if (audioConfig.equals("51")) {
|
||||||
|
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_51_SURROUND;
|
||||||
|
}
|
||||||
|
else /* if (audioConfig.equals("2")) */ {
|
||||||
|
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
|
||||||
|
}
|
||||||
|
|
||||||
config.videoFormat = getVideoFormatValue(context);
|
config.videoFormat = getVideoFormatValue(context);
|
||||||
|
|
||||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||||
|
|
||||||
|
config.oscOpacity = prefs.getInt(OSC_OPACITY_PREF_STRING, DEFAULT_OPACITY);
|
||||||
|
|
||||||
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
|
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
|
||||||
|
|
||||||
// Checkbox preferences
|
// Checkbox preferences
|
||||||
@@ -324,10 +375,8 @@ public class PreferenceConfiguration {
|
|||||||
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
||||||
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
||||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||||
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
|
||||||
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
||||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
||||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
|
||||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
config.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.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
||||||
@@ -341,6 +390,9 @@ public class PreferenceConfiguration {
|
|||||||
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
||||||
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
||||||
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
||||||
|
config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PREF_STRING, DEFAULT_FLIP_FACE_BUTTONS);
|
||||||
|
config.touchscreenTrackpad = prefs.getBoolean(TOUCHSCREEN_TRACKPAD_PREF_STRING, DEFAULT_TOUCHSCREEN_TRACKPAD);
|
||||||
|
config.enableLatencyToast = prefs.getBoolean(LATENCY_TOAST_PREF_STRING, DEFAULT_LATENCY_TOAST);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.Context;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.DialogPreference;
|
import android.preference.DialogPreference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
@@ -13,6 +14,8 @@ import android.widget.LinearLayout;
|
|||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
// 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
|
||||||
{
|
{
|
||||||
@@ -29,6 +32,8 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
private final int maxValue;
|
private final int maxValue;
|
||||||
private final int minValue;
|
private final int minValue;
|
||||||
private final int stepSize;
|
private final int stepSize;
|
||||||
|
private final int keyStepSize;
|
||||||
|
private final int divisor;
|
||||||
private int currentValue;
|
private int currentValue;
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
@@ -58,6 +63,8 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
||||||
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
||||||
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
||||||
|
divisor = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "divisor", 1);
|
||||||
|
keyStepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "keyStep", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -78,6 +85,8 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
valueText = new TextView(context);
|
valueText = new TextView(context);
|
||||||
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
|
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||||
valueText.setTextSize(32);
|
valueText.setTextSize(32);
|
||||||
|
// Default text for value; hides bug where OnSeekBarChangeListener isn't called when opacity is 0%
|
||||||
|
valueText.setText("0%");
|
||||||
params = new LinearLayout.LayoutParams(
|
params = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
@@ -98,7 +107,14 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String t = String.valueOf(value);
|
String t;
|
||||||
|
if (divisor != 1) {
|
||||||
|
float floatValue = roundedValue / (float)divisor;
|
||||||
|
t = String.format((Locale)null, "%.1f", floatValue);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +132,9 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
}
|
}
|
||||||
|
|
||||||
seekBar.setMax(maxValue);
|
seekBar.setMax(maxValue);
|
||||||
|
if (keyStepSize != 0) {
|
||||||
|
seekBar.setKeyProgressIncrement(keyStepSize);
|
||||||
|
}
|
||||||
seekBar.setProgress(currentValue);
|
seekBar.setProgress(currentValue);
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
@@ -125,6 +144,9 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
protected void onBindDialogView(View v) {
|
protected void onBindDialogView(View v) {
|
||||||
super.onBindDialogView(v);
|
super.onBindDialogView(v);
|
||||||
seekBar.setMax(maxValue);
|
seekBar.setMax(maxValue);
|
||||||
|
if (keyStepSize != 0) {
|
||||||
|
seekBar.setKeyProgressIncrement(keyStepSize);
|
||||||
|
}
|
||||||
seekBar.setProgress(currentValue);
|
seekBar.setProgress(currentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
@@ -7,6 +8,7 @@ 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.os.Handler;
|
||||||
|
import android.os.Vibrator;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceCategory;
|
import android.preference.PreferenceCategory;
|
||||||
@@ -23,8 +25,11 @@ 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.binding.video.MediaCodecHelper;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
@@ -54,9 +59,7 @@ public class StreamSettings extends Activity {
|
|||||||
|
|
||||||
// Check for changes that require a UI reload to take effect
|
// Check for changes that require a UI reload to take effect
|
||||||
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
|
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
|
||||||
if (newPrefs.listMode != previousPrefs.listMode ||
|
if (!newPrefs.language.equals(previousPrefs.language)) {
|
||||||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
|
|
||||||
!newPrefs.language.equals(previousPrefs.language)) {
|
|
||||||
// Restart the PC view to apply UI changes
|
// Restart the PC view to apply UI changes
|
||||||
Intent intent = new Intent(this, PcView.class);
|
Intent intent = new Intent(this, PcView.class);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
@@ -65,6 +68,7 @@ public class StreamSettings extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
|
private boolean nativeResolutionOptionPresent = false;
|
||||||
|
|
||||||
private void setValue(String preferenceKey, String value) {
|
private void setValue(String preferenceKey, String value) {
|
||||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||||
@@ -72,6 +76,38 @@ public class StreamSettings extends Activity {
|
|||||||
pref.setValue(value);
|
pref.setValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateNativeResolutionEntry(int nativeWidth, int nativeHeight) {
|
||||||
|
ListPreference pref = (ListPreference) findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING);
|
||||||
|
|
||||||
|
String nameSuffix = " ("+nativeWidth+"x"+nativeHeight+")";
|
||||||
|
String newValue = nativeWidth+"x"+nativeHeight;
|
||||||
|
|
||||||
|
CharSequence[] entries = pref.getEntries();
|
||||||
|
CharSequence[] values = pref.getEntryValues();
|
||||||
|
|
||||||
|
// Check if the native resolution is already present
|
||||||
|
for (CharSequence value : values) {
|
||||||
|
if (newValue.equals(value.toString())) {
|
||||||
|
// It is present in the default list, so remove the native option
|
||||||
|
nativeResolutionOptionPresent = false;
|
||||||
|
|
||||||
|
pref.setEntries(Arrays.copyOf(entries, entries.length - 1));
|
||||||
|
pref.setEntryValues(Arrays.copyOf(values, values.length - 1));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the name suffix to the native option
|
||||||
|
entries[entries.length - 1] = entries[entries.length - 1].toString() + nameSuffix;
|
||||||
|
values[values.length - 1] = newValue;
|
||||||
|
|
||||||
|
pref.setEntries(entries);
|
||||||
|
pref.setEntryValues(values);
|
||||||
|
|
||||||
|
nativeResolutionOptionPresent = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||||
int matchingCount = 0;
|
int matchingCount = 0;
|
||||||
|
|
||||||
@@ -108,8 +144,6 @@ public class StreamSettings extends Activity {
|
|||||||
pref.setEntryValues(entryValues);
|
pref.setEntryValues(entryValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||||
@@ -141,18 +175,42 @@ public class StreamSettings extends Activity {
|
|||||||
// hide on-screen controls category on non touch screen devices
|
// hide on-screen controls category on non touch screen devices
|
||||||
if (!getActivity().getPackageManager().
|
if (!getActivity().getPackageManager().
|
||||||
hasSystemFeature("android.hardware.touchscreen")) {
|
hasSystemFeature("android.hardware.touchscreen")) {
|
||||||
PreferenceCategory category =
|
{
|
||||||
(PreferenceCategory) findPreference("category_onscreen_controls");
|
PreferenceCategory category =
|
||||||
screen.removePreference(category);
|
(PreferenceCategory) findPreference("category_onscreen_controls");
|
||||||
|
screen.removePreference(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_input_settings");
|
||||||
|
category.removePreference(findPreference("checkbox_touchscreen_trackpad"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove PiP mode on devices pre-Oreo
|
// Remove PiP mode on devices pre-Oreo, where the feature is not available (some low RAM devices),
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
// and on Fire OS where it violates the Amazon App Store guidelines for some reason.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
|
||||||
|
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture") ||
|
||||||
|
getActivity().getPackageManager().hasSystemFeature("com.amazon.software.fireos")) {
|
||||||
PreferenceCategory category =
|
PreferenceCategory category =
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
(PreferenceCategory) findPreference("category_ui_settings");
|
||||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
category.removePreference(findPreference("checkbox_enable_pip"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the vibration options if the device can't vibrate
|
||||||
|
if (!((Vibrator)getActivity().getSystemService(Context.VIBRATOR_SERVICE)).hasVibrator()) {
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_input_settings");
|
||||||
|
category.removePreference(findPreference("checkbox_vibrate_fallback"));
|
||||||
|
|
||||||
|
// The entire OSC category may have already been removed by the touchscreen check above
|
||||||
|
category = (PreferenceCategory) findPreference("category_onscreen_controls");
|
||||||
|
if (category != null) {
|
||||||
|
category.removePreference(findPreference("checkbox_vibrate_osc"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int maxSupportedFps = 0;
|
int maxSupportedFps = 0;
|
||||||
|
|
||||||
// Hide non-supported resolution/FPS combinations
|
// Hide non-supported resolution/FPS combinations
|
||||||
@@ -168,6 +226,7 @@ public class StreamSettings extends Activity {
|
|||||||
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
|
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
|
||||||
// AVC supported width range: 64 - 384
|
// AVC supported width range: 64 - 384
|
||||||
// HEVC supported width range: 64 - 544
|
// HEVC supported width range: 64 - 544
|
||||||
|
int nativeWidth = 0, nativeHeight = 0;
|
||||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
// Some devices report their dimensions in the portrait orientation
|
// Some devices report their dimensions in the portrait orientation
|
||||||
// where height > width. Normalize these to the conventional width > height
|
// where height > width. Normalize these to the conventional width > height
|
||||||
@@ -176,6 +235,13 @@ public class StreamSettings extends Activity {
|
|||||||
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
||||||
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
||||||
|
|
||||||
|
if (width > nativeWidth) {
|
||||||
|
nativeWidth = width;
|
||||||
|
}
|
||||||
|
if (height > nativeHeight) {
|
||||||
|
nativeHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||||
maxSupportedResW = 3840;
|
maxSupportedResW = 3840;
|
||||||
}
|
}
|
||||||
@@ -191,6 +257,8 @@ public class StreamSettings extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateNativeResolutionEntry(nativeWidth, nativeHeight);
|
||||||
|
|
||||||
// This must be called to do runtime initialization before calling functions that evaluate
|
// This must be called to do runtime initialization before calling functions that evaluate
|
||||||
// decoder lists.
|
// decoder lists.
|
||||||
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
|
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
|
||||||
@@ -241,33 +309,33 @@ public class StreamSettings extends Activity {
|
|||||||
if (maxSupportedResW != 0) {
|
if (maxSupportedResW != 0) {
|
||||||
if (maxSupportedResW < 3840) {
|
if (maxSupportedResW < 3840) {
|
||||||
// 4K is unsupported
|
// 4K is unsupported
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_4K, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P);
|
||||||
resetBitrateToDefault(prefs, null, null);
|
resetBitrateToDefault(prefs, null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (maxSupportedResW < 2560) {
|
if (maxSupportedResW < 2560) {
|
||||||
// 1440p is unsupported
|
// 1440p is unsupported
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P);
|
||||||
resetBitrateToDefault(prefs, null, null);
|
resetBitrateToDefault(prefs, null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (maxSupportedResW < 1920) {
|
if (maxSupportedResW < 1920) {
|
||||||
// 1080p is unsupported
|
// 1080p is unsupported
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_720P);
|
||||||
resetBitrateToDefault(prefs, null, null);
|
resetBitrateToDefault(prefs, null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -275,6 +343,12 @@ public class StreamSettings extends Activity {
|
|||||||
// Never remove 720p
|
// Never remove 720p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||||
|
int width = Math.max(display.getWidth(), display.getHeight());
|
||||||
|
int height = Math.min(display.getWidth(), display.getHeight());
|
||||||
|
updateNativeResolutionEntry(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
||||||
// We give some extra room in case the FPS is rounded down
|
// We give some extra room in case the FPS is rounded down
|
||||||
@@ -302,12 +376,24 @@ public class StreamSettings extends Activity {
|
|||||||
// Never remove 30 FPS or 60 FPS
|
// Never remove 30 FPS or 60 FPS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Android L introduces proper 7.1 surround sound support. Remove the 7.1 option
|
||||||
|
// for earlier versions of Android to prevent AudioTrack initialization issues.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
LimeLog.info("Excluding 7.1 surround sound option based on OS");
|
||||||
|
removeValue(PreferenceConfiguration.AUDIO_CONFIG_PREF_STRING, "71", new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
setValue(PreferenceConfiguration.AUDIO_CONFIG_PREF_STRING, "51");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
|
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
|
||||||
// that the unlock FPS option relies on to not massively increase latency.
|
// that the unlock FPS option relies on to not massively increase latency.
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
||||||
PreferenceCategory category =
|
PreferenceCategory category =
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
(PreferenceCategory) findPreference("category_advanced_settings");
|
||||||
category.removePreference(findPreference("checkbox_unlock_fps"));
|
category.removePreference(findPreference("checkbox_unlock_fps"));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -372,6 +458,27 @@ public class StreamSettings extends Activity {
|
|||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
String valueStr = (String) newValue;
|
String valueStr = (String) newValue;
|
||||||
|
|
||||||
|
if (nativeResolutionOptionPresent) {
|
||||||
|
// Detect if this value is the native resolution option
|
||||||
|
CharSequence[] values = ((ListPreference)preference).getEntryValues();
|
||||||
|
boolean isNativeRes = true;
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
// If get a match prior to the end, it's not native res
|
||||||
|
if (valueStr.equals(values[i].toString()) && i < values.length - 1) {
|
||||||
|
isNativeRes = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is native resolution, show the warning dialog
|
||||||
|
if (isNativeRes) {
|
||||||
|
Dialog.displayDialog(getActivity(),
|
||||||
|
getResources().getString(R.string.title_native_res_dialog),
|
||||||
|
getResources().getString(R.string.text_native_res_dialog),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write the new bitrate value
|
// Write the new bitrate value
|
||||||
resetBitrateToDefault(prefs, valueStr, null);
|
resetBitrateToDefault(prefs, valueStr, null);
|
||||||
|
|
||||||
|
|||||||
@@ -3,41 +3,22 @@ package com.limelight.utils;
|
|||||||
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.PackageManager;
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
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 {
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
i.setData(Uri.parse(url));
|
i.setData(Uri.parse(url));
|
||||||
|
|
||||||
// Several Android TV devices will lie and say they do have a browser
|
// Several Android TV devices will lie and say they do have a browser even though the OS
|
||||||
// even though the OS just shows an error dialog if we try to use it. We need to
|
// just shows an error dialog if we try to use it. We used to try to be clever and check
|
||||||
// be a bit more clever on these devices and detect if the browser is a legitimate
|
// the package name of the resolved intent, but it's not worth it anymore with Android 11's
|
||||||
// browser or just a fake error message activity.
|
// package visibility changes. We'll just always use the WebView on Android TV.
|
||||||
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
|
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
isKnownBrowser(context, i)) {
|
|
||||||
context.startActivity(i);
|
context.startActivity(i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.net.NetworkCapabilities;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
public class NetHelper {
|
||||||
|
public static boolean isActiveNetworkVpn(Context context) {
|
||||||
|
ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Network activeNetwork = connMgr.getActiveNetwork();
|
||||||
|
if (activeNetwork != null) {
|
||||||
|
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
|
||||||
|
if (netCaps != null) {
|
||||||
|
return netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||||
|
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||||
|
if (activeNetworkInfo != null) {
|
||||||
|
return activeNetworkInfo.getType() == ConnectivityManager.TYPE_VPN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import com.limelight.AppView;
|
import com.limelight.AppView;
|
||||||
import com.limelight.Game;
|
import com.limelight.Game;
|
||||||
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.ShortcutTrampoline;
|
import com.limelight.ShortcutTrampoline;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
@@ -14,6 +15,7 @@ import com.limelight.nvstream.http.ComputerDetails;
|
|||||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
@@ -23,6 +25,8 @@ import java.net.UnknownHostException;
|
|||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
|
||||||
public class ServerHelper {
|
public class ServerHelper {
|
||||||
|
public static final String CONNECTION_TEST_SERVER = "android.conntest.moonlight-stream.org";
|
||||||
|
|
||||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
return computer.activeAddress;
|
return computer.activeAddress;
|
||||||
}
|
}
|
||||||
@@ -76,6 +80,38 @@ public class ServerHelper {
|
|||||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void doNetworkTest(final Activity parent) {
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SpinnerDialog spinnerDialog = SpinnerDialog.displayDialog(parent,
|
||||||
|
parent.getResources().getString(R.string.nettest_title_waiting),
|
||||||
|
parent.getResources().getString(R.string.nettest_text_waiting),
|
||||||
|
false);
|
||||||
|
|
||||||
|
int ret = MoonBridge.testClientConnectivity(CONNECTION_TEST_SERVER, 443, MoonBridge.ML_PORT_FLAG_ALL);
|
||||||
|
spinnerDialog.dismiss();
|
||||||
|
|
||||||
|
String dialogSummary;
|
||||||
|
if (ret == MoonBridge.ML_TEST_RESULT_INCONCLUSIVE) {
|
||||||
|
dialogSummary = parent.getResources().getString(R.string.nettest_text_inconclusive);
|
||||||
|
}
|
||||||
|
else if (ret == 0) {
|
||||||
|
dialogSummary = parent.getResources().getString(R.string.nettest_text_success);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialogSummary = parent.getResources().getString(R.string.nettest_text_failure);
|
||||||
|
dialogSummary += MoonBridge.stringifyPortFlags(ret, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.displayDialog(parent,
|
||||||
|
parent.getResources().getString(R.string.nettest_title_done),
|
||||||
|
dialogSummary,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
public static void doQuit(final Activity parent,
|
||||||
final ComputerDetails computer,
|
final ComputerDetails computer,
|
||||||
final NvApp app,
|
final NvApp app,
|
||||||
|
|||||||
@@ -192,4 +192,13 @@ public class ShortcutHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void enableAppShortcut(ComputerDetails computer, NvApp app) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
String id = getShortcutIdForGame(computer, app);
|
||||||
|
if (getInfoForId(id) != null) {
|
||||||
|
sm.enableShortcuts(Collections.singletonList(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,18 @@ public class TvChannelHelper {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri channelUri = context.getContentResolver().insert(
|
Uri channelUri;
|
||||||
TvContract.Channels.CONTENT_URI, builder.toContentValues());
|
|
||||||
|
try {
|
||||||
|
channelUri = context.getContentResolver().insert(
|
||||||
|
TvContract.Channels.CONTENT_URI, builder.toContentValues());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This can happen on HarmonyOS devices which report to
|
||||||
|
// support Leanback APIs, yet don't implement this URI
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (channelUri != null) {
|
if (channelUri != null) {
|
||||||
long id = ContentUris.parseId(channelUri);
|
long id = ContentUris.parseId(channelUri);
|
||||||
updateChannelIcon(id);
|
updateChannelIcon(id);
|
||||||
@@ -144,8 +154,15 @@ public class TvChannelHelper {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
|
try {
|
||||||
builder.toContentValues());
|
context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
|
||||||
|
builder.toContentValues());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This can happen on HarmonyOS devices which report to
|
||||||
|
// support Leanback APIs, yet don't implement this URI
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TvContract.requestChannelBrowsable(context, channelId);
|
TvContract.requestChannelBrowsable(context, channelId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,17 +68,6 @@ public class UiHelper {
|
|||||||
View rootView = activity.findViewById(android.R.id.content);
|
View rootView = activity.findViewById(android.R.id.content);
|
||||||
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
||||||
|
|
||||||
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
|
|
||||||
{
|
|
||||||
// Increase view padding on TVs
|
|
||||||
float scale = activity.getResources().getDisplayMetrics().density;
|
|
||||||
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
|
|
||||||
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
|
|
||||||
|
|
||||||
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
|
||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
// Allow this non-streaming activity to layout under notches.
|
// Allow this non-streaming activity to layout under notches.
|
||||||
//
|
//
|
||||||
@@ -89,7 +78,16 @@ public class UiHelper {
|
|||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
|
||||||
|
// Increase view padding on TVs
|
||||||
|
float scale = activity.getResources().getDisplayMetrics().density;
|
||||||
|
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
|
||||||
|
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
|
||||||
|
|
||||||
|
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||||
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
// Draw under the status bar on Android Q devices
|
// Draw under the status bar on Android Q devices
|
||||||
|
|
||||||
// Using getDecorView() here breaks the translucent status/navigation bar when gestures are disabled
|
// Using getDecorView() here breaks the translucent status/navigation bar when gestures are disabled
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
public class Vector2d {
|
public class Vector2d {
|
||||||
private float x;
|
private float x;
|
||||||
private float y;
|
private float y;
|
||||||
private double magnitude;
|
private double magnitude;
|
||||||
|
|
||||||
public static final Vector2d ZERO = new Vector2d();
|
public static final Vector2d ZERO = new Vector2d();
|
||||||
|
|
||||||
public Vector2d() {
|
public Vector2d() {
|
||||||
initialize(0, 0);
|
initialize(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize(float x, float y) {
|
public void initialize(float x, float y) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getMagnitude() {
|
public double getMagnitude() {
|
||||||
return magnitude;
|
return magnitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getNormalized(Vector2d vector) {
|
public void getNormalized(Vector2d vector) {
|
||||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scalarMultiply(double factor) {
|
public void scalarMultiply(double factor) {
|
||||||
initialize((float)(x * factor), (float)(y * factor));
|
initialize((float)(x * factor), (float)(y * factor));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setX(float x) {
|
public void setX(float x) {
|
||||||
initialize(x, this.y);
|
initialize(x, this.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setY(float y) {
|
public void setY(float y) {
|
||||||
initialize(this.x, y);
|
initialize(this.x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getX() {
|
public float getX() {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getY() {
|
public float getY() {
|
||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ LOCAL_MODULE := moonlight-core
|
|||||||
LOCAL_SRC_FILES := moonlight-common-c/src/AudioStream.c \
|
LOCAL_SRC_FILES := moonlight-common-c/src/AudioStream.c \
|
||||||
moonlight-common-c/src/ByteBuffer.c \
|
moonlight-common-c/src/ByteBuffer.c \
|
||||||
moonlight-common-c/src/Connection.c \
|
moonlight-common-c/src/Connection.c \
|
||||||
|
moonlight-common-c/src/ConnectionTester.c \
|
||||||
moonlight-common-c/src/ControlStream.c \
|
moonlight-common-c/src/ControlStream.c \
|
||||||
moonlight-common-c/src/FakeCallbacks.c \
|
moonlight-common-c/src/FakeCallbacks.c \
|
||||||
moonlight-common-c/src/InputStream.c \
|
moonlight-common-c/src/InputStream.c \
|
||||||
|
|||||||
@@ -80,16 +80,16 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
|
|||||||
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
|
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
|
||||||
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
|
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
|
||||||
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I");
|
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I");
|
||||||
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(I)I");
|
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(III)I");
|
||||||
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
|
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
|
||||||
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
|
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
|
||||||
BridgeArCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArCleanup", "()V");
|
BridgeArCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArCleanup", "()V");
|
||||||
BridgeArPlaySampleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArPlaySample", "([S)V");
|
BridgeArPlaySampleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArPlaySample", "([S)V");
|
||||||
BridgeClStageStartingMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageStarting", "(I)V");
|
BridgeClStageStartingMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageStarting", "(I)V");
|
||||||
BridgeClStageCompleteMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageComplete", "(I)V");
|
BridgeClStageCompleteMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageComplete", "(I)V");
|
||||||
BridgeClStageFailedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageFailed", "(IJ)V");
|
BridgeClStageFailedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageFailed", "(II)V");
|
||||||
BridgeClConnectionStartedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStarted", "()V");
|
BridgeClConnectionStartedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStarted", "()V");
|
||||||
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(J)V");
|
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(I)V");
|
||||||
BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V");
|
BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V");
|
||||||
BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V");
|
BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V");
|
||||||
}
|
}
|
||||||
@@ -98,12 +98,9 @@ int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void*
|
|||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSetupMethod, videoFormat, width, height, redrawRate);
|
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSetupMethod, videoFormat, width, height, redrawRate);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
// This is called on a Java thread, so it's safe to return
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
else if (err != 0) {
|
else if (err != 0) {
|
||||||
@@ -119,20 +116,12 @@ int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void*
|
|||||||
void BridgeDrStart(void) {
|
void BridgeDrStart(void) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStartMethod);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStartMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeDrStop(void) {
|
void BridgeDrStop(void) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStopMethod);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStopMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,10 +130,6 @@ void BridgeDrCleanup(void) {
|
|||||||
|
|
||||||
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
|
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrCleanupMethod);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrCleanupMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +137,6 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
|
|||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increase the size of our frame data buffer if our frame won't fit
|
// Increase the size of our frame data buffer if our frame won't fit
|
||||||
if ((*env)->GetArrayLength(env, DecodedFrameBuffer) < decodeUnit->fullLength) {
|
if ((*env)->GetArrayLength(env, DecodedFrameBuffer) < decodeUnit->fullLength) {
|
||||||
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
|
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
|
||||||
@@ -176,8 +157,10 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
|
|||||||
|
|
||||||
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
||||||
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
|
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
|
||||||
decodeUnit->frameNumber, decodeUnit->receiveTimeMs);
|
decodeUnit->frameNumber, (jlong)decodeUnit->receiveTimeMs);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
// We will crash here
|
||||||
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
return DR_OK;
|
return DR_OK;
|
||||||
}
|
}
|
||||||
else if (ret != DR_OK) {
|
else if (ret != DR_OK) {
|
||||||
@@ -192,22 +175,27 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
|
|||||||
currentEntry = currentEntry->next;
|
currentEntry = currentEntry->next;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
||||||
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
|
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
|
||||||
decodeUnit->frameNumber,
|
decodeUnit->frameNumber,
|
||||||
decodeUnit->receiveTimeMs);
|
(jlong)decodeUnit->receiveTimeMs);
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
// We will crash here
|
||||||
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
|
return DR_OK;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags) {
|
int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
|
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration, opusConfig->sampleRate, opusConfig->samplesPerFrame);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
return -1;
|
// This is called on a Java thread, so it's safe to return
|
||||||
}
|
|
||||||
|
|
||||||
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration);
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
err = -1;
|
err = -1;
|
||||||
}
|
}
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
@@ -233,20 +221,12 @@ int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusCon
|
|||||||
void BridgeArStart(void) {
|
void BridgeArStart(void) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStartMethod);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStartMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeArStop(void) {
|
void BridgeArStop(void) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStopMethod);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStopMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,21 +237,13 @@ void BridgeArCleanup() {
|
|||||||
|
|
||||||
(*env)->DeleteGlobalRef(env, DecodedAudioBuffer);
|
(*env)->DeleteGlobalRef(env, DecodedAudioBuffer);
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArCleanupMethod);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArCleanupMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
|
void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
jshort* decodedData = (*env)->GetPrimitiveArrayCritical(env, DecodedAudioBuffer, NULL);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jshort* decodedData = (*env)->GetShortArrayElements(env, DecodedAudioBuffer, 0);
|
|
||||||
|
|
||||||
int decodeLen = opus_multistream_decode(Decoder,
|
int decodeLen = opus_multistream_decode(Decoder,
|
||||||
(const unsigned char*)sampleData,
|
(const unsigned char*)sampleData,
|
||||||
@@ -280,85 +252,77 @@ void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
|
|||||||
OpusConfig.samplesPerFrame,
|
OpusConfig.samplesPerFrame,
|
||||||
0);
|
0);
|
||||||
if (decodeLen > 0) {
|
if (decodeLen > 0) {
|
||||||
// We must release the array elements first to ensure the data is copied before the callback
|
// We must release the array elements before making further JNI calls
|
||||||
(*env)->ReleaseShortArrayElements(env, DecodedAudioBuffer, decodedData, 0);
|
(*env)->ReleasePrimitiveArrayCritical(env, DecodedAudioBuffer, decodedData, 0);
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArPlaySampleMethod, DecodedAudioBuffer);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArPlaySampleMethod, DecodedAudioBuffer);
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
// We will crash here
|
||||||
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// We can abort here to avoid the copy back since no data was modified
|
// We can abort here to avoid the copy back since no data was modified
|
||||||
(*env)->ReleaseShortArrayElements(env, DecodedAudioBuffer, decodedData, JNI_ABORT);
|
(*env)->ReleasePrimitiveArrayCritical(env, DecodedAudioBuffer, decodedData, JNI_ABORT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeClStageStarting(int stage) {
|
void BridgeClStageStarting(int stage) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageStartingMethod, stage);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageStartingMethod, stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeClStageComplete(int stage) {
|
void BridgeClStageComplete(int stage) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageCompleteMethod, stage);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageCompleteMethod, stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeClStageFailed(int stage, long errorCode) {
|
void BridgeClStageFailed(int stage, int errorCode) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageFailedMethod, stage, errorCode);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageFailedMethod, stage, errorCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeClConnectionStarted(void) {
|
void BridgeClConnectionStarted(void) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStartedMethod);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStartedMethod, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeClConnectionTerminated(long errorCode) {
|
void BridgeClConnectionTerminated(int errorCode) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionTerminatedMethod, errorCode);
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionTerminatedMethod, errorCode);
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
// We will crash here
|
||||||
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) {
|
void BridgeClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
// The seemingly redundant short casts are required in order to convert the unsigned short to a signed short.
|
||||||
|
// If we leave it as an unsigned short, CheckJNI will fail when the value exceeds 32767. The cast itself is
|
||||||
|
// fine because the Java code treats the value as unsigned even though it's stored in a signed type.
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClRumbleMethod, controllerNumber, (short)lowFreqMotor, (short)highFreqMotor);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
return;
|
// We will crash here
|
||||||
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
}
|
}
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClRumbleMethod, controllerNumber, lowFreqMotor, highFreqMotor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeClConnectionStatusUpdate(int connectionStatus) {
|
void BridgeClConnectionStatusUpdate(int connectionStatus) {
|
||||||
JNIEnv* env = GetThreadEnv();
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStatusUpdateMethod, connectionStatus);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
// We will crash here
|
||||||
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStatusUpdateMethod, connectionStatus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BridgeClLogMessage(const char* format, ...) {
|
void BridgeClLogMessage(const char* format, ...) {
|
||||||
@@ -382,6 +346,7 @@ static AUDIO_RENDERER_CALLBACKS BridgeAudioRendererCallbacks = {
|
|||||||
.stop = BridgeArStop,
|
.stop = BridgeArStop,
|
||||||
.cleanup = BridgeArCleanup,
|
.cleanup = BridgeArCleanup,
|
||||||
.decodeAndPlaySample = BridgeArDecodeAndPlaySample,
|
.decodeAndPlaySample = BridgeArDecodeAndPlaySample,
|
||||||
|
.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION
|
||||||
};
|
};
|
||||||
|
|
||||||
static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
|
static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
|
||||||
|
|||||||
Submodule app/src/main/jni/moonlight-core/moonlight-common-c updated: 438b4f87d3...3aa2463856
@@ -10,6 +10,12 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendMouseMove(JNIEnv *env, jclass cla
|
|||||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_limelight_nvstream_jni_MoonBridge_sendMousePosition(JNIEnv *env, jclass clazz,
|
||||||
|
jshort x, jshort y, jshort referenceWidth, jshort referenceHeight) {
|
||||||
|
LiSendMousePositionEvent(x, y, referenceWidth, referenceHeight);
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_limelight_nvstream_jni_MoonBridge_sendMouseButton(JNIEnv *env, jclass clazz, jbyte buttonEvent, jbyte mouseButton) {
|
Java_com_limelight_nvstream_jni_MoonBridge_sendMouseButton(JNIEnv *env, jclass clazz, jbyte buttonEvent, jbyte mouseButton) {
|
||||||
LiSendMouseButtonEvent(buttonEvent, mouseButton);
|
LiSendMouseButtonEvent(buttonEvent, mouseButton);
|
||||||
@@ -43,6 +49,11 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendMouseScroll(JNIEnv *env, jclass c
|
|||||||
LiSendScrollEvent(scrollClicks);
|
LiSendScrollEvent(scrollClicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_limelight_nvstream_jni_MoonBridge_sendMouseHighResScroll(JNIEnv *env, jclass clazz, jshort scrollAmount) {
|
||||||
|
LiSendHighResScrollEvent(scrollAmount);
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_limelight_nvstream_jni_MoonBridge_stopConnection(JNIEnv *env, jclass clazz) {
|
Java_com_limelight_nvstream_jni_MoonBridge_stopConnection(JNIEnv *env, jclass clazz) {
|
||||||
LiStopConnection();
|
LiStopConnection();
|
||||||
@@ -83,11 +94,44 @@ Java_com_limelight_nvstream_jni_MoonBridge_findExternalAddressIP4(JNIEnv *env, j
|
|||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_com_limelight_nvstream_jni_MoonBridge_getPendingAudioFrames(JNIEnv *env, jclass clazz) {
|
Java_com_limelight_nvstream_jni_MoonBridge_getPendingAudioDuration(JNIEnv *env, jclass clazz) {
|
||||||
return LiGetPendingAudioFrames();
|
return LiGetPendingAudioDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_com_limelight_nvstream_jni_MoonBridge_getPendingVideoFrames(JNIEnv *env, jclass clazz) {
|
Java_com_limelight_nvstream_jni_MoonBridge_getPendingVideoFrames(JNIEnv *env, jclass clazz) {
|
||||||
return LiGetPendingVideoFrames();
|
return LiGetPendingVideoFrames();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_nvstream_jni_MoonBridge_testClientConnectivity(JNIEnv *env, jclass clazz, jstring testServerHostName, jint referencePort, jint testFlags) {
|
||||||
|
int ret;
|
||||||
|
const char* testServerHostNameStr = (*env)->GetStringUTFChars(env, testServerHostName, NULL);
|
||||||
|
|
||||||
|
ret = LiTestClientConnectivity(testServerHostNameStr, (unsigned short)referencePort, testFlags);
|
||||||
|
|
||||||
|
(*env)->ReleaseStringUTFChars(env, testServerHostName, testServerHostNameStr);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_nvstream_jni_MoonBridge_getPortFlagsFromStage(JNIEnv *env, jclass clazz, jint stage) {
|
||||||
|
return LiGetPortFlagsFromStage(stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_nvstream_jni_MoonBridge_getPortFlagsFromTerminationErrorCode(JNIEnv *env, jclass clazz, jint errorCode) {
|
||||||
|
return LiGetPortFlagsFromTerminationErrorCode(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_com_limelight_nvstream_jni_MoonBridge_stringifyPortFlags(JNIEnv *env, jclass clazz, jint portFlags, jstring separator) {
|
||||||
|
const char* separatorStr = (*env)->GetStringUTFChars(env, separator, NULL);
|
||||||
|
char outputBuffer[512];
|
||||||
|
|
||||||
|
LiStringifyPortFlags(portFlags, separatorStr, outputBuffer, sizeof(outputBuffer));
|
||||||
|
|
||||||
|
(*env)->ReleaseStringUTFChars(env, separator, separatorStr);
|
||||||
|
return (*env)->NewStringUTF(env, outputBuffer);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:interpolator="@android:anim/accelerate_interpolator"
|
||||||
|
android:duration="100"/>
|
||||||
|
</set>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:interpolator="@android:anim/accelerate_interpolator"
|
||||||
|
android:duration="100"/>
|
||||||
|
</set>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,49 +1,29 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="150dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="200dp"
|
||||||
android:padding="20dp">
|
android:padding="10dp">
|
||||||
<RelativeLayout
|
<ImageView
|
||||||
android:id="@+id/grid_image_layout"
|
android:id="@+id/grid_image"
|
||||||
|
android:cropToPadding="false"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
</ImageView>
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/grid_overlay"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_width="wrap_content"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_width="50dp"
|
||||||
<ProgressBar
|
android:layout_height="50dp">
|
||||||
android:id="@+id/grid_spinner"
|
</ImageView>
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_width="75dp"
|
|
||||||
android:layout_height="75dp"
|
|
||||||
android:indeterminate="true">
|
|
||||||
</ProgressBar>
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/grid_image"
|
|
||||||
android:cropToPadding="false"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_width="150dp"
|
|
||||||
android:layout_height="175dp">
|
|
||||||
</ImageView>
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/grid_overlay"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_width="50dp"
|
|
||||||
android:layout_height="50dp">
|
|
||||||
</ImageView>
|
|
||||||
</RelativeLayout>
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/grid_text"
|
android:id="@+id/grid_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/grid_image_layout"
|
android:layout_below="@id/grid_overlay"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_margin="5dp"
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:singleLine="true"
|
android:textSize="18sp">
|
||||||
android:ellipsize="marquee"
|
|
||||||
android:marqueeRepeatLimit="marquee_forever"
|
|
||||||
android:scrollHorizontally="true"
|
|
||||||
android:textSize="18sp" >
|
|
||||||
</TextView>
|
</TextView>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@@ -1,49 +1,29 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="100dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="133dp"
|
||||||
android:padding="10dp">
|
android:padding="5dp">
|
||||||
<RelativeLayout
|
<ImageView
|
||||||
android:id="@+id/grid_image_layout"
|
android:id="@+id/grid_image"
|
||||||
|
android:cropToPadding="false"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
</ImageView>
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/grid_overlay"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_width="wrap_content"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_width="33dp"
|
||||||
<ProgressBar
|
android:layout_height="33dp">
|
||||||
android:id="@+id/grid_spinner"
|
</ImageView>
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_width="50dp"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:indeterminate="true">
|
|
||||||
</ProgressBar>
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/grid_image"
|
|
||||||
android:cropToPadding="false"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_width="100dp"
|
|
||||||
android:layout_height="117dp">
|
|
||||||
</ImageView>
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/grid_overlay"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_width="33dp"
|
|
||||||
android:layout_height="33dp">
|
|
||||||
</ImageView>
|
|
||||||
</RelativeLayout>
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/grid_text"
|
android:id="@+id/grid_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/grid_image_layout"
|
android:layout_below="@id/grid_overlay"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_margin="5dp"
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:singleLine="true"
|
android:textSize="14sp">
|
||||||
android:ellipsize="marquee"
|
|
||||||
android:marqueeRepeatLimit="marquee_forever"
|
|
||||||
android:scrollHorizontally="true"
|
|
||||||
android:textSize="14sp" >
|
|
||||||
</TextView>
|
</TextView>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="fill_parent"
|
android:layout_height="fill_parent"
|
||||||
android:numColumns="auto_fit"
|
android:numColumns="auto_fit"
|
||||||
android:columnWidth="160dp"
|
android:columnWidth="150dp"
|
||||||
android:stretchMode="spacingWidthUniform"
|
android:stretchMode="spacingWidthUniform"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="fill_parent"
|
android:layout_height="fill_parent"
|
||||||
android:numColumns="auto_fit"
|
android:numColumns="auto_fit"
|
||||||
android:columnWidth="105dp"
|
android:columnWidth="100dp"
|
||||||
android:stretchMode="spacingWidthUniform"
|
android:stretchMode="spacingWidthUniform"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/fragmentView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fastScrollEnabled="true"
|
|
||||||
android:longClickable="false"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:stackFromBottom="false"
|
|
||||||
android:clipToPadding="false"/>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="15dp">
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/grid_image_layout"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/grid_image"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_width="75dp"
|
|
||||||
android:layout_height="75dp">
|
|
||||||
</ImageView>
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/grid_overlay"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_width="33dp"
|
|
||||||
android:layout_height="33dp">
|
|
||||||
</ImageView>
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/grid_spinner"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_width="33dp"
|
|
||||||
android:layout_height="33dp"
|
|
||||||
android:indeterminate="true">
|
|
||||||
</ProgressBar>
|
|
||||||
</RelativeLayout>
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/grid_text"
|
|
||||||
android:layout_width="75dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/grid_image_layout"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textSize="14sp" >
|
|
||||||
</TextView>
|
|
||||||
</RelativeLayout>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/fragmentView"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="fill_parent"
|
|
||||||
android:numColumns="auto_fit"
|
|
||||||
android:columnWidth="105dp"
|
|
||||||
android:stretchMode="spacingWidthUniform"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:nextFocusLeft="@id/settingsButton"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:gravity="center"/>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:padding="10dp"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/grid_text"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textIsSelectable="false"
|
|
||||||
android:textSize="16sp" >
|
|
||||||
</TextView>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="decoder_names">
|
||||||
|
<item>Decoder automatisch auswählen</item>
|
||||||
|
<item>Software Decodierung erzwingen</item>
|
||||||
|
<item>Hardware Decodierung erzwingen</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="video_format_names">
|
||||||
|
<item>Verwende H.265 so fern stabile Unterstützung vorhanden ist</item>
|
||||||
|
<item>Immer H.265 verwenden (könnte Crashes verursachen)</item>
|
||||||
|
<item>Nie H.265 verwenden</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_label" translatable="false">Moonlight</string>
|
||||||
|
<string name="app_label_root" translatable="false">Moonlight (Root)</string>
|
||||||
|
|
||||||
|
<!-- Shortcut strings -->
|
||||||
|
<string name="scut_deleted_pc">PC gelöscht</string>
|
||||||
|
<string name="scut_not_paired">PC nicht verbunden</string>
|
||||||
|
<string name="scut_pc_not_found">PC nicht gefunden</string>
|
||||||
|
<string name="scut_invalid_uuid">Angegebener PC ist unzulässig</string>
|
||||||
|
<string name="scut_invalid_app_id">Angegebene App ist unzulässig</string>
|
||||||
|
|
||||||
|
<!-- Help strings -->
|
||||||
|
<string name="help_loading_title">Hilfsanzeige</string>
|
||||||
|
<string name="help_loading_msg">Lade Hilfesanzeige…</string>
|
||||||
|
|
||||||
|
<!-- PC view menu entries -->
|
||||||
|
<string name="pcview_menu_app_list">Spiele anzeigen</string>
|
||||||
|
<string name="pcview_menu_pair_pc">Mit PC verbinden</string>
|
||||||
|
<string name="pcview_menu_unpair_pc">Verbindung beenden</string>
|
||||||
|
<string name="pcview_menu_send_wol">Wake-On-LAN Anfrage senden</string>
|
||||||
|
<string name="pcview_menu_delete_pc">PC löschen</string>
|
||||||
|
<string name="pcview_menu_test_network">Netzwerkverbindung testen</string>
|
||||||
|
<string name="pcview_menu_details">Details anzeigen</string>
|
||||||
|
|
||||||
|
<!-- Network test strings -->
|
||||||
|
<string name="nettest_title_waiting">Netzwerkverbindung wird getest</string>
|
||||||
|
<string name="nettest_text_waiting">Moonlight testet Ihre Netzwerkverbindung, um festzustellen, ob NVIDIA GameStream blockiert ist. Dies kann einige Sekunden dauern...</string>
|
||||||
|
<string name="nettest_title_done">Netzwerktest abgeschlossen</string>
|
||||||
|
<string name="nettest_text_success">Ihr Netzwerk scheint das Mondlicht nicht zu blockieren. Wenn Sie weiterhin Probleme mit der Verbindung haben, überprüfen Sie die Firewall-Einstellungen Ihres PC\n\n\n Wenn Sie versuchen, über das Internet zu streamen, installieren Sie das Moonlight Internet Hosting Tool auf Ihrem PC und führen Sie den mitgelieferten Internet-Streaming-Tester aus, um die Internetverbindung Ihres PCs zu überprüfen.</string>
|
||||||
|
<string name="nettest_text_inconclusive">Der Netzwerktest konnte nicht durchgeführt werden, da keiner der Verbindungstestserver von Moonlight erreichbar war. Überprüfen Sie Ihre Internetverbindung oder versuchen Sie es später noch einmal.</string>
|
||||||
|
<string name="nettest_text_failure">Die aktuelle Netzwerkverbindung Ihres Geräts scheint das Mondlicht zu blockieren. Das Streaming über das Internet funktioniert möglicherweise nicht, während Sie mit diesem Netzwerk verbunden sind.\n\n Folgende Netzwerkports wurden blockiert:\n</string>
|
||||||
|
<string name="nettest_text_blocked">Die aktuelle Netzwerkverbindung Ihres Geräts blockiert Moonlight. Das Streaming über das Internet funktioniert möglicherweise nicht, während Sie mit diesem Netzwerk verbunden sind.</string>
|
||||||
|
|
||||||
|
<!-- Pair messages -->
|
||||||
|
<string name="pairing">Verbinden…</string>
|
||||||
|
<string name="pair_pc_offline">Computer ist offline</string>
|
||||||
|
<string name="pair_pc_ingame">Am Computer ist bereits ein Spiel aktiv. Sie müssen das Spiel beenden bevor sie sich koppeln können.</string>
|
||||||
|
<string name="pair_pairing_title">Verbinden</string>
|
||||||
|
<string name="pair_pairing_msg">Bitte geben sie folgenden PIN auf ihrem PC ein:</string>
|
||||||
|
<string name="pair_incorrect_pin">PIN inkorrekt</string>
|
||||||
|
<string name="pair_fail">Verbindung fehlgeschlagen</string>
|
||||||
|
<string name="pair_already_in_progress">Verbindungsaufbau bereits im Gange</string>
|
||||||
|
|
||||||
|
<!-- WOL messages -->
|
||||||
|
<string name="wol_pc_online">Computer ist online</string>
|
||||||
|
<string name="wol_no_mac">PC konnte nicht geweckt werden, da GFE keine MAC-Adresse gesendet hat.</string>
|
||||||
|
<string name="wol_waking_pc">PC wird geweckt…</string>
|
||||||
|
<string name="wol_waking_msg">Es kann einige Momente dauern ihren PC zu wecken.
|
||||||
|
Sollte dies fehlschlagen stellen sie bitte sicher, dass Wake-On-LAN korrekt konfiguriert ist.
|
||||||
|
</string>
|
||||||
|
<string name="wol_fail">Senden der Wake-On-LAN Pakete ist fehlgeschlagen.</string>
|
||||||
|
|
||||||
|
<!-- Unpair messages -->
|
||||||
|
<string name="unpairing">Trenne Verbindung…</string>
|
||||||
|
<string name="unpair_success">Verbindung erfolgreich getrennt</string>
|
||||||
|
<string name="unpair_fail">Trennung der Verbindung fehlgeschlagen</string>
|
||||||
|
<string name="unpair_error">Gerät wurde nicht verbunden</string>
|
||||||
|
|
||||||
|
<!-- Errors -->
|
||||||
|
<string name="error_pc_offline">Computer ist offline</string>
|
||||||
|
<string name="error_manager_not_running">Der ComputerManager Service läuft nicht. Bitte warten sie ein paar Sekunden oder starten sie Die App neu.</string>
|
||||||
|
<string name="error_unknown_host">Host konnte nicht aufgelößt werden.</string>
|
||||||
|
<string name="error_404">GFE ist auf einen HTTP 404 Fehler gestoßen. Stellen sie sicher, dass die GPU ihres PCs unterstützt wird.
|
||||||
|
Die Verwendung von Remote-Desktop Software kann ebenso diesen Fehler verursachen. Starten sie ihren Computer neu oder reinstallieren sie GFE.
|
||||||
|
</string>
|
||||||
|
<string name="title_decoding_error">Video Decoder ist gecrashed</string>
|
||||||
|
<string name="message_decoding_error">Moonlight ist wegen einer Inkompatibilität zu dem Video-Decoder ihres Gerätes gecrasht. Stellen sie sicher, dass GeForce Experience Version auf ihrem PC auf dem neuesten Stand ist. Sollten weiterin Crashes auftreten, versuchen sie ihre Stream Einstellungen zu justieren.</string>
|
||||||
|
<string name="title_decoding_reset">Video Einstellungen zurücksetzen</string>
|
||||||
|
<string name="message_decoding_reset">Der Video-Decoder ihres Geräts ist wiederholt mit den ausgewählten Einstellungen gecrasht. Ihre Streamingeinstellungen wurden zurückgesetzt.</string>
|
||||||
|
<string name="error_usb_prohibited">USB Zugriff ist administrativ unterbunden. Bitte überprüfen sie ihre Knox oder MDM Einstellungen.</string>
|
||||||
|
<string name="unable_to_pin_shortcut">Die zur Zeit aktive Launcher App ünterstützt das erstellen angehefteter Shortcuts nicht.</string>
|
||||||
|
<string name="video_decoder_init_failed">Der Videodecoder konnte nicht initialisiert werden. Ihr Gerät unterstützt möglicherweise die gewählte Auflösung oder Bildfrequenz nicht.</string>
|
||||||
|
<string name="no_video_received_error">Kein Video vom Gastgeber empfangen. Überprüfen Sie die Firewall und Portweiterleitungsregeln des Host-PCs.</string>
|
||||||
|
<string name="no_frame_received_error">Ihre Netzwerkverbindung funktioniert nicht gut. Reduzieren Sie die Einstellung der Videobitrate oder versuchen Sie eine schnellere Verbindung.</string>
|
||||||
|
|
||||||
|
<!-- Start application messages -->
|
||||||
|
<string name="conn_establishing_title">Verbindung herstellen</string>
|
||||||
|
<string name="conn_establishing_msg">Verbindung starten</string>
|
||||||
|
<string name="conn_metered">Warnung: Das Datentransfervolumen ihrer Netzwerkverbindung ist limitiert!</string>
|
||||||
|
<string name="conn_client_latency">Durchschnittliche Frame-Dekodierungslatenz:</string>
|
||||||
|
<string name="conn_client_latency_hw">Hardware-Dekodierungslatenz:</string>
|
||||||
|
<string name="conn_hardware_latency">Durchschnittliche Hardware-Dekodierungslatenz:</string>
|
||||||
|
<string name="conn_starting">Startet</string>
|
||||||
|
<string name="conn_error_title">Verbindungsfehler</string>
|
||||||
|
<string name="conn_error_msg">Start fehlgeschlagen</string>
|
||||||
|
<string name="conn_terminated_title">Verbindung beendet</string>
|
||||||
|
<string name="conn_terminated_msg">Die Verbindung wurde beendet</string>
|
||||||
|
|
||||||
|
<!-- General strings -->
|
||||||
|
<string name="ip_hint">IP-Adresse des GeForce PCs</string>
|
||||||
|
<string name="searching_pc">Suche nach PCs wo GeForce Experience aktiv ist…\n\n
|
||||||
|
Stellen Sie sicher, dass GameStream in den GeForce Experience SHIELD-Einstellungen aktiviert ist.</string>
|
||||||
|
<string name="yes">Ja</string>
|
||||||
|
<string name="no">Nein</string>
|
||||||
|
<string name="lost_connection">Verbindung zum PC verloren</string>
|
||||||
|
<string name="title_details">Details</string>
|
||||||
|
<string name="help">Hilfe</string>
|
||||||
|
<string name="delete_pc_msg">Sind sie sicher, dass sie diesen PC löschen möchten?</string>
|
||||||
|
<string name="slow_connection_msg">Langsame Verbindung zum PC\nReduzieren sie die Bitrate</string>
|
||||||
|
<string name="poor_connection_msg">Sehr langsame Verbindung zum PC</string>
|
||||||
|
<string name="perf_overlay_text">Videodimensionen: %1$s\nDecoder: %2$s\nGeschätzte PC Bildwiederholrate: %3$.2f FPS\nBildwiederholrate der Netzwerkübertragung: %4$.2f FPS\nWiedergabe-Bildwiederholungsrate: %5$.2f FPS\nWegen Netzwerkübertraung ausgelassene Frames: %6$.2f%%\nDurchschnittliche Übertragungsdauer: %7$.2f ms\nDurchschnittliche decoding dauer: %8$.2f ms</string>
|
||||||
|
|
||||||
|
<!-- AppList activity -->
|
||||||
|
<string name="applist_connect_msg">Verbinde mit PC…</string>
|
||||||
|
<string name="applist_menu_resume">Sitzung Wiederherstellen</string>
|
||||||
|
<string name="applist_menu_quit">Sitzung Beenden</string>
|
||||||
|
<string name="applist_menu_quit_and_start">Aktuelles Spiel beenden und Starte</string>
|
||||||
|
<string name="applist_menu_cancel">Abbrechen</string>
|
||||||
|
<string name="applist_menu_details">Details anzeigen</string>
|
||||||
|
<string name="applist_menu_scut">Shortcut erstellen</string>
|
||||||
|
<string name="applist_menu_tv_channel">Zu Kanal hinzufügen</string>
|
||||||
|
<string name="applist_menu_hide_app">App ausblenden</string>
|
||||||
|
<string name="applist_refresh_title">App Liste</string>
|
||||||
|
<string name="applist_refresh_msg">Aktualisiere Apps…</string>
|
||||||
|
<string name="applist_refresh_error_title">Fehler</string>
|
||||||
|
<string name="applist_refresh_error_msg">Abfrage der App-Liste fehlgeschlagen</string>
|
||||||
|
<string name="applist_quit_app">Beenden</string>
|
||||||
|
<string name="applist_quit_success">Erfolgreich beendet</string>
|
||||||
|
<string name="applist_quit_fail">Beenden fehlgeschlagen</string>
|
||||||
|
<string name="applist_quit_confirmation">Sind sie sicher, dass die die laufende App schließen möchten? Ungespeicherte Daten gehen verloren.</string>
|
||||||
|
<string name="applist_details_id">App ID:</string>
|
||||||
|
|
||||||
|
<!-- Add computer manually activity -->
|
||||||
|
<string name="title_add_pc">PC manuell hinzufügen</string>
|
||||||
|
<string name="msg_add_pc">Verbinde zum PC…</string>
|
||||||
|
<string name="addpc_fail">Verbindung fehlgeschlagen. Stellen sie sicher, dass die benötigten Ports von einer Firewall gefiltert werden.</string>
|
||||||
|
<string name="addpc_success">Computer erfolgreich hinzugefügt</string>
|
||||||
|
<string name="addpc_unknown_host">Auflößen der PC-Adresse fehlgeschlagen. Stellen sie sicher, dass die Adresse keine Tippfehler beinhaltet.</string>
|
||||||
|
<string name="addpc_enter_ip">Sie müssen eine IP-Addresse eingeben.</string>
|
||||||
|
<string name="addpc_wrong_sitelocal">Diese Adresse sieht falsch aus. Sie müssen die öffentliche IP-Adresse ihreres Routers benutzen um über das Internet zu streamen.</string>
|
||||||
|
|
||||||
|
<!-- Preferences -->
|
||||||
|
<string name="category_basic_settings">Allgemeine Einstellungen</string>
|
||||||
|
<string name="title_resolution_list">Videoauflösung</string>
|
||||||
|
<string name="summary_resolution_list">Ehöhen für klarere Bilder. Reduzieren für flüssigere Darstellung auf langsameren Geräten und Netzwerken.</string>
|
||||||
|
<string name="title_fps_list">Bildwiederholungsrate</string>
|
||||||
|
<string name="summary_fps_list">Erhöhen für einen gleichmäßigeren Video-Stream. Verringern um auf langsameren Geräten eine bessere Performance zu erzielen.</string>
|
||||||
|
<string name="title_seekbar_bitrate">Video Bitrate</string>
|
||||||
|
<string name="summary_seekbar_bitrate">Erhöhen für einen schärferen Video-Stream. Verringern um auf langsameren Geräten eine bessere Performance zu erzielen.</string>
|
||||||
|
<string name="suffix_seekbar_bitrate_mbps">Mbps</string>
|
||||||
|
<string name="title_checkbox_stretch_video">Video auf Vollbildschirm strecken</string>
|
||||||
|
|
||||||
|
<string name="category_audio_settings">Audio Einstellungen</string>
|
||||||
|
<string name="title_audio_config_list">Surround Sound Konfiguration </string>
|
||||||
|
<string name="summary_audio_config_list">Aktivieren von 5.1- oder 7.1-Surround-Sound für Heimkinosysteme</string>
|
||||||
|
|
||||||
|
<string name="category_input_settings">Eingabe Einstellungen</string>
|
||||||
|
<string name="title_checkbox_touchscreen_trackpad">Verwenden des Touchscreen als Trackpad</string>
|
||||||
|
<string name="summary_checkbox_touchscreen_trackpad">Wenn aktiviert, verhält sich der Touchscreen wie ein Trackpad. Wenn er deaktiviert ist, steuert der Touchscreen den Mauszeiger direkt.</string>
|
||||||
|
<string name="title_checkbox_multi_controller">Automatische GamePad-Erkennung</string>
|
||||||
|
<string name="summary_checkbox_multi_controller">Abwählen dieser Option erzwingt dass immer ein GamePad präsent ist</string>
|
||||||
|
<string name="title_checkbox_vibrate_fallback">Vibrationsemulation aktivieren</string>
|
||||||
|
<string name="summary_checkbox_vibrate_fallback">Lässt das Gerät vibrieren falls das GamePad keine Vibration unterstütz</string>
|
||||||
|
<string name="title_seekbar_deadzone">Deadzone des Analogsticks</string>
|
||||||
|
<string name="suffix_seekbar_deadzone">%</string>
|
||||||
|
<string name="title_checkbox_xb1_driver">Xbox 360/One GamePad Treiber</string>
|
||||||
|
<string name="summary_checkbox_xb1_driver">Aktiviert eingebauten USB Treiber für Geräte die keinen Xbox GamePad-Unterstützung haben</string>
|
||||||
|
<string name="title_checkbox_usb_bind_all">Android GamePad Unterstütung überlagern</string>
|
||||||
|
<string name="summary_checkbox_usb_bind_all">Erzwingt die Verwendung von Moonlights USB Treiber für alle Xbox kompatiblen GamePads</string>
|
||||||
|
<string name="title_checkbox_mouse_emulation">Maus Emulation via GamePad</string>
|
||||||
|
<string name="summary_checkbox_mouse_emulation">Langes gedrückt halten der Start-Taste wechselt in den Maus Modus</string>
|
||||||
|
<string name="title_checkbox_mouse_nav_buttons">Vor- und Zurück-Tasten aktivieren</string>
|
||||||
|
<string name="summary_checkbox_mouse_nav_buttons">Aktivierung dieser Option kann auf fehleranfälligen Geräten Rechts-Clicks verunmöglichen</string>
|
||||||
|
<string name="title_checkbox_flip_face_buttons">Buttons umkehren</string>
|
||||||
|
<string name="summary_checkbox_flip_face_buttons">Kehrt die Buttons A/B und X/Y für Gamepads und die Bildschirmsteuerung um</string>
|
||||||
|
|
||||||
|
<string name="category_on_screen_controls_settings">On-Screen Steuerungseinstellungen</string>
|
||||||
|
<string name="title_checkbox_show_onscreen_controls">Zeige On-Screen Steuerung</string>
|
||||||
|
<string name="summary_checkbox_show_onscreen_controls">Zeige virtuelle Steuerelemente als Overlay auf dem Touchscreen</string>
|
||||||
|
<string name="title_checkbox_vibrate_osc">Vibrationen aktivieren</string>
|
||||||
|
<string name="summary_checkbox_vibrate_osc">Vibrieren als haptisches Feedback für die On-Screen Steuerelemente</string>
|
||||||
|
<string name="title_only_l3r3">Nur L3 und R3 anzeigen</string>
|
||||||
|
<string name="summary_only_l3r3">Alle virtuellen Steuerelemente außer L3 und R3 verbregen.</string>
|
||||||
|
<string name="title_reset_osc">Gespeicherte On-Screen Steuerungelemente zurücksetzen</string>
|
||||||
|
<string name="summary_reset_osc">Alle On-Screen Steuerelemente auf ihre Vorgabegröße und Position zurück setzen</string>
|
||||||
|
<string name="dialog_title_reset_osc">Layout Zurücksetzen</string>
|
||||||
|
<string name="dialog_text_reset_osc">Sind sie sicher, dass sie das gepseicherte on-screen Layout der Steuerelemte löschen wollen?</string>
|
||||||
|
<string name="toast_reset_osc_success">On-Screen Steuerelemente wurden zurückgesetzt</string>
|
||||||
|
<string name="title_osc_opacity">Ändern der Transparenz von Bildschirmsteuerelementen</string>
|
||||||
|
<string name="summary_osc_opacity">Steuerelemente auf dem Bildschirm mehr/weniger transparent machen</string>
|
||||||
|
<string name="suffix_osc_opacity">%</string>
|
||||||
|
<string name="dialog_title_osc_opacity">Transparenz</string>
|
||||||
|
|
||||||
|
<string name="category_ui_settings">UI Einstellungen</string>
|
||||||
|
<string name="title_checkbox_enable_pip">Aktivieren des Bild-in-Bild-Beobachtungsmodus</string>
|
||||||
|
<string name="summary_checkbox_enable_pip">Ermöglicht die Anzeige (aber nicht die Steuerung) des Streams während des Multitasking</string>
|
||||||
|
<string name="title_language_list">Sprache</string>
|
||||||
|
<string name="summary_language_list">Sprache die Moonlight verwenden soll</string>
|
||||||
|
<string name="title_checkbox_small_icon_mode">Verwende kleine Icons</string>
|
||||||
|
<string name="summary_checkbox_small_icon_mode">Verwende kleine Icons in der Rasteransicht, um mehr gleichzeitig anzeigen zu können</string>
|
||||||
|
|
||||||
|
<string name="category_host_settings">Host Einstellungen</string>
|
||||||
|
<string name="title_checkbox_enable_sops">Spieleinstellungen optimieren</string>
|
||||||
|
<string name="summary_checkbox_enable_sops">GFE erlauben die die Spieleinstellungen zu optimieren.</string>
|
||||||
|
<string name="title_checkbox_host_audio">Audio auf dem PC wiedergeben</string>
|
||||||
|
<string name="summary_checkbox_host_audio">Audio auf dem PC und auf diesem Gerät wiedergeben</string>
|
||||||
|
|
||||||
|
<string name="category_advanced_settings">Erweiterte Einstellungen</string>
|
||||||
|
<string name="title_disable_frame_drop">Nie Frames Überspringen</string>
|
||||||
|
<string name="title_unlock_fps">Alle möglichen Bildfrequenzen freischalten</string>
|
||||||
|
<string name="summary_unlock_fps">Streaming mit 90 oder 120 FPS kann die Latenzzeit auf High-End-Geräten verringern, kann aber bei Geräten, die dies nicht unterstützen, zu Verzögerungen oder Instabilität führen.</string>
|
||||||
|
<string name="title_checkbox_disable_warnings">Warnhinweise deaktivieren</string>
|
||||||
|
<string name="summary_checkbox_disable_warnings">On-Screen Warnmeldungen während des Streaming deaktivieren</string>
|
||||||
|
<string name="summary_disable_frame_drop">Kann potentiell das Mikro-Ruckeln auf einigen Geräten reduzieren, allerdings erhöt dies gleichzeitig die Latenz</string>
|
||||||
|
<string name="title_video_format">Ändere H.265 Einstellungen</string>
|
||||||
|
<string name="summary_video_format">H.265 verringert die Video-Bandbreitenanforderung, funktioniert allerdings nur auf sehr neuen Geräten</string>
|
||||||
|
<string name="title_enable_hdr">HDR aktivieren (experimentell)</string>
|
||||||
|
<string name="summary_enable_hdr">HDR-Streaming sofern dies von der PC GPU unterstützt wird. HDR erfordert eine GPU der GTX 1000 Serie oder neuer.</string>
|
||||||
|
<string name="title_enable_perf_overlay">Performance Overlay aktivieren</string>
|
||||||
|
<string name="summary_enable_perf_overlay">Leistungsmerkmale während des Streamens in Echtzeit einblenden.</string>
|
||||||
|
<string name="title_enable_post_stream_toast">Zeige Informationen über Latenz nach Streaming</string>
|
||||||
|
<string name="summary_enable_post_stream_toast">Anzeige einer Informationsmeldung über Latenz nach dem Ende des Streams</string>
|
||||||
|
</resources>
|
||||||
@@ -88,14 +88,11 @@
|
|||||||
<string name="summary_resolution_list">Establecer unos valores demasiado altos puede causar lag o cierres inesperados</string>
|
<string name="summary_resolution_list">Establecer unos valores demasiado altos puede causar lag o cierres inesperados</string>
|
||||||
<string name="title_seekbar_bitrate">Seleccionar bitrate de vídeo</string>
|
<string name="title_seekbar_bitrate">Seleccionar bitrate de vídeo</string>
|
||||||
<string name="summary_seekbar_bitrate">Usa bitrate bajo para reducir "parpadeo". Incrementa el bitrate para mayor calidad de imagen.</string>
|
<string name="summary_seekbar_bitrate">Usa bitrate bajo para reducir "parpadeo". Incrementa el bitrate para mayor calidad de imagen.</string>
|
||||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
|
||||||
<string name="title_checkbox_stretch_video">Ajustar vídeo a pantalla completa</string>
|
<string name="title_checkbox_stretch_video">Ajustar vídeo a pantalla completa</string>
|
||||||
<string name="title_checkbox_disable_warnings">Desactivar mensajes de advertencia</string>
|
<string name="title_checkbox_disable_warnings">Desactivar mensajes de advertencia</string>
|
||||||
<string name="summary_checkbox_disable_warnings">Desactivar mensajes de advertencia en pantalla durante la transmisión</string>
|
<string name="summary_checkbox_disable_warnings">Desactivar mensajes de advertencia en pantalla durante la transmisión</string>
|
||||||
|
|
||||||
<string name="category_audio_settings">Configuración de audio</string>
|
<string name="category_audio_settings">Configuración de audio</string>
|
||||||
<string name="title_checkbox_51_surround">Activar sonido 5.1 surround</string>
|
|
||||||
<string name="summary_checkbox_51_surround">Desmarcar si experimentas problemas de audio. Requiere GFE 2.7 o superior.</string>
|
|
||||||
|
|
||||||
<string name="title_checkbox_multi_controller">Soporte para múltiples mandos</string>
|
<string name="title_checkbox_multi_controller">Soporte para múltiples mandos</string>
|
||||||
<string name="summary_checkbox_multi_controller">Si no está marcado, todos los mandos aparecen como uno solo</string>
|
<string name="summary_checkbox_multi_controller">Si no está marcado, todos los mandos aparecen como uno solo</string>
|
||||||
@@ -113,8 +110,6 @@
|
|||||||
<string name="category_ui_settings">Configuración de la interfaz</string>
|
<string name="category_ui_settings">Configuración de la interfaz</string>
|
||||||
<string name="title_language_list">Idioma</string>
|
<string name="title_language_list">Idioma</string>
|
||||||
<string name="summary_language_list">Idioma a usar por Moonlight</string>
|
<string name="summary_language_list">Idioma a usar por Moonlight</string>
|
||||||
<string name="title_checkbox_list_mode">Usar listas en lugar de cuadrículas</string>
|
|
||||||
<string name="summary_checkbox_list_mode">Mostrar aplicaciones y PCs en listas en lugar de el cuadrículas</string>
|
|
||||||
<string name="title_checkbox_small_icon_mode">Usar iconos pequeños</string>
|
<string name="title_checkbox_small_icon_mode">Usar iconos pequeños</string>
|
||||||
<string name="summary_checkbox_small_icon_mode">Usar iconos pequeños en las entradas de cuadrículas para permitir más entradas en la pantalla</string>
|
<string name="summary_checkbox_small_icon_mode">Usar iconos pequeños en las entradas de cuadrículas para permitir más entradas en la pantalla</string>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="fps_names">
|
||||||
|
<item>30 IPS</item>
|
||||||
|
<item>60 IPS</item>
|
||||||
|
<item>90 IPS</item>
|
||||||
|
<item>120 IPS</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="audio_config_names">
|
||||||
|
<item>Stéréo</item>
|
||||||
|
<item>Son surround 5.1</item>
|
||||||
|
<item>Son surround 7.1</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="decoder_names">
|
||||||
|
<item>Sélection automatique du décodeur</item>
|
||||||
|
<item>Contraindre le décodage logiciel</item>
|
||||||
|
<item>Contraindre le décodage matériel</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="video_format_names">
|
||||||
|
<item>Utiliser H.265 uniquement s\'il est stable</item>
|
||||||
|
<item>Utilisez toujours H.265 (mais il peut planter)</item>
|
||||||
|
<item>N\'utilisez jamais H.265</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
@@ -19,6 +19,15 @@
|
|||||||
<string name="pcview_menu_send_wol">Envoyer la requête Wake-On-LAN</string>
|
<string name="pcview_menu_send_wol">Envoyer la requête Wake-On-LAN</string>
|
||||||
<string name="pcview_menu_delete_pc">Supprimer PC</string>
|
<string name="pcview_menu_delete_pc">Supprimer PC</string>
|
||||||
<string name="pcview_menu_details">Voir les détails</string>
|
<string name="pcview_menu_details">Voir les détails</string>
|
||||||
|
|
||||||
|
<!-- Network test strings -->
|
||||||
|
<string name="nettest_title_waiting">Test de la connexion réseau</string>
|
||||||
|
<string name="nettest_text_waiting">Moonlight teste votre connexion réseau pour déterminer si NVIDIA GameStream est bloqué.\n\nCela peut prendre quelques secondes.</string>
|
||||||
|
<string name="nettest_title_done">Test du réseau terminé</string>
|
||||||
|
<string name="nettest_text_success">Votre réseau ne semble pas bloquer Moonlight. Si vous avez encore de la difficulté à vous connecter, vérifiez les paramètres du pare-feu de votre PC.\n\nSi vous essayez de diffuser sur Internet, installez l\'outil d\'hébergement Internet Moonlight sur votre PC et exécutez le testeur de streaming Internet inclus pour vérifier la connexion Internet de votre PC.</string>
|
||||||
|
<string name="nettest_text_inconclusive">Le test réseau n’a pas pu être effectué car aucun des serveurs de test de connexion Moonlight n’était accessible. Vérifiez votre connexion Internet ou réessayez plus tard.</string>
|
||||||
|
<string name="nettest_text_failure">La connexion réseau actuelle de votre appareil semble bloquer Moonlight. Le streaming sur Internet peut ne pas fonctionner lorsqu’il est connecté à ce réseau.\n\nLes ports réseau suivants ont été bloqués:\n</string>
|
||||||
|
<string name="nettest_text_blocked">La connexion réseau actuelle de votre appareil bloque Moonlight. Le streaming sur Internet peut ne pas fonctionner lorsqu’il est connecté à ce réseau.</string>
|
||||||
|
|
||||||
<!-- Pair messages -->
|
<!-- Pair messages -->
|
||||||
<string name="pairing">Appariement…</string>
|
<string name="pairing">Appariement…</string>
|
||||||
@@ -53,7 +62,7 @@
|
|||||||
L\'utilisation d\'un logiciel de bureau à distance peut également provoquer cette erreur. Essayez de redémarrer votre machine ou de réinstaller GFE.
|
L\'utilisation d\'un logiciel de bureau à distance peut également provoquer cette erreur. Essayez de redémarrer votre machine ou de réinstaller GFE.
|
||||||
</string>
|
</string>
|
||||||
<string name="title_decoding_error">Le décodeur vidéo s\'est écrasé</string>
|
<string name="title_decoding_error">Le décodeur vidéo s\'est écrasé</string>
|
||||||
<string name="message_decoding_error">Moonlight s\'est écrasé en raison d\'une incompatibilité avec le décodeur vidéo de cet appareil. Assurez-vous que GeForce Experience soit mis à jour vers la dernière version sur votre PC. Essayez de régler les paramètres de diffusion si les plantages continuent.</string>
|
<string name="message_decoding_error">Moonlight s\'est arrêté en raison d\'une incompatibilité avec le décodeur vidéo de cet appareil. Assurez-vous que GeForce Experience soit mis à jour vers la dernière version sur votre PC. Essayez de régler les paramètres de diffusion si les plantages continuent.</string>
|
||||||
<string name="title_decoding_reset">Paramètres vidéo réinitialiser</string>
|
<string name="title_decoding_reset">Paramètres vidéo réinitialiser</string>
|
||||||
<string name="message_decoding_reset">Le décodeur vidéo de votre appareil continue de planter avec les paramètres de diffusion sélectionnés. Vos paramètres de diffusion ont été réinitialisés par défaut.</string>
|
<string name="message_decoding_reset">Le décodeur vidéo de votre appareil continue de planter avec les paramètres de diffusion sélectionnés. Vos paramètres de diffusion ont été réinitialisés par défaut.</string>
|
||||||
<string name="error_usb_prohibited">L\'accès USB est interdit par votre appareil. Vérifiez vos paramètres Knox ou MDM.</string>
|
<string name="error_usb_prohibited">L\'accès USB est interdit par votre appareil. Vérifiez vos paramètres Knox ou MDM.</string>
|
||||||
@@ -122,24 +131,22 @@
|
|||||||
<string name="summary_fps_list">Augmenter pour un flux vidéo plus lisse. Diminution pour de meilleures performances sur les périphériques bas de gamme.</string>
|
<string name="summary_fps_list">Augmenter pour un flux vidéo plus lisse. Diminution pour de meilleures performances sur les périphériques bas de gamme.</string>
|
||||||
<string name="title_seekbar_bitrate">Sélectionnez le bitrate vidéo à obtenir</string>
|
<string name="title_seekbar_bitrate">Sélectionnez le bitrate vidéo à obtenir</string>
|
||||||
<string name="summary_seekbar_bitrate">Bitrate inférieur pour réduire la saccade. Augmentez le bitrate pour augmenter la qualité de l\'image.</string>
|
<string name="summary_seekbar_bitrate">Bitrate inférieur pour réduire la saccade. Augmentez le bitrate pour augmenter la qualité de l\'image.</string>
|
||||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
|
||||||
<string name="title_unlock_fps">Débloquer tous les taux d\'images possibles</string>
|
|
||||||
<string name="summary_unlock_fps">La diffusion en continu à 90 ou 120 FPS peut réduire la latence sur les périphériques haut de gamme, mais peut provoquer des retards ou des blocages sur les périphériques qui ne peuvent \pas le prendre en charge</string>
|
|
||||||
<string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string>
|
<string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string>
|
||||||
<string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string>
|
<string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string>
|
||||||
<string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string>
|
|
||||||
<string name="title_checkbox_enable_pip">Activer le mode observateur dans l\'image</string>
|
<string name="title_checkbox_enable_pip">Activer le mode observateur dans l\'image</string>
|
||||||
<string name="summary_checkbox_enable_pip">Permet de visualiser le flux (sans le contrôleur) tout en multitâche</string>
|
<string name="summary_checkbox_enable_pip">Permet de visualiser le flux (sans le contrôleur) tout en multitâche</string>
|
||||||
|
|
||||||
<string name="category_audio_settings">Paramètres audio</string>
|
<string name="category_audio_settings">Paramètres audio</string>
|
||||||
<string name="title_checkbox_51_surround">Activer son surround 5.1</string>
|
<string name="title_audio_config_list">Configuration son surround</string>
|
||||||
<string name="summary_checkbox_51_surround">Décochez si vous rencontrez des problèmes audio. Nécessite GFE 2.7 ou supérieur.</string>
|
<string name="summary_audio_config_list">Activer le son surround 5.1 ou 7.1 pour les systèmes home cinéma</string>
|
||||||
|
|
||||||
<string name="category_input_settings">Paramètres d\'entrée</string>
|
<string name="category_input_settings">Paramètres d\'entrée</string>
|
||||||
|
<string name="title_checkbox_touchscreen_trackpad">Utilisez l\'écran tactile comme trackpad</string>
|
||||||
|
<string name="summary_checkbox_touchscreen_trackpad">S\'il est activé, l\'écran tactile agit comme un trackpad. S\'il est désactivé, l\'écran tactile contrôle directement le curseur de la souris.</string>
|
||||||
<string name="title_checkbox_multi_controller">Prise en charge de plusieurs contrôleurs</string>
|
<string name="title_checkbox_multi_controller">Prise en charge de plusieurs contrôleurs</string>
|
||||||
<string name="summary_checkbox_multi_controller">Lorsqu\'elle n\'est pas cochée, tous les contrôleurs sont regroupés</string>
|
<string name="summary_checkbox_multi_controller">Lorsqu\'elle n\'est pas cochée, tous les contrôleurs sont regroupés</string>
|
||||||
<string name="title_checkbox_vibrate_fallback">Emuler le support vibration</string>
|
<string name="title_checkbox_vibrate_fallback">Emuler support vibration de secours</string>
|
||||||
<string name="summary_checkbox_vibrate_fallback">Vibre votre appareil pour émuler une vibration si votre manette ne le prend pas en charge</string>
|
<string name="summary_checkbox_vibrate_fallback">Emuler des tremblements si votre manette ne le prend pas en charge</string>
|
||||||
<string name="title_seekbar_deadzone">Régler la zone morte du stick analogique</string>
|
<string name="title_seekbar_deadzone">Régler la zone morte du stick analogique</string>
|
||||||
<string name="suffix_seekbar_deadzone">%</string>
|
<string name="suffix_seekbar_deadzone">%</string>
|
||||||
<string name="title_checkbox_xb1_driver">Pilote de contrôleur Xbox 360/One</string>
|
<string name="title_checkbox_xb1_driver">Pilote de contrôleur Xbox 360/One</string>
|
||||||
@@ -148,14 +155,16 @@
|
|||||||
<string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string>
|
<string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string>
|
||||||
<string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string>
|
<string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string>
|
||||||
<string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string>
|
<string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string>
|
||||||
<string name="title_checkbox_mouse_nav_buttons">Activer les boutons de souris arrière et avant</string>
|
<string name="title_checkbox_mouse_nav_buttons">Activer les boutons de la souris arrière et avant</string>
|
||||||
<string name="summary_checkbox_mouse_nav_buttons">L\'activation de cette option peut entraîner un clic droit sur certains périphériques.</string>
|
<string name="summary_checkbox_mouse_nav_buttons">L\' activation de cette option peut entraîner un clic droit sur certains périphériques.</string>
|
||||||
|
<string name="title_checkbox_flip_face_buttons">Boutons de face inversé</string>
|
||||||
|
<string name="summary_checkbox_flip_face_buttons">Commute les boutons de face A/B et X/Y pour les manettes de jeu et les commandes à l\'écran</string>
|
||||||
|
|
||||||
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
|
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
|
||||||
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
|
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
|
||||||
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
|
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
|
||||||
<string name="title_checkbox_vibrate_osc">Activer les vibrations</string>
|
<string name="title_checkbox_vibrate_osc">Activer les vibrations</string>
|
||||||
<string name="summary_checkbox_vibrate_osc">Vibre votre appareil pour émuler les vibrations des commandes à l\'écran</string>
|
<string name="summary_checkbox_vibrate_osc">Emuler des tremblements des commandes à l\'écran</string>
|
||||||
<string name="title_only_l3r3">Montre seulement L3 et R3</string>
|
<string name="title_only_l3r3">Montre seulement L3 et R3</string>
|
||||||
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
|
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
|
||||||
<string name="title_reset_osc">Effacer la disposition des commandes à l\'écran sauvegardée</string>
|
<string name="title_reset_osc">Effacer la disposition des commandes à l\'écran sauvegardée</string>
|
||||||
@@ -163,12 +172,14 @@
|
|||||||
<string name="dialog_title_reset_osc">Réinitialiser la mise en page</string>
|
<string name="dialog_title_reset_osc">Réinitialiser la mise en page</string>
|
||||||
<string name="dialog_text_reset_osc">Êtes-vous sûr de vouloir supprimer la disposition des commandes à l\'écran que vous avez sauvegardée?</string>
|
<string name="dialog_text_reset_osc">Êtes-vous sûr de vouloir supprimer la disposition des commandes à l\'écran que vous avez sauvegardée?</string>
|
||||||
<string name="toast_reset_osc_success">Les contrôles à l\'écran sont réinitialisés</string>
|
<string name="toast_reset_osc_success">Les contrôles à l\'écran sont réinitialisés</string>
|
||||||
|
<string name="title_osc_opacity">Modifier l\'opacité des contrôles à l\'écran</string>
|
||||||
|
<string name="summary_osc_opacity">Rendre les contrôles à l\'écran plus/moins transparents</string>
|
||||||
|
<string name="dialog_title_osc_opacity">Modifiez l\'opacité</string>
|
||||||
|
<string name="suffix_osc_opacity">%</string>
|
||||||
|
|
||||||
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
|
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
|
||||||
<string name="title_language_list">Langue</string>
|
<string name="title_language_list">Langue</string>
|
||||||
<string name="summary_language_list">Langue à utiliser pour Moonlight</string>
|
<string name="summary_language_list">Langue à utiliser pour Moonlight</string>
|
||||||
<string name="title_checkbox_list_mode">Utiliser des listes au lieu des grilles</string>
|
|
||||||
<string name="summary_checkbox_list_mode">Afficher les applications et les PC en listes au lieu de grilles</string>
|
|
||||||
<string name="title_checkbox_small_icon_mode">Utiliser des petites icônes</string>
|
<string name="title_checkbox_small_icon_mode">Utiliser des petites icônes</string>
|
||||||
<string name="summary_checkbox_small_icon_mode">Utilisez les petites icônes dans les éléments de la grille pour permettre plus d\'éléments à l\'écran</string>
|
<string name="summary_checkbox_small_icon_mode">Utilisez les petites icônes dans les éléments de la grille pour permettre plus d\'éléments à l\'écran</string>
|
||||||
|
|
||||||
@@ -179,7 +190,10 @@
|
|||||||
<string name="summary_checkbox_host_audio">Lire l\'audio de l\'ordinateur et de ce périphérique</string>
|
<string name="summary_checkbox_host_audio">Lire l\'audio de l\'ordinateur et de ce périphérique</string>
|
||||||
|
|
||||||
<string name="category_advanced_settings">Réglages avancés</string>
|
<string name="category_advanced_settings">Réglages avancés</string>
|
||||||
<string name="title_disable_frame_drop">Désactiver la suppression d\'image</string>
|
<string name="title_unlock_fps">Débloquez toutes les fréquences d\'images possibles</string>
|
||||||
|
<string name="summary_unlock_fps">Le streaming à 90 ou 120 FPS peut réduire la latence sur les appareils haut de gamme, mais peut entraîner un décalage ou une instabilité sur les appareils qui ne peuvent pas le prendre en charge</string>
|
||||||
|
<string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant la diffusion</string>
|
||||||
|
<string name="title_disable_frame_drop">Ne jamais laisser tomber les frames</string>
|
||||||
<string name="summary_disable_frame_drop">Peut réduire les micro-saccades sur certains appareils, mais peut augmenter la latence</string>
|
<string name="summary_disable_frame_drop">Peut réduire les micro-saccades sur certains appareils, mais peut augmenter la latence</string>
|
||||||
<string name="title_video_format">Modifier les paramètres H.265</string>
|
<string name="title_video_format">Modifier les paramètres H.265</string>
|
||||||
<string name="summary_video_format">H.265 réduit les besoins en bande passante vidéo mais nécessite un périphérique très récent</string>
|
<string name="summary_video_format">H.265 réduit les besoins en bande passante vidéo mais nécessite un périphérique très récent</string>
|
||||||
@@ -187,5 +201,6 @@
|
|||||||
<string name="summary_enable_hdr">Diffuser du HDR lorsque le jeu et le processeur graphique du PC le prennent en charge. HDR nécessite un GPU série GTX 1000 ou une version ultérieure.</string>
|
<string name="summary_enable_hdr">Diffuser du HDR lorsque le jeu et le processeur graphique du PC le prennent en charge. HDR nécessite un GPU série GTX 1000 ou une version ultérieure.</string>
|
||||||
<string name="title_enable_perf_overlay">Activer la superposition de performance</string>
|
<string name="title_enable_perf_overlay">Activer la superposition de performance</string>
|
||||||
<string name="summary_enable_perf_overlay">Afficher une superposition à l\'écran avec des informations de performance en temps réel pendant la lecture en continu</string>
|
<string name="summary_enable_perf_overlay">Afficher une superposition à l\'écran avec des informations de performance en temps réel pendant la lecture en continu</string>
|
||||||
|
<string name="title_enable_post_stream_toast">Afficher le message de latence après la diffusion en continu</string>
|
||||||
|
<string name="summary_enable_post_stream_toast">Afficher un message d’informations de latence après la fin du flux</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -105,7 +105,6 @@
|
|||||||
<string name="summary_resolution_list">Valori troppo elevati possono causare lag o crash</string>
|
<string name="summary_resolution_list">Valori troppo elevati possono causare lag o crash</string>
|
||||||
<string name="title_seekbar_bitrate">Velocità di trasmissione video</string>
|
<string name="title_seekbar_bitrate">Velocità di trasmissione video</string>
|
||||||
<string name="summary_seekbar_bitrate">Abbassa la velocità di trasmissione per ridurre lo stuttering; alzala per migliorare la qualità dell\'immagine</string>
|
<string name="summary_seekbar_bitrate">Abbassa la velocità di trasmissione per ridurre lo stuttering; alzala per migliorare la qualità dell\'immagine</string>
|
||||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
|
||||||
<string name="title_checkbox_stretch_video">Forza video a schermo intero</string>
|
<string name="title_checkbox_stretch_video">Forza video a schermo intero</string>
|
||||||
<string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</string>
|
<string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</string>
|
||||||
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
|
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
|
||||||
@@ -113,8 +112,6 @@
|
|||||||
<string name="summary_checkbox_enable_pip">Permette di osservare (ma non di controllare) la stream in multitasking</string>
|
<string name="summary_checkbox_enable_pip">Permette di osservare (ma non di controllare) la stream in multitasking</string>
|
||||||
|
|
||||||
<string name="category_audio_settings">Impostazioni audio</string>
|
<string name="category_audio_settings">Impostazioni audio</string>
|
||||||
<string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string>
|
|
||||||
<string name="summary_checkbox_51_surround">Se riscontri problemi, disabilitalo. Richiede GFE 2.7 o versioni sucessive.</string>
|
|
||||||
|
|
||||||
<string name="title_checkbox_multi_controller">Supporto a più controller</string>
|
<string name="title_checkbox_multi_controller">Supporto a più controller</string>
|
||||||
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string>
|
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string>
|
||||||
@@ -141,8 +138,6 @@
|
|||||||
<string name="category_ui_settings">Impostazioni dell\'interfaccia</string>
|
<string name="category_ui_settings">Impostazioni dell\'interfaccia</string>
|
||||||
<string name="title_language_list">Lingua</string>
|
<string name="title_language_list">Lingua</string>
|
||||||
<string name="summary_language_list">Lingua da usare in Moonlight</string>
|
<string name="summary_language_list">Lingua da usare in Moonlight</string>
|
||||||
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
|
|
||||||
<string name="summary_checkbox_list_mode">Visualizza le applicazioni e i PC in una lista invece di una griglia</string>
|
|
||||||
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
|
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
|
||||||
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella griglia per avere più oggetti a schermo</string>
|
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella griglia per avere più oggetti a schermo</string>
|
||||||
|
|
||||||
@@ -159,5 +154,6 @@
|
|||||||
<string name="summary_video_format">H.265 riduce i requisiti di larghezza di banda video ma richiede un dispositivo molto recente</string>
|
<string name="summary_video_format">H.265 riduce i requisiti di larghezza di banda video ma richiede un dispositivo molto recente</string>
|
||||||
<string name="title_enable_hdr">Abilita HDR (sperimentale)</string>
|
<string name="title_enable_hdr">Abilita HDR (sperimentale)</string>
|
||||||
<string name="summary_enable_hdr">Utilizza l\'HDR quando il gioco e la scheda video del PC lo supportano. L\'HDR richiede una scheda video serie GTX 1000 o sucessive.</string>
|
<string name="summary_enable_hdr">Utilizza l\'HDR quando il gioco e la scheda video del PC lo supportano. L\'HDR richiede una scheda video serie GTX 1000 o sucessive.</string>
|
||||||
|
<string name="suffix_osc_opacity">%</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -84,14 +84,11 @@
|
|||||||
<string name="summary_resolution_list">品質が高いほどラグとクラッシュが発生しやすくなります</string>
|
<string name="summary_resolution_list">品質が高いほどラグとクラッシュが発生しやすくなります</string>
|
||||||
<string name="title_seekbar_bitrate">映像のビットレート</string>
|
<string name="title_seekbar_bitrate">映像のビットレート</string>
|
||||||
<string name="summary_seekbar_bitrate">ビットレートを低くすればカクつきが抑制され、高くすれば画質が向上します</string>
|
<string name="summary_seekbar_bitrate">ビットレートを低くすればカクつきが抑制され、高くすれば画質が向上します</string>
|
||||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
|
||||||
<string name="title_checkbox_stretch_video">映像を全画面に拡大</string>
|
<string name="title_checkbox_stretch_video">映像を全画面に拡大</string>
|
||||||
<string name="title_checkbox_disable_warnings">警告を無効化</string>
|
<string name="title_checkbox_disable_warnings">警告を無効化</string>
|
||||||
<string name="summary_checkbox_disable_warnings">ストリーミング中に画面に警告メッセージを表示しない</string>
|
<string name="summary_checkbox_disable_warnings">ストリーミング中に画面に警告メッセージを表示しない</string>
|
||||||
|
|
||||||
<string name="category_audio_settings">音声</string>
|
<string name="category_audio_settings">音声</string>
|
||||||
<string name="title_checkbox_51_surround">5.1chサラウンド</string>
|
|
||||||
<string name="summary_checkbox_51_surround">音声に問題が生じる場合はチェックを外してください。バージョン2.7以降のGFEが必要です</string>
|
|
||||||
|
|
||||||
<string name="title_checkbox_multi_controller">複数のゲームコントローラ</string>
|
<string name="title_checkbox_multi_controller">複数のゲームコントローラ</string>
|
||||||
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
|
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
|
||||||
@@ -107,8 +104,6 @@
|
|||||||
<string name="category_ui_settings">インターフェース</string>
|
<string name="category_ui_settings">インターフェース</string>
|
||||||
<string name="title_language_list">言語</string>
|
<string name="title_language_list">言語</string>
|
||||||
<string name="summary_language_list">Moonlightで使用する言語</string>
|
<string name="summary_language_list">Moonlightで使用する言語</string>
|
||||||
<string name="title_checkbox_list_mode">リストメニュー</string>
|
|
||||||
<string name="summary_checkbox_list_mode">ゲームをグリッドではなく、リストで表示します</string>
|
|
||||||
<string name="title_checkbox_small_icon_mode">小さなアイコン</string>
|
<string name="title_checkbox_small_icon_mode">小さなアイコン</string>
|
||||||
<string name="summary_checkbox_small_icon_mode">グリッドで小さなアイコンを使用します</string>
|
<string name="summary_checkbox_small_icon_mode">グリッドで小さなアイコンを使用します</string>
|
||||||
|
|
||||||
@@ -121,5 +116,18 @@
|
|||||||
<string name="category_advanced_settings">高度な設定</string>
|
<string name="category_advanced_settings">高度な設定</string>
|
||||||
<string name="title_video_format">H.265</string>
|
<string name="title_video_format">H.265</string>
|
||||||
<string name="summary_video_format">H.265は動画に必要な帯域幅を圧縮します。この機能にはなるべく新しいデバイスが必要です</string>
|
<string name="summary_video_format">H.265は動画に必要な帯域幅を圧縮します。この機能にはなるべく新しいデバイスが必要です</string>
|
||||||
|
<string name="suffix_osc_opacity">%</string>
|
||||||
|
<string name="dialog_title_osc_opacity">透過率</string>
|
||||||
|
<string name="title_osc_opacity">透過率</string>
|
||||||
|
<string name="summary_osc_opacity">オンスクリーンコントローラの透過率を調整します</string>
|
||||||
|
<string name="title_only_l3r3">L3 と R3 のみ表示します</string>
|
||||||
|
<string name="summary_only_l3r3">L3 と R3 以外のボタンを表示しない</string>
|
||||||
|
<string name="title_reset_osc">オンスクリーンコントローラをデフォルトに戻します</string>
|
||||||
|
<string name="summary_reset_osc">サイズやレイアウトを戻します</string>
|
||||||
|
<string name="dialog_title_reset_osc">デフォルトに戻します</string>
|
||||||
|
<string name="dialog_text_reset_osc">本当にデフォルトに戻しますか?</string>
|
||||||
|
<string name="toast_reset_osc_success">オンスクリーンコントローラをデフォルトに戻しました</string>
|
||||||
|
<string name="title_checkbox_vibrate_osc">振動</string>
|
||||||
|
<string name="summary_checkbox_vibrate_osc">コントローラの振動を真似します</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -99,14 +99,11 @@
|
|||||||
<string name="summary_resolution_list">세팅 값이 자신의 PC 성능보다 너무 높으면 렉이나 깨짐을 유발할 수 있습니다.</string>
|
<string name="summary_resolution_list">세팅 값이 자신의 PC 성능보다 너무 높으면 렉이나 깨짐을 유발할 수 있습니다.</string>
|
||||||
<string name="title_seekbar_bitrate">비트레이트 타겟 지정</string>
|
<string name="title_seekbar_bitrate">비트레이트 타겟 지정</string>
|
||||||
<string name="summary_seekbar_bitrate">낮은 비트레이트는 끊김을 줄이고, 높은 비트레이트는 품질을 높입니다.</string>
|
<string name="summary_seekbar_bitrate">낮은 비트레이트는 끊김을 줄이고, 높은 비트레이트는 품질을 높입니다.</string>
|
||||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
|
||||||
<string name="title_checkbox_stretch_video">전체 화면으로 렌더링 스크린 늘이기</string>
|
<string name="title_checkbox_stretch_video">전체 화면으로 렌더링 스크린 늘이기</string>
|
||||||
<string name="title_checkbox_disable_warnings">경고 메세지 끄기</string>
|
<string name="title_checkbox_disable_warnings">경고 메세지 끄기</string>
|
||||||
<string name="summary_checkbox_disable_warnings">화면 상의 연결 경고 메세지를 스트리밍 중에 비활성화합니다.</string>
|
<string name="summary_checkbox_disable_warnings">화면 상의 연결 경고 메세지를 스트리밍 중에 비활성화합니다.</string>
|
||||||
|
|
||||||
<string name="category_audio_settings">오디오 설정</string>
|
<string name="category_audio_settings">오디오 설정</string>
|
||||||
<string name="title_checkbox_51_surround">5.1 서라운드 사운드 활성화</string>
|
|
||||||
<string name="summary_checkbox_51_surround">오디오 문제가 발생한다면 체크를 해제하세요. GFE 2.7이나 그 이상 버전이 필요합니다.</string>
|
|
||||||
|
|
||||||
<string name="title_checkbox_multi_controller">다중 컨트롤러 지원</string>
|
<string name="title_checkbox_multi_controller">다중 컨트롤러 지원</string>
|
||||||
<string name="summary_checkbox_multi_controller">이 옵션을 선택하지 않으면 모든 컨트롤러가 하나로 표시됩니다</string>
|
<string name="summary_checkbox_multi_controller">이 옵션을 선택하지 않으면 모든 컨트롤러가 하나로 표시됩니다</string>
|
||||||
@@ -122,8 +119,6 @@
|
|||||||
<string name="category_ui_settings">UI 설정</string>
|
<string name="category_ui_settings">UI 설정</string>
|
||||||
<string name="title_language_list">언어</string>
|
<string name="title_language_list">언어</string>
|
||||||
<string name="summary_language_list">Moonlight에서 사용할 언어를 선택합니다.</string>
|
<string name="summary_language_list">Moonlight에서 사용할 언어를 선택합니다.</string>
|
||||||
<string name="title_checkbox_list_mode">그리드(바둑판) 대신 리스트 뷰 사용</string>
|
|
||||||
<string name="summary_checkbox_list_mode">그리드 대신 리스트로 앱과 PC를 표시합니다.</string>
|
|
||||||
<string name="title_checkbox_small_icon_mode">작은 아이콘 사용</string>
|
<string name="title_checkbox_small_icon_mode">작은 아이콘 사용</string>
|
||||||
<string name="summary_checkbox_small_icon_mode">더 많이 표시하기 위해 그리드 표시에서 작은 아이콘을 사용합니다.</string>
|
<string name="summary_checkbox_small_icon_mode">더 많이 표시하기 위해 그리드 표시에서 작은 아이콘을 사용합니다.</string>
|
||||||
|
|
||||||
@@ -136,5 +131,6 @@
|
|||||||
<string name="category_advanced_settings">고급 설정</string>
|
<string name="category_advanced_settings">고급 설정</string>
|
||||||
<string name="title_video_format">H.265 설정 변경</string>
|
<string name="title_video_format">H.265 설정 변경</string>
|
||||||
<string name="summary_video_format">H.265는 비디오 대역폭 요구사항을 낮춰주지만 최신 장치가 필요합니다.</string>
|
<string name="summary_video_format">H.265는 비디오 대역폭 요구사항을 낮춰주지만 최신 장치가 필요합니다.</string>
|
||||||
|
<string name="suffix_osc_opacity">%</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -88,14 +88,11 @@
|
|||||||
<string name="summary_resolution_list">Te hoge instellingen kunnen crashes en haperingen veroorzaken.</string>
|
<string name="summary_resolution_list">Te hoge instellingen kunnen crashes en haperingen veroorzaken.</string>
|
||||||
<string name="title_seekbar_bitrate">Selecteer doel video bitsnelheid</string>
|
<string name="title_seekbar_bitrate">Selecteer doel video bitsnelheid</string>
|
||||||
<string name="summary_seekbar_bitrate">Verlaag bitsnelheid om haperingen te verminderen. Verhoog de bitsnelheid voor een betere videokwaliteit.</string>
|
<string name="summary_seekbar_bitrate">Verlaag bitsnelheid om haperingen te verminderen. Verhoog de bitsnelheid voor een betere videokwaliteit.</string>
|
||||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
|
||||||
<string name="title_checkbox_stretch_video">Rek video uit tot volledig scherm</string>
|
<string name="title_checkbox_stretch_video">Rek video uit tot volledig scherm</string>
|
||||||
<string name="title_checkbox_disable_warnings">Verberg waarschuwingsberichten</string>
|
<string name="title_checkbox_disable_warnings">Verberg waarschuwingsberichten</string>
|
||||||
<string name="summary_checkbox_disable_warnings">Verberg on-screen verbindingswaarschuwingen tijdens het streamen</string>
|
<string name="summary_checkbox_disable_warnings">Verberg on-screen verbindingswaarschuwingen tijdens het streamen</string>
|
||||||
|
|
||||||
<string name="category_audio_settings">Geluidsinstellingen</string>
|
<string name="category_audio_settings">Geluidsinstellingen</string>
|
||||||
<string name="title_checkbox_51_surround">Gebruik 5.1 surround sound</string>
|
|
||||||
<string name="summary_checkbox_51_surround">Gebruik dit niet als er problemen zijn met de audio. Vereist GFE 2.7 of hoger.</string>
|
|
||||||
|
|
||||||
<string name="title_checkbox_multi_controller">Multi-gamepad support</string>
|
<string name="title_checkbox_multi_controller">Multi-gamepad support</string>
|
||||||
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
|
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
|
||||||
@@ -111,8 +108,6 @@
|
|||||||
<string name="category_ui_settings">UI Installingen</string>
|
<string name="category_ui_settings">UI Installingen</string>
|
||||||
<string name="title_language_list">Taal</string>
|
<string name="title_language_list">Taal</string>
|
||||||
<string name="summary_language_list">Taal te gebruiken in Moonlight</string>
|
<string name="summary_language_list">Taal te gebruiken in Moonlight</string>
|
||||||
<string name="title_checkbox_list_mode">Gebruik lijsten in plaats van kolommen</string>
|
|
||||||
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
|
|
||||||
<string name="title_checkbox_small_icon_mode">Gebruik kleine iconen</string>
|
<string name="title_checkbox_small_icon_mode">Gebruik kleine iconen</string>
|
||||||
<string name="summary_checkbox_small_icon_mode">Gebruik kleine iconen in kolom onderdelen zodat meer items tegelijk zichtbaar worden.</string>
|
<string name="summary_checkbox_small_icon_mode">Gebruik kleine iconen in kolom onderdelen zodat meer items tegelijk zichtbaar worden.</string>
|
||||||
|
|
||||||
@@ -125,5 +120,6 @@
|
|||||||
<string name="category_advanced_settings">Geavanceerde Instellingen</string>
|
<string name="category_advanced_settings">Geavanceerde Instellingen</string>
|
||||||
<string name="title_video_format">Verander H.265 instellingen</string>
|
<string name="title_video_format">Verander H.265 instellingen</string>
|
||||||
<string name="summary_video_format">H.265 verlaagt video bandbreedte vereisten maar benodigdt een recent apparaat.</string>
|
<string name="summary_video_format">H.265 verlaagt video bandbreedte vereisten maar benodigdt een recent apparaat.</string>
|
||||||
|
<string name="suffix_osc_opacity">%</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="audio_config_names">
|
||||||
|
<item>Stereo</item>
|
||||||
|
<item>Sunet Surround 5.1</item>
|
||||||
|
<item>Sunet Surround 7.1</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="decoder_names">
|
||||||
|
<item>Auto-selectează decodorul</item>
|
||||||
|
<item>Forțează decodarea Software</item>
|
||||||
|
<item>Forțează decodarea Hardware</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="video_format_names">
|
||||||
|
<item>Folosește H.265 doar dacă e stabil</item>
|
||||||
|
<item>Folosește H.265 mereu (se poate bloca)</item>
|
||||||
|
<item>Nu folosi H.265</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Shortcut strings -->
|
||||||
|
<string name="scut_deleted_pc">PC șters</string>
|
||||||
|
<string name="scut_not_paired">PC neîmperecheat</string>
|
||||||
|
<string name="scut_pc_not_found">PC negăsit</string>
|
||||||
|
<string name="scut_invalid_uuid">PC-ul este invalid</string>
|
||||||
|
<string name="scut_invalid_app_id">Aplicația este invalidă</string>
|
||||||
|
|
||||||
|
<!-- Help strings -->
|
||||||
|
<string name="help_loading_title">Ajutor</string>
|
||||||
|
<string name="help_loading_msg">Se încarcă pagina de ajutor…</string>
|
||||||
|
|
||||||
|
<!-- PC view menu entries -->
|
||||||
|
<string name="pcview_menu_app_list">Vezi lista de jocuri</string>
|
||||||
|
<string name="pcview_menu_pair_pc">Împerechează PC-ul</string>
|
||||||
|
<string name="pcview_menu_unpair_pc">Desperechează</string>
|
||||||
|
<string name="pcview_menu_send_wol">Trimite o cerere Wake-On-LAN</string>
|
||||||
|
<string name="pcview_menu_delete_pc">Șterge PC</string>
|
||||||
|
<string name="pcview_menu_details">Vezi detalii</string>
|
||||||
|
|
||||||
|
<!-- Pair messages -->
|
||||||
|
<string name="pairing">Se împerechează…</string>
|
||||||
|
<string name="pair_pc_offline">PC-ul nu este accesibil</string>
|
||||||
|
<string name="pair_pc_ingame">PC-ul rulează un joc acum. Trebuie să închizi jocul pentru a-l putea împerechea.</string>
|
||||||
|
<string name="pair_pairing_title">Se împerechează</string>
|
||||||
|
<string name="pair_pairing_msg">Te rugăm să introduci urmatorul PIN în PC-ul pe care îl împerechezi:</string>
|
||||||
|
<string name="pair_incorrect_pin">PIN-ul este greșit</string>
|
||||||
|
<string name="pair_fail">Împerecherea a eșuat</string>
|
||||||
|
<string name="pair_already_in_progress">Împerecherea este deja în curs</string>
|
||||||
|
|
||||||
|
<!-- WOL messages -->
|
||||||
|
<string name="wol_pc_online">PC-ul este accesibil</string>
|
||||||
|
<string name="wol_no_mac">Nu s-a putut porni PC-ul deoarece GFE nu a comunicat o adresa MAC</string>
|
||||||
|
<string name="wol_waking_pc">Se pornește PC-ul…</string>
|
||||||
|
<string name="wol_waking_msg">Poate dura puțin până PC-ul pornește. Dacă nu pornește, verifică dacă este configurat corect pentru Wake-On-LAN.</string>
|
||||||
|
<string name="wol_fail">Nu s-au putut trimite pachetele Wake-On-LAN</string>
|
||||||
|
|
||||||
|
<!-- Unpair messages -->
|
||||||
|
<string name="unpairing">Desperecherechere…</string>
|
||||||
|
<string name="unpair_success">Desperecherechere efectuată cu succes</string>
|
||||||
|
<string name="unpair_fail">Desperecherea a eșuat</string>
|
||||||
|
<string name="unpair_error">Dispozitivul nu este împerecheat</string>
|
||||||
|
|
||||||
|
<!-- Errors -->
|
||||||
|
<string name="error_pc_offline">PC-ul este inaccesibil</string>
|
||||||
|
<string name="error_manager_not_running">Serviciul ComputerManager nu este pornit. Te rugăm să aștepți câteva secunde sau să repornești aplicația.</string>
|
||||||
|
<string name="error_unknown_host">Nu am putut identifica hostul</string>
|
||||||
|
<string name="error_404">GFE a returnat un cod de eroare HTTP 404. Asigură-te ca PC-ul are o placa video suportată.
|
||||||
|
Este posibil sa apară această eroare daca folosești o alta aplicație de remote desktop. Încearcă să repornești PC-ul sau să reinstalezi GFE.
|
||||||
|
</string>
|
||||||
|
<string name="title_decoding_error">Decodorul video s-a închis în mod neașteptat</string>
|
||||||
|
<string name="message_decoding_error">Moonlight s-a închis în mod neașteptat datorită unei incompatibilități cu decodorul video al acestui dispozitiv. Asigură-te ca folosești ultima versiune de GFE. Dacă problema persistă, ajustează setările de streaming.</string>
|
||||||
|
<string name="title_decoding_reset">Resetează setările video</string>
|
||||||
|
<string name="message_decoding_reset">Decodorul video al acestui dispozitiv continuă să se blocheze folosind setările video curente. Au fost resetate cele implicite.</string>
|
||||||
|
<string name="error_usb_prohibited">Accesul USB este interzis de către administratorul dispozitivului. Verifică setarile Knox sau MDM.</string>
|
||||||
|
<string name="unable_to_pin_shortcut">Launcher-ul tău curent nu permite crearea de scurtături fixate.</string>
|
||||||
|
<string name="video_decoder_init_failed">Inițializarea decodorului video a eșuat. Este posibil ca acest dispozitiv să nu suporte rezoluția sau rata cadrelor selectată.</string>
|
||||||
|
|
||||||
|
<!-- Start application messages -->
|
||||||
|
<string name="conn_establishing_title">Se stabilește conexiunea</string>
|
||||||
|
<string name="conn_establishing_msg">Se pornește conexiunea</string>
|
||||||
|
<string name="conn_metered">Atenție: Conexiunea ta curentă este contorizată!</string>
|
||||||
|
<string name="conn_client_latency">Latența medie a decodării cadrelor:</string>
|
||||||
|
<string name="conn_client_latency_hw">latența decodorului hardware:</string>
|
||||||
|
<string name="conn_hardware_latency">Latența medie a decodării cadrelor (hardware):</string>
|
||||||
|
<string name="conn_starting">Se pornește</string>
|
||||||
|
<string name="conn_error_title">Eroare la conectare</string>
|
||||||
|
<string name="conn_error_msg">Pornirea a eșuat</string>
|
||||||
|
<string name="conn_terminated_title">Conexiunea închisă</string>
|
||||||
|
<string name="conn_terminated_msg">Conexiunea a fost terminată</string>
|
||||||
|
|
||||||
|
<!-- General strings -->
|
||||||
|
<string name="ip_hint">Adresa IP a PC-ului cu GFE</string>
|
||||||
|
<string name="searching_pc">Se caută PC-uri cu GameStream activat…\n\n
|
||||||
|
Asigură-te ca GameStream este activat în setările Geforce Experience SHIELD.</string>
|
||||||
|
<string name="yes">Da</string>
|
||||||
|
<string name="no">Nu</string>
|
||||||
|
<string name="lost_connection">S-a pierdut conexiunea catre PC</string>
|
||||||
|
<string name="title_details">Detalii</string>
|
||||||
|
<string name="help">Ajutor</string>
|
||||||
|
<string name="delete_pc_msg">Sigur dorești să ștergi acest PC?</string>
|
||||||
|
<string name="slow_connection_msg">Conexiune inceată catre PC\nRedu rata de biți</string>
|
||||||
|
<string name="poor_connection_msg">Conexiune slabă catre PC</string>
|
||||||
|
<string name="perf_overlay_text">Dimensiunile video: %1$s\nDecodor: %2$s\nRata cadrelor estimata PC: %3$.2f FPS\nRata cadrelor primite din rețea: %4$.2f FPS\nRata de afisare a cadrelor: %5$.2f FPS\nCadre pierdute de rețea: %6$.2f%%\nTimpul mediu de primire: %7$.2f ms\nTimpul mediu de decodare: %8$.2f ms</string>
|
||||||
|
|
||||||
|
<!-- AppList activity -->
|
||||||
|
<string name="applist_connect_msg">Se conectează la PC…</string>
|
||||||
|
<string name="applist_menu_resume">Continuă Sesiunea</string>
|
||||||
|
<string name="applist_menu_quit">Închide Sesiunea</string>
|
||||||
|
<string name="applist_menu_quit_and_start">Închide Jocul Curent si Pornește</string>
|
||||||
|
<string name="applist_menu_cancel">Anulează</string>
|
||||||
|
<string name="applist_menu_details">Vezi detalii</string>
|
||||||
|
<string name="applist_menu_scut">Creează o scurtătură</string>
|
||||||
|
<string name="applist_menu_tv_channel">Adaugă la canal</string>
|
||||||
|
<string name="applist_refresh_title">Lista de aplicații</string>
|
||||||
|
<string name="applist_refresh_msg">Reîmprospătare aplicații…</string>
|
||||||
|
<string name="applist_refresh_error_title">Eroare</string>
|
||||||
|
<string name="applist_refresh_error_msg">Nu s-a putut obține lista de aplicații</string>
|
||||||
|
<string name="applist_quit_app">Închidere în curs</string>
|
||||||
|
<string name="applist_quit_success">Închis cu succes</string>
|
||||||
|
<string name="applist_quit_fail">Nu s-a putut închide lista</string>
|
||||||
|
<string name="applist_quit_confirmation">Sigur dorești să închizi aplicația curentă? Toate datele nesalvate vor fi pierdute.</string>
|
||||||
|
<string name="applist_details_id">ID-ul aplicației:</string>
|
||||||
|
|
||||||
|
<!-- Add computer manually activity -->
|
||||||
|
<string name="title_add_pc">Adaugă PC manual</string>
|
||||||
|
<string name="msg_add_pc">Conectare în curs…</string>
|
||||||
|
<string name="addpc_fail">Nu s-a putut efectua conectarea la adresa introdusă. Asigurăte ca porturile nu sunt blocate in firewall.</string>
|
||||||
|
<string name="addpc_success">PC adăugat cu succes</string>
|
||||||
|
<string name="addpc_unknown_host">Nu am putut identifica adresa PC-ului. Asigură-te că ai introdus-o corect.</string>
|
||||||
|
<string name="addpc_enter_ip">Trebuie să introduci o adresa IP</string>
|
||||||
|
<string name="addpc_wrong_sitelocal">Adresa introdusă nu pare corectă. Pentru conectare prin Internet, este nevoie de adresa publică a routerului.</string>
|
||||||
|
|
||||||
|
<!-- Preferences -->
|
||||||
|
<string name="category_basic_settings">Setări de bază</string>
|
||||||
|
<string name="title_resolution_list">Rezolutia video</string>
|
||||||
|
<string name="summary_resolution_list">Crește-o pentru a îmbunătăți claritatea imaginii. Descrește-o pentru dispozitive neperformante sau conexiune slabă.</string>
|
||||||
|
<string name="title_fps_list">Rata cadrelor</string>
|
||||||
|
<string name="summary_fps_list">Crește-o pentru a îmbunătăți fluiditatea imaginilor. Descrește-o pentru dispozitive neperformante sau conexiune slabă.</string>
|
||||||
|
<string name="title_seekbar_bitrate">Rata de biți</string>
|
||||||
|
<string name="summary_seekbar_bitrate">Crește-o pentru a îmbunătăți calitatea imaginilor. Descrește-o pentru dispozitive neperformante sau conexiune slabă.</string>
|
||||||
|
<string name="title_unlock_fps">Deblochează toate ratele de cadre posibile</string>
|
||||||
|
<string name="summary_unlock_fps">Fluxul video de rate mai mari poate reduce latența folosind dispozitive performante, dar poate introduce erori daca nu sunt suportate.</string>
|
||||||
|
<string name="title_checkbox_stretch_video">Întindeți video pe ecranul complet</string>
|
||||||
|
<string name="title_checkbox_disable_warnings">Dezactivează mesajele de avertizare</string>
|
||||||
|
<string name="summary_checkbox_disable_warnings">Dezactivează mesajele de avertizare privind rețeaua în timpul conexiunii</string>
|
||||||
|
<string name="title_checkbox_enable_pip">Activează modul Picture-In-Picture</string>
|
||||||
|
<string name="summary_checkbox_enable_pip">Permite vizualizarea (dar nu și controlul) când efectuezi multitasking</string>
|
||||||
|
|
||||||
|
<string name="category_audio_settings">Setări Audio</string>
|
||||||
|
<string name="title_audio_config_list">Configurarea sunetului surround</string>
|
||||||
|
<string name="summary_audio_config_list">Activeaza sunetul 5.1 sau 7.1 pentru sisteme home-theater</string>
|
||||||
|
|
||||||
|
<string name="category_input_settings">Setări de control</string>
|
||||||
|
<string name="title_checkbox_multi_controller">Detectează automat prezența controllerelor.</string>
|
||||||
|
<string name="summary_checkbox_multi_controller">Dezactivarea acestei opțiuni implică prezența constantă a unui controller</string>
|
||||||
|
<string name="title_checkbox_vibrate_fallback">Simuleaza efectul de vibratie</string>
|
||||||
|
<string name="summary_checkbox_vibrate_fallback">Dacă controllerul nu suportă vibrații, va vibra dispozitivul în schimb.</string>
|
||||||
|
<string name="title_seekbar_deadzone">Zona moartă a stickului analogic</string>
|
||||||
|
<string name="suffix_seekbar_deadzone">%</string>
|
||||||
|
<string name="title_checkbox_xb1_driver">Driver pentru controllerele de Xbox 360/One</string>
|
||||||
|
<string name="summary_checkbox_xb1_driver">Activează un driver USB pentru dispozitivele fără suport nativ pentru controllere Xbox</string>
|
||||||
|
<string name="title_checkbox_usb_bind_all">Inlocuiește driverul implicit pentru controllere</string>
|
||||||
|
<string name="summary_checkbox_usb_bind_all">Forțează driverul USB Moonlight să preia toate controllerele Xbox suportate</string>
|
||||||
|
<string name="title_checkbox_mouse_emulation">Simulează mouse cu controllerul</string>
|
||||||
|
<string name="summary_checkbox_mouse_emulation">Apăsarea lungă pe butonul Start schimba modul de operare a controllerului în modul mouse.</string>
|
||||||
|
<string name="title_checkbox_mouse_nav_buttons">Activează butoanele de înainte și înapoi ale mousului</string>
|
||||||
|
<string name="summary_checkbox_mouse_nav_buttons">Această opțiune poate afecta click dreapta pentru unele dispozitive problematice.</string>
|
||||||
|
|
||||||
|
<string name="category_on_screen_controls_settings">Setări ale controalelor pe ecran</string>
|
||||||
|
<string name="title_checkbox_show_onscreen_controls">Afișează controale pe ecran</string>
|
||||||
|
<string name="summary_checkbox_show_onscreen_controls">Afișează un controller virtual pe ecran</string>
|
||||||
|
<string name="title_checkbox_vibrate_osc">Activează vibrațiile</string>
|
||||||
|
<string name="summary_checkbox_vibrate_osc">Dispozitivul va vibra asemănător unui controller</string>
|
||||||
|
<string name="title_only_l3r3">Afișează doar L3 si R3</string>
|
||||||
|
<string name="summary_only_l3r3">Ascunde toate butoanele în afară de L3 and R3</string>
|
||||||
|
<string name="title_reset_osc">Șterge schema salvată a controalelor</string>
|
||||||
|
<string name="summary_reset_osc">Resetează toate controalele de pe ecran la poziția și dimensiunea implicită</string>
|
||||||
|
<string name="dialog_title_reset_osc">Resetarea schemei de controale</string>
|
||||||
|
<string name="dialog_text_reset_osc">Sigur dorești să ștergi schema salvată a controalelor de pe ecran?</string>
|
||||||
|
<string name="toast_reset_osc_success">Controalele de pe ecran au fost resetate la setarile implicite</string>
|
||||||
|
<string name="title_osc_opacity">Modifică opacitatea controalelor de pe ecran</string>
|
||||||
|
<string name="summary_osc_opacity">Ajustează gradul de transparență al controalelor de pe ecran</string>
|
||||||
|
<string name="dialog_title_osc_opacity">Modifică opacitatea</string>
|
||||||
|
<string name="suffix_osc_opacity">%</string>
|
||||||
|
|
||||||
|
<string name="category_ui_settings">Setari UI</string>
|
||||||
|
<string name="title_language_list">Limba (Language)</string>
|
||||||
|
<string name="summary_language_list">Limba folosită de către Moonlight</string>
|
||||||
|
<string name="title_checkbox_small_icon_mode">Folosește iconițe mici</string>
|
||||||
|
<string name="summary_checkbox_small_icon_mode">Iconițele folosite în grile vor fi mici pentru a încăpea mai multe odata</string>
|
||||||
|
|
||||||
|
<string name="category_host_settings">Setările PC-ului gazdă</string>
|
||||||
|
<string name="title_checkbox_enable_sops">Optimizarea setărilor de joc</string>
|
||||||
|
<string name="summary_checkbox_enable_sops">Permite GFE să modifice setările jocurilor pentru experiența optimă</string>
|
||||||
|
<string name="title_checkbox_host_audio">Redă audio si pe PC</string>
|
||||||
|
<string name="summary_checkbox_host_audio">Sunetul se va auzi atat pe acest dispozitiv cât și pe PC</string>
|
||||||
|
|
||||||
|
<string name="category_advanced_settings">Setări avansate</string>
|
||||||
|
<string name="title_disable_frame_drop">Nu pierde cadre intenționat</string>
|
||||||
|
<string name="summary_disable_frame_drop">Poate să reducă micro-stuttering pe anumite device-uri, dar s-ar putea să crească latența</string>
|
||||||
|
<string name="title_video_format">Modifica setările H.265</string>
|
||||||
|
<string name="summary_video_format">H.265 funcționează cu o conexiune mai slaba, dar necesită un dispozitiv recent, performant</string>
|
||||||
|
<string name="title_enable_hdr">Activează HDR (Experimental)</string>
|
||||||
|
<string name="summary_enable_hdr">Folosește HDR daca aplicația si placa video suportă. Necesită o placa video seria GTX 1000 sau mai nouă.</string>
|
||||||
|
<string name="title_enable_perf_overlay">Activează statisticile de performanță</string>
|
||||||
|
<string name="summary_enable_perf_overlay">Afișează în timp real statisticile de performanță ale conexiunii.</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user