Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91261f9520 |
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
# Moonlight
|
#Moonlight
|
||||||
|
|
||||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
@@ -10,35 +10,35 @@ 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.
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
## Features
|
##Features
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
* Streams any of your games from your PC to your Android device
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* Automatically finds GameStream-compatible PCs on your network
|
||||||
|
|
||||||
## Installation
|
##Installation
|
||||||
|
|
||||||
* Download and install Moonlight for Android from
|
* Download and install Moonlight for Android from
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||||
|
|
||||||
## Requirements
|
##Requirements
|
||||||
|
|
||||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
|
||||||
* Android device running 4.1 (Jelly Bean) or higher
|
* Android device running 4.1 (Jelly Bean) or higher
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
* High-end wireless router (802.11n dual-band recommended)
|
||||||
|
|
||||||
## Usage
|
##Usage
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC and add the PIN if needed
|
* Accept the pairing confirmation on your PC
|
||||||
* Tap your PC again to view the list of apps to stream
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
|
|
||||||
## Contribute
|
##Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
|
|
||||||
@@ -46,13 +46,13 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
## Building
|
##Building
|
||||||
* Install Android Studio and the Android NDK
|
* Install Android Studio and the Android NDK
|
||||||
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
||||||
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
||||||
* Build the APK using Android Studio
|
* Build the APK using Android Studio
|
||||||
|
|
||||||
## Authors
|
##Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
||||||
|
|||||||
@@ -1,43 +1,33 @@
|
|||||||
|
import com.android.builder.model.ProductFlavor
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 25
|
||||||
|
buildToolsVersion '25.0.3'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 27
|
targetSdkVersion 25
|
||||||
|
|
||||||
versionName "5.7.2"
|
versionName "5.0.1"
|
||||||
versionCode = 150
|
versionCode = 123
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "root"
|
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
root {
|
root {
|
||||||
// Android O has native mouse capture, so don't show the rooted
|
|
||||||
// version to devices running O on the Play Store.
|
|
||||||
maxSdkVersion 25
|
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
ndkBuild {
|
|
||||||
arguments "PRODUCT_FLAVOR=root"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationId "com.limelight.root"
|
applicationId "com.limelight.root"
|
||||||
dimension "root"
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
externalNativeBuild {
|
|
||||||
ndkBuild {
|
|
||||||
arguments "PRODUCT_FLAVOR=nonRoot"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationId "com.limelight"
|
applicationId "com.limelight"
|
||||||
dimension "root"
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,42 +36,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
}
|
|
||||||
release {
|
release {
|
||||||
// To whomever is releasing/using an APK in release mode with
|
|
||||||
// Moonlight's official application ID, please stop. I see every
|
|
||||||
// single one of your crashes in my Play Console and it makes
|
|
||||||
// Moonlight's reliability look worse and makes it more difficult
|
|
||||||
// to distinguish real crashes from your crashy VR app. Seriously,
|
|
||||||
// 44 of the *same* native crash in 72 hours and a few each of
|
|
||||||
// several other crashes.
|
|
||||||
//
|
|
||||||
// This is technically not your fault. I would have hoped Google
|
|
||||||
// would validate the signature of the APK before attributing
|
|
||||||
// the crash to it. I asked their Play Store support about this
|
|
||||||
// and they said they don't and don't have plans to, so that sucks.
|
|
||||||
//
|
|
||||||
// In any case, it's bad form to release an APK using someone
|
|
||||||
// else's application ID. There is no legitimate reason, that
|
|
||||||
// anyone would need to comment out the following line, except me
|
|
||||||
// when I release an official signed Moonlight build. If you feel
|
|
||||||
// like doing so would solve something, I can tell you it will not.
|
|
||||||
// You can't upgrade an app while retaining data without having the
|
|
||||||
// same signature as the official version. Nor can you post it on
|
|
||||||
// the Play Store, since that application ID is already taken.
|
|
||||||
// Reputable APK hosting websites similarly validate the signature
|
|
||||||
// is consistent with the Play Store and won't allow an APK that
|
|
||||||
// isn't signed the same as the original.
|
|
||||||
//
|
|
||||||
// I wish any and all people using Moonlight as the basis of other
|
|
||||||
// cool projects the best of luck with their efforts. All I ask
|
|
||||||
// is to please change the applicationId before you publish.
|
|
||||||
//
|
|
||||||
// TL;DR: Leave the following line alone!
|
|
||||||
applicationIdSuffix ".unofficial"
|
|
||||||
|
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||||
}
|
}
|
||||||
@@ -95,9 +50,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
compile 'org.bouncycastle:bcpkix-jdk15on:1.52'
|
||||||
implementation 'org.jcodec:jcodec:0.2.3'
|
compile project(':moonlight-common')
|
||||||
|
compile files('libs/jcodec-0.1.9-patched.jar')
|
||||||
implementation project(':moonlight-common')
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="app_label" translatable="false">Moonlight (Debug)</string>
|
|
||||||
<string name="app_label_root" translatable="false">Moonlight (Root Debug)</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -24,20 +24,12 @@
|
|||||||
android:name="android.software.leanback"
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<!-- Disable legacy input emulation on ChromeOS -->
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.type.pc"
|
|
||||||
android:required="false"/>
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:isGame="true"
|
android:isGame="true"
|
||||||
android:banner="@drawable/atv_banner"
|
android:banner="@drawable/atv_banner"
|
||||||
android:appCategory="game"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<!-- Samsung multi-window support -->
|
<!-- Samsung multi-window support -->
|
||||||
@@ -104,10 +96,7 @@
|
|||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
android:supportsPictureInPicture="true"
|
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:theme="@style/StreamTheme">
|
android:theme="@style/StreamTheme">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 58 KiB |
@@ -2,6 +2,7 @@ package com.limelight;
|
|||||||
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
@@ -26,6 +27,7 @@ import android.app.Service;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
@@ -235,10 +237,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Assume we're in the foreground when created to avoid a race
|
|
||||||
// between binding to CMS and onResume()
|
|
||||||
inForeground = true;
|
|
||||||
|
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
@@ -252,7 +250,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
|
||||||
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
||||||
TextView label = findViewById(R.id.appListText);
|
TextView label = (TextView) findViewById(R.id.appListText);
|
||||||
setTitle(labelText);
|
setTitle(labelText);
|
||||||
label.setText(labelText);
|
label.setText(labelText);
|
||||||
|
|
||||||
@@ -304,9 +302,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
// Display a decoder crash notification if we've returned after a crash
|
|
||||||
UiHelper.showDecoderCrashDialog(this);
|
|
||||||
|
|
||||||
inForeground = true;
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public class AppViewShortcutTrampoline extends Activity {
|
|||||||
// If a game is running, we'll make the stream the top level activity
|
// If a game is running, we'll make the stream the top level activity
|
||||||
if (details.runningGameId != 0) {
|
if (details.runningGameId != 0) {
|
||||||
intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this,
|
intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this,
|
||||||
new NvApp("app", details.runningGameId, false), details, managerBinder));
|
new NvApp("app", details.runningGameId), details, managerBinder));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now start the activities
|
// Now start the activities
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.limelight.binding.input.TouchContext;
|
|||||||
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.virtual_controller.VirtualController;
|
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||||
import com.limelight.binding.video.CrashListener;
|
|
||||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
@@ -20,7 +19,6 @@ import com.limelight.nvstream.http.NvApp;
|
|||||||
import com.limelight.nvstream.input.KeyboardPacket;
|
import com.limelight.nvstream.input.KeyboardPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
import com.limelight.nvstream.jni.MoonBridge;
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
import com.limelight.preferences.GlPreferences;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
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;
|
||||||
@@ -30,17 +28,13 @@ import com.limelight.utils.SpinnerDialog;
|
|||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.PictureInPictureParams;
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
@@ -50,7 +44,6 @@ import android.os.Bundle;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Rational;
|
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@@ -88,7 +81,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private VirtualController virtualController;
|
private VirtualController virtualController;
|
||||||
|
|
||||||
private PreferenceConfiguration prefConfig;
|
private PreferenceConfiguration prefConfig;
|
||||||
private SharedPreferences tombstonePrefs;
|
|
||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
private SpinnerDialog spinner;
|
private SpinnerDialog spinner;
|
||||||
@@ -105,7 +97,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private ShortcutHelper shortcutHelper;
|
private ShortcutHelper shortcutHelper;
|
||||||
|
|
||||||
private MediaCodecDecoderRenderer decoderRenderer;
|
private MediaCodecDecoderRenderer decoderRenderer;
|
||||||
private boolean reportedCrash;
|
|
||||||
|
|
||||||
private WifiManager.WifiLock wifiLock;
|
private WifiManager.WifiLock wifiLock;
|
||||||
|
|
||||||
@@ -131,12 +122,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
public static final String EXTRA_PC_UUID = "UUID";
|
public static final String EXTRA_PC_UUID = "UUID";
|
||||||
public static final String EXTRA_PC_NAME = "PcName";
|
public static final String EXTRA_PC_NAME = "PcName";
|
||||||
public static final String EXTRA_APP_HDR = "HDR";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
|
|
||||||
// We don't want a title bar
|
// We don't want a title bar
|
||||||
@@ -173,33 +165,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Read the stream preferences
|
// Read the stream preferences
|
||||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
|
||||||
|
|
||||||
|
|
||||||
// Listen for events on the game surface
|
// Listen for events on the game surface
|
||||||
streamView = findViewById(R.id.surfaceView);
|
streamView = (StreamView) findViewById(R.id.surfaceView);
|
||||||
streamView.setOnGenericMotionListener(this);
|
streamView.setOnGenericMotionListener(this);
|
||||||
streamView.setOnTouchListener(this);
|
streamView.setOnTouchListener(this);
|
||||||
|
|
||||||
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
// The view must be focusable for pointer capture to work.
|
|
||||||
streamView.setFocusable(true);
|
|
||||||
streamView.setDefaultFocusHighlightEnabled(false);
|
|
||||||
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
|
|
||||||
return handleMotionEvent(motionEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn the user if they're on a metered connection
|
// Warn the user if they're on a metered connection
|
||||||
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
checkDataConnection();
|
||||||
if (connMgr.isActiveNetworkMetered()) {
|
|
||||||
displayTransientMessage(getResources().getString(R.string.conn_metered));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure Wi-Fi is fully powered up
|
// Make sure Wi-Fi is fully powered up
|
||||||
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||||
@@ -214,7 +187,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||||
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||||
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||||
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
|
|
||||||
|
|
||||||
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||||
finish();
|
finish();
|
||||||
@@ -222,144 +194,41 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
|
||||||
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
|
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
|
||||||
shortcutHelper.reportShortcutUsed(uuid);
|
shortcutHelper.reportShortcutUsed(uuid);
|
||||||
|
|
||||||
// Initialize the MediaCodec helper before creating the decoder
|
// Initialize the MediaCodec helper before creating the decoder
|
||||||
GlPreferences glPrefs = GlPreferences.readPreferences(this);
|
MediaCodecHelper.initializeWithContext(this);
|
||||||
MediaCodecHelper.initialize(this, glPrefs.glRenderer);
|
|
||||||
|
|
||||||
// Check if the user has enabled HDR
|
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat);
|
||||||
if (prefConfig.enableHdr) {
|
|
||||||
// Check if the app supports it
|
|
||||||
if (!willStreamHdr) {
|
|
||||||
Toast.makeText(this, "This game does not support HDR10", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
// It does, so start our HDR checklist
|
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
// We already know the app supports HDR if willStreamHdr is set.
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
|
||||||
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
|
|
||||||
|
|
||||||
// We must now ensure our display is compatible with HDR10
|
|
||||||
boolean foundHdr10 = false;
|
|
||||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
|
||||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
|
||||||
LimeLog.info("Display supports HDR10");
|
|
||||||
foundHdr10 = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundHdr10) {
|
|
||||||
// Nope, no HDR for us :(
|
|
||||||
willStreamHdr = false;
|
|
||||||
Toast.makeText(this, "Display does not support HDR10", Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Toast.makeText(this, "HDR requires Android 7.0 or later", Toast.LENGTH_LONG).show();
|
|
||||||
willStreamHdr = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
willStreamHdr = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig,
|
|
||||||
new CrashListener() {
|
|
||||||
@Override
|
|
||||||
public void notifyCrash(Exception e) {
|
|
||||||
// The MediaCodec instance is going down due to a crash
|
|
||||||
// let's tell the user something when they open the app again
|
|
||||||
|
|
||||||
// We must use commit because the app will crash when we return from this function
|
|
||||||
tombstonePrefs.edit().putInt("CrashCount", tombstonePrefs.getInt("CrashCount", 0) + 1).commit();
|
|
||||||
reportedCrash = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tombstonePrefs.getInt("CrashCount", 0),
|
|
||||||
connMgr.isActiveNetworkMetered(),
|
|
||||||
willStreamHdr,
|
|
||||||
glPrefs.glRenderer
|
|
||||||
);
|
|
||||||
|
|
||||||
// Don't stream HDR if the decoder can't support it
|
|
||||||
if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) {
|
|
||||||
willStreamHdr = false;
|
|
||||||
Toast.makeText(this, "Decoder does not support HEVC Main10HDR10", Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
|
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
|
||||||
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
||||||
Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
|
if (!decoderRenderer.isAvcSupported()) {
|
||||||
if (!prefConfig.multiController && gamepadMask != 0) {
|
if (spinner != null) {
|
||||||
// If any gamepads are present in non-MC mode, set only gamepad 1.
|
spinner.dismiss();
|
||||||
gamepadMask = 1;
|
spinner = null;
|
||||||
}
|
|
||||||
if (prefConfig.onscreenController) {
|
|
||||||
// If we're using OSC, always set at least gamepad 1.
|
|
||||||
gamepadMask |= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set to the optimal mode for streaming
|
|
||||||
float displayRefreshRate = prepareDisplayForRendering();
|
|
||||||
LimeLog.info("Display refresh rate: "+displayRefreshRate);
|
|
||||||
|
|
||||||
// HACK: Despite many efforts to ensure low latency consistent frame
|
|
||||||
// delivery, the best non-lossy mechanism is to buffer 1 extra frame
|
|
||||||
// in the output pipeline. Android does some buffering on its end
|
|
||||||
// in SurfaceFlinger and it's difficult (impossible?) to inspect
|
|
||||||
// the precise state of the buffer queue to the screen after we
|
|
||||||
// release a frame for rendering.
|
|
||||||
//
|
|
||||||
// Since buffering a frame adds latency and we are primarily a
|
|
||||||
// latency-optimized client, rather than one designed for picture-perfect
|
|
||||||
// accuracy, we will synthetically induce a negative pressure on the display
|
|
||||||
// output pipeline by driving the decoder input pipeline under the speed
|
|
||||||
// that the display can refresh. This ensures a constant negative pressure
|
|
||||||
// to keep latency down but does induce a periodic frame loss. However, this
|
|
||||||
// periodic frame loss is *way* less than what we'd already get in Marshmallow's
|
|
||||||
// display pipeline where frames are dropped outside of our control if they land
|
|
||||||
// on the same V-sync.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
int roundedRefreshRate = Math.round(displayRefreshRate);
|
|
||||||
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) {
|
|
||||||
if (roundedRefreshRate <= 49) {
|
|
||||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
|
||||||
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
|
|
||||||
}
|
}
|
||||||
// HACK: Avoid crashing on some MTK devices
|
|
||||||
else if (roundedRefreshRate == 50 && decoderRenderer.is49FpsBlacklisted()) {
|
|
||||||
// Use the old rendering strategy on these broken devices
|
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
prefConfig.fps = roundedRefreshRate - 1;
|
|
||||||
LimeLog.info("Adjusting FPS target for screen to "+prefConfig.fps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// If we can't find an AVC decoder, we can't proceed
|
||||||
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||||
|
"This device or ROM doesn't support hardware accelerated H.264 playback.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setRefreshRate(prefConfig.fps)
|
||||||
.setApp(new NvApp(appName, appId, willStreamHdr))
|
.setApp(new NvApp(appName, appId))
|
||||||
.setBitrate(prefConfig.bitrate)
|
.setBitrate(prefConfig.bitrate * 1000)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292)
|
.setMaxPacketSize(remote ? 1024 : 1292)
|
||||||
.setRemote(remote)
|
.setRemote(remote)
|
||||||
.setHevcBitratePercentageMultiplier(75)
|
|
||||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||||
.setEnableHdr(willStreamHdr)
|
|
||||||
.setAttachedGamepadMask(gamepadMask)
|
|
||||||
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
|
|
||||||
.setAudioConfiguration(prefConfig.enable51Surround ?
|
.setAudioConfiguration(prefConfig.enable51Surround ?
|
||||||
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
|
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
|
||||||
MoonBridge.AUDIO_CONFIGURATION_STEREO)
|
MoonBridge.AUDIO_CONFIGURATION_STEREO)
|
||||||
@@ -367,11 +236,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this));
|
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this));
|
||||||
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
controllerHandler = new ControllerHandler(this, conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
|
||||||
|
|
||||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
inputManager.registerInputDeviceListener(controllerHandler, null);
|
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||||
|
|
||||||
|
// Set to the optimal mode for streaming
|
||||||
|
prepareDisplayForRendering();
|
||||||
|
|
||||||
// Initialize touch contexts
|
// Initialize touch contexts
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
touchContextMap[i] = new TouchContext(conn, i,
|
touchContextMap[i] = new TouchContext(conn, i,
|
||||||
@@ -385,10 +257,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
getWindow().setSustainedPerformanceMode(true);
|
getWindow().setSustainedPerformanceMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||||
|
|
||||||
if (prefConfig.onscreenController) {
|
if (prefConfig.onscreenController) {
|
||||||
// create virtual onscreen controller
|
// create virtual onscreen controller
|
||||||
virtualController = new VirtualController(conn,
|
virtualController = new VirtualController(conn,
|
||||||
(FrameLayout)streamView.getParent(),
|
(FrameLayout)findViewById(R.id.surfaceView).getParent(),
|
||||||
this);
|
this);
|
||||||
virtualController.refreshLayout();
|
virtualController.refreshLayout();
|
||||||
}
|
}
|
||||||
@@ -399,57 +273,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
usbDriverServiceConnection, Service.BIND_AUTO_CREATE);
|
usbDriverServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decoderRenderer.isAvcSupported()) {
|
|
||||||
if (spinner != null) {
|
|
||||||
spinner.dismiss();
|
|
||||||
spinner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we can't find an AVC decoder, we can't proceed
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
|
||||||
"This device or ROM doesn't support hardware accelerated H.264 playback.", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The connection will be started when the surface gets created
|
// The connection will be started when the surface gets created
|
||||||
streamView.getHolder().addCallback(this);
|
streamView.getHolder().addCallback(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void prepareDisplayForRendering() {
|
||||||
public void onUserLeaveHint() {
|
|
||||||
super.onUserLeaveHint();
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
if (prefConfig.enablePip && connected) {
|
|
||||||
enterPictureInPictureMode(
|
|
||||||
new PictureInPictureParams.Builder()
|
|
||||||
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
|
|
||||||
.setSourceRectHint(new Rect(
|
|
||||||
streamView.getLeft(), streamView.getTop(),
|
|
||||||
streamView.getRight(), streamView.getBottom()))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWindowFocusChanged(boolean hasFocus) {
|
|
||||||
super.onWindowFocusChanged(hasFocus);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
// Capture is lost when focus is lost, so it must be requested again
|
|
||||||
// when focus is regained.
|
|
||||||
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
|
|
||||||
// Recapture the pointer if focus was regained
|
|
||||||
streamView.requestPointerCapture();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float prepareDisplayForRendering() {
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||||
float displayRefreshRate;
|
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -487,7 +317,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
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());
|
||||||
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||||
displayRefreshRate = bestMode.getRefreshRate();
|
|
||||||
}
|
}
|
||||||
// On L, we can at least tell the OS that we want 60 Hz
|
// On L, we can at least tell the OS that we want 60 Hz
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
@@ -500,12 +329,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
||||||
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
||||||
displayRefreshRate = bestRefreshRate;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Otherwise, the active display refresh rate is just
|
|
||||||
// whatever is currently in use.
|
|
||||||
displayRefreshRate = display.getRefreshRate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the display mode change
|
// Apply the display mode change
|
||||||
@@ -541,22 +364,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Set the surface to scale based on the aspect ratio of the stream
|
// Set the surface to scale based on the aspect ratio of the stream
|
||||||
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return displayRefreshRate;
|
private void checkDataConnection()
|
||||||
|
{
|
||||||
|
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (mgr.isActiveNetworkMetered()) {
|
||||||
|
displayTransientMessage(getResources().getString(R.string.conn_metered));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private final Runnable hideSystemUi = new Runnable() {
|
private final Runnable hideSystemUi = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// In multi-window mode on N+, we need to drop our layout flags or we'll
|
|
||||||
// be drawing underneath the system UI.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode()) {
|
|
||||||
Game.this.getWindow().getDecorView().setSystemUiVisibility(
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
|
||||||
}
|
|
||||||
// Use immersive mode on 4.4+ or standard low profile on previous builds
|
// Use immersive mode on 4.4+ or standard low profile on previous builds
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
Game.this.getWindow().getDecorView().setSystemUiVisibility(
|
Game.this.getWindow().getDecorView().setSystemUiVisibility(
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
@@ -581,34 +404,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
|
||||||
public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
|
|
||||||
super.onMultiWindowModeChanged(isInMultiWindowMode);
|
|
||||||
|
|
||||||
// In multi-window, we don't want to use the full-screen layout
|
|
||||||
// flag. It will cause us to collide with the system UI.
|
|
||||||
// This function will also be called for PiP so we can cover
|
|
||||||
// that case here too.
|
|
||||||
if (isInMultiWindowMode) {
|
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
||||||
|
|
||||||
// Disable performance optimizations for foreground
|
|
||||||
getWindow().setSustainedPerformanceMode(false);
|
|
||||||
decoderRenderer.notifyVideoBackground();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
||||||
|
|
||||||
// Enable performance optimizations for foreground
|
|
||||||
getWindow().setSustainedPerformanceMode(true);
|
|
||||||
decoderRenderer.notifyVideoForeground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Correct the system UI visibility flags
|
|
||||||
hideSystemUi(50);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
@@ -625,17 +420,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
unbindService(usbDriverServiceConnection);
|
unbindService(usbDriverServiceConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy the capture provider
|
|
||||||
inputCaptureProvider.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
SpinnerDialog.closeDialogs(this);
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
|
|
||||||
if (conn != null) {
|
if (conn != null) {
|
||||||
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
||||||
|
|
||||||
@@ -657,10 +441,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// 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) {
|
||||||
message += " [H.265 HDR]";
|
|
||||||
}
|
|
||||||
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
|
|
||||||
message += " [H.265]";
|
message += " [H.265]";
|
||||||
}
|
}
|
||||||
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
|
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
|
||||||
@@ -671,15 +452,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
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
|
|
||||||
if (!reportedCrash && tombstonePrefs.getInt("CrashCount", 0) != 0) {
|
|
||||||
tombstonePrefs.edit()
|
|
||||||
.putInt("CrashCount", 0)
|
|
||||||
.putInt("LastNotifiedCrashCount", 0)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@@ -699,19 +480,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Returns true if the key stroke was consumed
|
// Returns true if the key stroke was consumed
|
||||||
private boolean handleSpecialKeys(int androidKeyCode, boolean down) {
|
private boolean handleSpecialKeys(short translatedKey, boolean down) {
|
||||||
int modifierMask = 0;
|
int modifierMask = 0;
|
||||||
|
|
||||||
if (androidKeyCode == KeyEvent.KEYCODE_CTRL_LEFT ||
|
// Mask off the high byte
|
||||||
androidKeyCode == KeyEvent.KEYCODE_CTRL_RIGHT) {
|
translatedKey &= 0xff;
|
||||||
|
|
||||||
|
if (translatedKey == KeyboardTranslator.VK_CONTROL) {
|
||||||
modifierMask = KeyboardPacket.MODIFIER_CTRL;
|
modifierMask = KeyboardPacket.MODIFIER_CTRL;
|
||||||
}
|
}
|
||||||
else if (androidKeyCode == KeyEvent.KEYCODE_SHIFT_LEFT ||
|
else if (translatedKey == KeyboardTranslator.VK_SHIFT) {
|
||||||
androidKeyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
|
|
||||||
modifierMask = KeyboardPacket.MODIFIER_SHIFT;
|
modifierMask = KeyboardPacket.MODIFIER_SHIFT;
|
||||||
}
|
}
|
||||||
else if (androidKeyCode == KeyEvent.KEYCODE_ALT_LEFT ||
|
else if (translatedKey == KeyboardTranslator.VK_ALT) {
|
||||||
androidKeyCode == KeyEvent.KEYCODE_ALT_RIGHT) {
|
|
||||||
modifierMask = KeyboardPacket.MODIFIER_ALT;
|
modifierMask = KeyboardPacket.MODIFIER_ALT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,7 +504,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if Ctrl+Shift+Z is pressed
|
// Check if Ctrl+Shift+Z is pressed
|
||||||
if (androidKeyCode == KeyEvent.KEYCODE_Z &&
|
if (translatedKey == KeyboardTranslator.VK_Z &&
|
||||||
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) ==
|
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) ==
|
||||||
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT))
|
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT))
|
||||||
{
|
{
|
||||||
@@ -784,17 +565,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle a synthetic back button event that some Android OS versions
|
|
||||||
// create as a result of a right-click.
|
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean handled = false;
|
boolean handled = false;
|
||||||
|
|
||||||
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
boolean detectedGamepad = event.getDevice() == null ? false :
|
||||||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
||||||
|
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||||
if (detectedGamepad || (event.getDevice() == null ||
|
if (detectedGamepad || (event.getDevice() == null ||
|
||||||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
||||||
)) {
|
)) {
|
||||||
@@ -811,7 +586,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Let this method take duplicate key down events
|
// Let this method take duplicate key down events
|
||||||
if (handleSpecialKeys(keyCode, true)) {
|
if (handleSpecialKeys(translated, true)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,7 +595,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState(event));
|
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState());
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -833,16 +608,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle a synthetic back button event that some Android OS versions
|
|
||||||
// create as a result of a right-click.
|
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
|
||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean handled = false;
|
boolean handled = false;
|
||||||
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
boolean detectedGamepad = event.getDevice() == null ? false :
|
||||||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
||||||
|
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||||
if (detectedGamepad || (event.getDevice() == null ||
|
if (detectedGamepad || (event.getDevice() == null ||
|
||||||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
||||||
)) {
|
)) {
|
||||||
@@ -858,7 +627,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleSpecialKeys(keyCode, false)) {
|
if (handleSpecialKeys(translated, false)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,8 +671,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
|
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
|
|
||||||
{
|
{
|
||||||
// This case is for mice
|
// This case is for mice
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
@@ -912,11 +680,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
{
|
{
|
||||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||||
|
|
||||||
// Ignore mouse input if we're not capturing from our input source
|
|
||||||
if (!inputCaptureProvider.isCapturingActive()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
||||||
// Send the vertical scroll packet
|
// Send the vertical scroll packet
|
||||||
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||||
@@ -1128,18 +891,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private void stopConnection() {
|
private void stopConnection() {
|
||||||
if (connecting || connected) {
|
if (connecting || connected) {
|
||||||
connecting = connected = false;
|
connecting = connected = false;
|
||||||
|
conn.stop();
|
||||||
// Stop may take a few hundred ms to do some network I/O to tell
|
|
||||||
// the server we're going away and clean up. Let it run in a separate
|
|
||||||
// thread to keep things smooth for the UI. Inside moonlight-common,
|
|
||||||
// we prevent another thread from starting a connection before and
|
|
||||||
// during the process of stopping this one.
|
|
||||||
new Thread() {
|
|
||||||
public void run() {
|
|
||||||
conn.stop();
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable cursor visibility again
|
||||||
|
inputCaptureProvider.disableCapture();
|
||||||
|
|
||||||
|
// Destroy the capture provider
|
||||||
|
inputCaptureProvider.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1149,23 +908,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
spinner = null;
|
spinner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable cursor visibility again
|
|
||||||
inputCaptureProvider.disableCapture();
|
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
LimeLog.severe(stage+" failed: "+errorCode);
|
LimeLog.severe(stage+" failed: "+errorCode);
|
||||||
|
|
||||||
// If video initialization failed and the surface is still valid, display extra information for the user
|
stopConnection();
|
||||||
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||||
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
||||||
}
|
}
|
||||||
@@ -1173,14 +920,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionTerminated(long errorCode) {
|
public void connectionTerminated(long errorCode) {
|
||||||
// Enable cursor visibility again
|
|
||||||
inputCaptureProvider.disableCapture();
|
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
LimeLog.severe("Connection terminated: "+errorCode);
|
LimeLog.severe("Connection terminated: "+errorCode);
|
||||||
stopConnection();
|
|
||||||
|
|
||||||
|
stopConnection();
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
||||||
getResources().getString(R.string.conn_terminated_msg), true);
|
getResources().getString(R.string.conn_terminated_msg), true);
|
||||||
}
|
}
|
||||||
@@ -1247,10 +991,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
// Let the decoder know immediately that the surface is gone
|
|
||||||
decoderRenderer.prepareForStop();
|
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
// HACK: Android is supposed to let you return from this function
|
||||||
|
// before throwing a fit if you access the surface again. Unfortunately,
|
||||||
|
// MediaCodec often tries to access the destroyed surface and triggers
|
||||||
|
// an IllegalStateException. To workaround this, we will invoke
|
||||||
|
// the DecoderRenderer's stop function ourselves, so it will hopefully
|
||||||
|
// happen early enough to not trigger the bug
|
||||||
|
decoderRenderer.stop();
|
||||||
|
|
||||||
stopConnection();
|
stopConnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1297,8 +1046,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
public void keyboardEvent(boolean buttonDown, short keyCode) {
|
public void keyboardEvent(boolean buttonDown, short keyCode) {
|
||||||
short keyMap = KeyboardTranslator.translate(keyCode);
|
short keyMap = KeyboardTranslator.translate(keyCode);
|
||||||
if (keyMap != 0) {
|
if (keyMap != 0) {
|
||||||
// handleSpecialKeys() takes the Android keycode
|
if (handleSpecialKeys(keyMap, buttonDown)) {
|
||||||
if (handleSpecialKeys(keyCode, buttonDown)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,14 @@ public class HelpActivity extends Activity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
if (url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
||||||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
|
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase())) {
|
||||||
|
// Allow navigation to Moonlight docs
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.limelight;
|
|||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
@@ -16,7 +18,6 @@ import com.limelight.nvstream.http.PairingManager;
|
|||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
import com.limelight.nvstream.wol.WakeOnLanSender;
|
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||||
import com.limelight.preferences.AddComputerManually;
|
import com.limelight.preferences.AddComputerManually;
|
||||||
import com.limelight.preferences.GlPreferences;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.preferences.StreamSettings;
|
import com.limelight.preferences.StreamSettings;
|
||||||
import com.limelight.ui.AdapterFragment;
|
import com.limelight.ui.AdapterFragment;
|
||||||
@@ -33,8 +34,6 @@ import android.content.ComponentName;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.opengl.GLSurfaceView;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
@@ -52,15 +51,12 @@ import android.widget.RelativeLayout;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig;
|
|
||||||
import javax.microedition.khronos.opengles.GL10;
|
|
||||||
|
|
||||||
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private RelativeLayout noPcFoundLayout;
|
private RelativeLayout noPcFoundLayout;
|
||||||
private PcGridAdapter pcGridAdapter;
|
private PcGridAdapter pcGridAdapter;
|
||||||
private ShortcutHelper shortcutHelper;
|
private ShortcutHelper shortcutHelper;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
|
private boolean freezeUpdates, runningPolling, inForeground;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
@@ -90,19 +86,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
|
|
||||||
// Only reinitialize views if completeOnCreate() was called
|
// Reinitialize views just in case orientation changed
|
||||||
// before this callback. If it was not, completeOnCreate() will
|
initializeViews();
|
||||||
// handle initializing views with the config change accounted for.
|
|
||||||
// This is not prone to races because both callbacks are invoked
|
|
||||||
// in the main thread.
|
|
||||||
if (completeOnCreateCalled) {
|
|
||||||
// Reinitialize views just in case orientation changed
|
|
||||||
initializeViews();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static int APP_LIST_ID = 1;
|
private final static int APP_LIST_ID = 1;
|
||||||
@@ -122,9 +110,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
||||||
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
||||||
ImageButton helpButton = findViewById(R.id.helpButton);
|
ImageButton helpButton = (ImageButton) findViewById(R.id.helpButton);
|
||||||
|
|
||||||
settingsButton.setOnClickListener(new OnClickListener() {
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -150,7 +138,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
|
|
||||||
noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
|
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
|
||||||
if (pcGridAdapter.getCount() == 0) {
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
@@ -164,52 +152,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Assume we're in the foreground when created to avoid a race
|
|
||||||
// between binding to CMS and onResume()
|
|
||||||
inForeground = true;
|
|
||||||
|
|
||||||
// Create a GLSurfaceView to fetch GLRenderer unless we have
|
|
||||||
// a cached result already.
|
|
||||||
final GlPreferences glPrefs = GlPreferences.readPreferences(this);
|
|
||||||
if (!glPrefs.savedFingerprint.equals(Build.FINGERPRINT) || glPrefs.glRenderer.isEmpty()) {
|
|
||||||
GLSurfaceView surfaceView = new GLSurfaceView(this);
|
|
||||||
surfaceView.setRenderer(new GLSurfaceView.Renderer() {
|
|
||||||
@Override
|
|
||||||
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
|
|
||||||
// Save the GLRenderer string so we don't need to do this next time
|
|
||||||
glPrefs.glRenderer = gl10.glGetString(GL10.GL_RENDERER);
|
|
||||||
glPrefs.savedFingerprint = Build.FINGERPRINT;
|
|
||||||
glPrefs.writePreferences();
|
|
||||||
|
|
||||||
LimeLog.info("Fetched GL Renderer: " + glPrefs.glRenderer);
|
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
completeOnCreate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDrawFrame(GL10 gl10) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setContentView(surfaceView);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.info("Cached GL Renderer: " + glPrefs.glRenderer);
|
|
||||||
completeOnCreate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void completeOnCreate() {
|
|
||||||
completeOnCreateCalled = true;
|
|
||||||
|
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
@@ -278,9 +220,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
// Display a decoder crash notification if we've returned after a crash
|
|
||||||
UiHelper.showDecoderCrashDialog(this);
|
|
||||||
|
|
||||||
inForeground = true;
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
@@ -367,7 +306,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Stop updates and wait while pairing
|
// Stop updates and wait while pairing
|
||||||
stopComputerUpdates(true);
|
stopComputerUpdates(true);
|
||||||
|
|
||||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
InetAddress addr;
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
|
addr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Unknown reachability - using local IP");
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpConn = new NvHTTP(addr,
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
PlatformBinding.getDeviceName(),
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
@@ -492,7 +443,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
InetAddress addr;
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
|
addr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Unknown reachability - using local IP");
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpConn = new NvHTTP(addr,
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
PlatformBinding.getDeviceName(),
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
@@ -579,7 +542,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder);
|
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case QUIT_ID:
|
case QUIT_ID:
|
||||||
@@ -594,7 +557,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void run() {
|
public void run() {
|
||||||
ServerHelper.doQuit(PcView.this,
|
ServerHelper.doQuit(PcView.this,
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
||||||
new NvApp("app", 0, false), managerBinder, null);
|
new NvApp("app", 0), managerBinder, null);
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.limelight.binding.audio;
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
import android.media.AudioAttributes;
|
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
@@ -14,58 +12,10 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
|
|
||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
|
||||||
48000,
|
|
||||||
channelConfig,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
|
|
||||||
.setUsage(AudioAttributes.USAGE_GAME);
|
|
||||||
AudioFormat format = new AudioFormat.Builder()
|
|
||||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
||||||
.setSampleRate(48000)
|
|
||||||
.setChannelMask(channelConfig)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
// Use FLAG_LOW_LATENCY on L through N
|
|
||||||
if (lowLatency) {
|
|
||||||
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
|
|
||||||
.setAudioFormat(format)
|
|
||||||
.setAudioAttributes(attributesBuilder.build())
|
|
||||||
.setTransferMode(AudioTrack.MODE_STREAM)
|
|
||||||
.setBufferSizeInBytes(bufferSize);
|
|
||||||
|
|
||||||
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
|
|
||||||
if (lowLatency) {
|
|
||||||
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trackBuilder.build();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return new AudioTrack(attributesBuilder.build(),
|
|
||||||
format,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM,
|
|
||||||
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int setup(int audioConfiguration) {
|
public int setup(int audioConfiguration) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
|
int bufferSize;
|
||||||
int bytesPerFrame;
|
int bytesPerFrame;
|
||||||
|
|
||||||
switch (audioConfiguration)
|
switch (audioConfiguration)
|
||||||
@@ -88,82 +38,44 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
// do this on many devices and it lowers audio latency.
|
// do this on many devices and it lowers audio latency.
|
||||||
// We'll try the small buffer size first and if it fails,
|
// We'll try the small buffer size first and if it fails,
|
||||||
// use the recommended larger buffer size.
|
// use the recommended larger buffer size.
|
||||||
|
try {
|
||||||
|
// Buffer two frames of audio if possible
|
||||||
|
bufferSize = bytesPerFrame * 2;
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
boolean lowLatency;
|
48000,
|
||||||
int bufferSize;
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM);
|
||||||
|
track.play();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Try to release the AudioTrack if we got far enough
|
||||||
|
try {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
// We will try:
|
// Now try the larger buffer size
|
||||||
// 1) Small buffer, low latency mode
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
||||||
// 2) Large buffer, low latency mode
|
|
||||||
// 3) Small buffer, standard mode
|
|
||||||
// 4) Large buffer, standard mode
|
|
||||||
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
lowLatency = true;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
lowLatency = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Unreachable
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
case 2:
|
|
||||||
bufferSize = bytesPerFrame * 2;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
case 3:
|
|
||||||
// Try the larger buffer size
|
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
bytesPerFrame * 2);
|
bytesPerFrame * 2);
|
||||||
|
|
||||||
// Round to next frame
|
// Round to next frame
|
||||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Unreachable
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip low latency options if hardware sample rate isn't 48000Hz
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
48000,
|
||||||
continue;
|
channelConfig,
|
||||||
}
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
try {
|
AudioTrack.MODE_STREAM);
|
||||||
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
track.play();
|
||||||
track.play();
|
|
||||||
|
|
||||||
// Successfully created working AudioTrack. We're done here.
|
|
||||||
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Try to release the AudioTrack if we got far enough
|
|
||||||
e.printStackTrace();
|
|
||||||
try {
|
|
||||||
if (track != null) {
|
|
||||||
track.release();
|
|
||||||
track = null;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track == null) {
|
|
||||||
// Couldn't create any audio track for playback
|
|
||||||
return -2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Audio track buffer size: "+bufferSize);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,14 +88,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
public void start() {}
|
public void start() {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {}
|
public void stop() {
|
||||||
|
|
||||||
@Override
|
|
||||||
public void cleanup() {
|
|
||||||
// Immediately drop all pending data
|
// Immediately drop all pending data
|
||||||
track.pause();
|
track.pause();
|
||||||
track.flush();
|
track.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
track.release();
|
track.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,8 @@ import java.security.KeyPair;
|
|||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.NoSuchProviderException;
|
import java.security.NoSuchProviderException;
|
||||||
import java.security.Provider;
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@@ -54,7 +54,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
|
|
||||||
private static final Object globalCryptoLock = new Object();
|
private static final Object globalCryptoLock = new Object();
|
||||||
|
|
||||||
private static final Provider bcProvider = new BouncyCastleProvider();
|
static {
|
||||||
|
// Install the Bouncy Castle provider
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
public AndroidCryptoProvider(Context c) {
|
public AndroidCryptoProvider(Context c) {
|
||||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||||
@@ -93,10 +96,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
pemCertBytes = certBytes;
|
pemCertBytes = certBytes;
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
// May happen if the cert is corrupt
|
// May happen if the cert is corrupt
|
||||||
@@ -110,6 +113,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
// May happen if the key is corrupt
|
// May happen if the key is corrupt
|
||||||
LimeLog.warning("Corrupted key");
|
LimeLog.warning("Corrupted key");
|
||||||
return false;
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -122,13 +129,17 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
|
|
||||||
KeyPair keyPair;
|
KeyPair keyPair;
|
||||||
try {
|
try {
|
||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
keyPairGenerator.initialize(2048);
|
keyPairGenerator.initialize(2048);
|
||||||
keyPair = keyPairGenerator.generateKeyPair();
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
@@ -149,8 +160,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
||||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Nothing should go wrong here
|
// Nothing should go wrong here
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbManager;
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
@@ -14,11 +12,9 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.input.driver.UsbDriverListener;
|
import com.limelight.binding.input.driver.UsbDriverListener;
|
||||||
import com.limelight.binding.input.driver.UsbDriverService;
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
import com.limelight.utils.Vector2d;
|
import com.limelight.utils.Vector2d;
|
||||||
|
|
||||||
@@ -51,26 +47,22 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private final GameGestures gestures;
|
private final GameGestures gestures;
|
||||||
private boolean hasGameController;
|
private boolean hasGameController;
|
||||||
|
|
||||||
private final PreferenceConfiguration prefConfig;
|
private final boolean multiControllerEnabled;
|
||||||
private short currentControllers, initialControllers;
|
private short currentControllers;
|
||||||
|
|
||||||
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
|
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
||||||
this.activityContext = activityContext;
|
this.activityContext = activityContext;
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.gestures = gestures;
|
this.gestures = gestures;
|
||||||
this.prefConfig = prefConfig;
|
this.multiControllerEnabled = multiControllerEnabled;
|
||||||
|
|
||||||
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
||||||
// is required for controller batching support to work.
|
// is required for controller batching support to work.
|
||||||
int deadzonePercentage = 10;
|
deadzonePercentage = 10;
|
||||||
|
|
||||||
int[] ids = InputDevice.getDeviceIds();
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
InputDevice dev = InputDevice.getDevice(id);
|
InputDevice dev = InputDevice.getDevice(id);
|
||||||
if (dev == null) {
|
|
||||||
// This device was removed during enumeration
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
||||||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
||||||
// This looks like a gamepad, but we'll check X and Y to be sure
|
// This looks like a gamepad, but we'll check X and Y to be sure
|
||||||
@@ -100,18 +92,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
defaultContext.controllerNumber = (short) 0;
|
defaultContext.controllerNumber = (short) 0;
|
||||||
defaultContext.assignedControllerNumber = true;
|
defaultContext.assignedControllerNumber = true;
|
||||||
|
|
||||||
// Some devices (GPD XD) have a back button which sends input events
|
|
||||||
// with device ID == 0. This hits the default context which would normally
|
|
||||||
// consume these. Instead, let's ignore them since that's probably the
|
|
||||||
// most likely case.
|
|
||||||
defaultContext.ignoreBack = true;
|
|
||||||
|
|
||||||
// Get the initially attached set of gamepads. As each gamepad receives
|
|
||||||
// its initial InputEvent, we will move these from this set onto the
|
|
||||||
// currentControllers set which will allow them to properly unplug
|
|
||||||
// if they are removed.
|
|
||||||
initialControllers = getAttachedControllerMask(activityContext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||||
@@ -149,51 +129,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
onInputDeviceAdded(deviceId);
|
onInputDeviceAdded(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static short getAttachedControllerMask(Context context) {
|
|
||||||
int count = 0;
|
|
||||||
short mask = 0;
|
|
||||||
|
|
||||||
// Count all input devices that are gamepads
|
|
||||||
InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
|
|
||||||
for (int id : im.getInputDeviceIds()) {
|
|
||||||
InputDevice dev = im.getInputDevice(id);
|
|
||||||
if (dev == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0) {
|
|
||||||
LimeLog.info("Counting InputDevice: "+dev.getName());
|
|
||||||
mask |= 1 << count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count all USB devices that match our drivers
|
|
||||||
if (PreferenceConfiguration.readPreferences(context).usbDriver) {
|
|
||||||
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
|
||||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
|
||||||
// We explicitly ask not to claim devices that appear as InputDevices
|
|
||||||
// otherwise we will double count them.
|
|
||||||
if (UsbDriverService.shouldClaimDevice(dev, false)) {
|
|
||||||
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
|
|
||||||
mask |= 1 << count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Enumerated "+count+" gamepads");
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releaseControllerNumber(GenericControllerContext context) {
|
private void releaseControllerNumber(GenericControllerContext context) {
|
||||||
// If we reserved a controller number, remove that reservation
|
// If this device sent data as a gamepad, zero the values before removing
|
||||||
if (context.reservedControllerNumber) {
|
|
||||||
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
|
|
||||||
currentControllers &= ~(1 << context.controllerNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this device sent data as a gamepad, zero the values before removing.
|
|
||||||
// We must do this after clearing the currentControllers entry so this
|
|
||||||
// causes the device to be removed on the server PC.
|
|
||||||
if (context.assignedControllerNumber) {
|
if (context.assignedControllerNumber) {
|
||||||
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
|
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
|
||||||
(short) 0,
|
(short) 0,
|
||||||
@@ -201,6 +138,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
(short) 0, (short) 0,
|
(short) 0, (short) 0,
|
||||||
(short) 0, (short) 0);
|
(short) 0, (short) 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we reserved a controller number, remove that reservation
|
||||||
|
if (context.reservedControllerNumber) {
|
||||||
|
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
|
||||||
|
currentControllers &= ~(1 << context.controllerNumber);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called before sending input but after we've determined that this
|
// Called before sending input but after we've determined that this
|
||||||
@@ -220,7 +163,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
}
|
}
|
||||||
else if (prefConfig.multiController && devContext.hasJoystickAxes) {
|
else if (multiControllerEnabled && devContext.hasJoystickAxes) {
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
|
|
||||||
LimeLog.info("Reserving the next available controller number");
|
LimeLog.info("Reserving the next available controller number");
|
||||||
@@ -228,10 +171,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if ((currentControllers & (1 << i)) == 0) {
|
if ((currentControllers & (1 << i)) == 0) {
|
||||||
// Found an unused controller value
|
// Found an unused controller value
|
||||||
currentControllers |= (1 << i);
|
currentControllers |= (1 << i);
|
||||||
|
|
||||||
// Take this value out of the initial gamepad set
|
|
||||||
initialControllers &= ~(1 << i);
|
|
||||||
|
|
||||||
context.controllerNumber = i;
|
context.controllerNumber = i;
|
||||||
context.reservedControllerNumber = true;
|
context.reservedControllerNumber = true;
|
||||||
break;
|
break;
|
||||||
@@ -244,7 +183,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (prefConfig.multiController) {
|
if (multiControllerEnabled) {
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
|
|
||||||
LimeLog.info("Reserving the next available controller number");
|
LimeLog.info("Reserving the next available controller number");
|
||||||
@@ -252,10 +191,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if ((currentControllers & (1 << i)) == 0) {
|
if ((currentControllers & (1 << i)) == 0) {
|
||||||
// Found an unused controller value
|
// Found an unused controller value
|
||||||
currentControllers |= (1 << i);
|
currentControllers |= (1 << i);
|
||||||
|
|
||||||
// Take this value out of the initial gamepad set
|
|
||||||
initialControllers &= ~(1 << i);
|
|
||||||
|
|
||||||
context.controllerNumber = i;
|
context.controllerNumber = i;
|
||||||
context.reservedControllerNumber = true;
|
context.reservedControllerNumber = true;
|
||||||
break;
|
break;
|
||||||
@@ -436,7 +371,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.triggerDeadzone = 0.30f;
|
context.triggerDeadzone = 0.30f;
|
||||||
}
|
}
|
||||||
// Classify this device as a remote by name
|
// Classify this device as a remote by name
|
||||||
else if (devName.toLowerCase().contains("remote")) {
|
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
|
||||||
// It's only a remote if it doesn't any sticks
|
// It's only a remote if it doesn't any sticks
|
||||||
if (!context.hasJoystickAxes) {
|
if (!context.hasJoystickAxes) {
|
||||||
context.ignoreBack = true;
|
context.ignoreBack = true;
|
||||||
@@ -448,10 +383,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.rightStickDeadzoneRadius = 0.07f;
|
context.rightStickDeadzoneRadius = 0.07f;
|
||||||
}
|
}
|
||||||
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
|
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
|
||||||
// back presses on this device. The Goodix buttons on the Nokia 6 also appear
|
// back presses on this device
|
||||||
// non-virtual so we'll ignore those too.
|
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey")) {
|
||||||
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey") ||
|
|
||||||
devName.equals("goodix_fp")) {
|
|
||||||
context.ignoreBack = true;
|
context.ignoreBack = true;
|
||||||
}
|
}
|
||||||
// The Serval has a couple of unknown buttons that are start and select. It also has
|
// The Serval has a couple of unknown buttons that are start and select. It also has
|
||||||
@@ -527,8 +460,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
private short getActiveControllerMask() {
|
private short getActiveControllerMask() {
|
||||||
if (prefConfig.multiController) {
|
if (multiControllerEnabled) {
|
||||||
return (short)(currentControllers | initialControllers);
|
return currentControllers;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Only Player 1 is active with multi-controller disabled
|
// Only Player 1 is active with multi-controller disabled
|
||||||
@@ -614,16 +547,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((changedMask & ControllerPacket.UP_FLAG) != 0) {
|
|
||||||
if ((inputMap & ControllerPacket.UP_FLAG) != 0) {
|
|
||||||
conn.sendMouseScroll((byte) 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((changedMask & ControllerPacket.DOWN_FLAG) != 0) {
|
|
||||||
if ((inputMap & ControllerPacket.DOWN_FLAG) != 0) {
|
|
||||||
conn.sendMouseScroll((byte) -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
|
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
|
||||||
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
|
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
|
||||||
@@ -990,8 +913,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// Make sure it's real by checking that the key is actually down before taking
|
// Make sure it's real by checking that the key is actually down before taking
|
||||||
// any action.
|
// any action.
|
||||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||||
SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS &&
|
SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS) {
|
||||||
prefConfig.mouseEmulation) {
|
|
||||||
toggleMouseEmulation(context);
|
toggleMouseEmulation(context);
|
||||||
}
|
}
|
||||||
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||||
@@ -1040,17 +962,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
if (context.leftTriggerAxis >= 0) {
|
|
||||||
// Suppress this digital event if an analog trigger is present
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
context.leftTrigger = 0;
|
context.leftTrigger = 0;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
if (context.rightTriggerAxis >= 0) {
|
|
||||||
// Suppress this digital event if an analog trigger is present
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
context.rightTrigger = 0;
|
context.rightTrigger = 0;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -1160,17 +1074,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
if (context.leftTriggerAxis >= 0) {
|
|
||||||
// Suppress this digital event if an analog trigger is present
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
context.leftTrigger = (byte)0xFF;
|
context.leftTrigger = (byte)0xFF;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
if (context.rightTriggerAxis >= 0) {
|
|
||||||
// Suppress this digital event if an analog trigger is present
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
context.rightTrigger = (byte)0xFF;
|
context.rightTrigger = (byte)0xFF;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to translate a Android key code into the codes GFE is expecting
|
* Class to translate a Android key code into the codes GFE is expecting
|
||||||
* @author Diego Waxemberg
|
* @author Diego Waxemberg
|
||||||
@@ -18,15 +20,22 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_9 = 57;
|
public static final int VK_9 = 57;
|
||||||
public static final int VK_A = 65;
|
public static final int VK_A = 65;
|
||||||
public static final int VK_Z = 90;
|
public static final int VK_Z = 90;
|
||||||
|
public static final int VK_ALT = 18;
|
||||||
public static final int VK_NUMPAD0 = 96;
|
public static final int VK_NUMPAD0 = 96;
|
||||||
public static final int VK_BACK_SLASH = 92;
|
public static final int VK_BACK_SLASH = 92;
|
||||||
public static final int VK_CAPS_LOCK = 20;
|
public static final int VK_CAPS_LOCK = 20;
|
||||||
public static final int VK_CLEAR = 12;
|
public static final int VK_CLEAR = 12;
|
||||||
public static final int VK_COMMA = 44;
|
public static final int VK_COMMA = 44;
|
||||||
|
public static final int VK_CONTROL = 17;
|
||||||
public static final int VK_BACK_SPACE = 8;
|
public static final int VK_BACK_SPACE = 8;
|
||||||
public static final int VK_EQUALS = 61;
|
public static final int VK_EQUALS = 61;
|
||||||
public static final int VK_ESCAPE = 27;
|
public static final int VK_ESCAPE = 27;
|
||||||
public static final int VK_F1 = 112;
|
public static final int VK_F1 = 112;
|
||||||
|
public static final int VK_PERIOD = 46;
|
||||||
|
public static final int VK_INSERT = 155;
|
||||||
|
public static final int VK_OPEN_BRACKET = 91;
|
||||||
|
public static final int VK_WINDOWS = 524;
|
||||||
|
public static final int VK_MINUS = 45;
|
||||||
public static final int VK_END = 35;
|
public static final int VK_END = 35;
|
||||||
public static final int VK_HOME = 36;
|
public static final int VK_HOME = 36;
|
||||||
public static final int VK_NUM_LOCK = 144;
|
public static final int VK_NUM_LOCK = 144;
|
||||||
@@ -36,6 +45,7 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_CLOSE_BRACKET = 93;
|
public static final int VK_CLOSE_BRACKET = 93;
|
||||||
public static final int VK_SCROLL_LOCK = 145;
|
public static final int VK_SCROLL_LOCK = 145;
|
||||||
public static final int VK_SEMICOLON = 59;
|
public static final int VK_SEMICOLON = 59;
|
||||||
|
public static final int VK_SHIFT = 16;
|
||||||
public static final int VK_SLASH = 47;
|
public static final int VK_SLASH = 47;
|
||||||
public static final int VK_SPACE = 32;
|
public static final int VK_SPACE = 32;
|
||||||
public static final int VK_PRINTSCREEN = 154;
|
public static final int VK_PRINTSCREEN = 154;
|
||||||
@@ -56,9 +66,10 @@ public class KeyboardTranslator {
|
|||||||
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
|
/* There seems to be no clean mapping between Android key codes
|
||||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
* and what Nvidia sends over the wire. If someone finds one,
|
||||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
* I'll happily delete this code :)
|
||||||
|
*/
|
||||||
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;
|
||||||
@@ -78,11 +89,8 @@ public class KeyboardTranslator {
|
|||||||
else {
|
else {
|
||||||
switch (keycode) {
|
switch (keycode) {
|
||||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||||
translated = 0xA4;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||||
translated = 0xA5;
|
translated = VK_ALT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_BACKSLASH:
|
case KeyEvent.KEYCODE_BACKSLASH:
|
||||||
@@ -102,11 +110,8 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||||
translated = 0xA2;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||||
translated = 0xA3;
|
translated = VK_CONTROL;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DEL:
|
case KeyEvent.KEYCODE_DEL:
|
||||||
@@ -126,25 +131,23 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||||
translated = 0x2e;
|
// Nvidia maps period to delete
|
||||||
|
translated = VK_PERIOD;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_INSERT:
|
case KeyEvent.KEYCODE_INSERT:
|
||||||
translated = 0x2d;
|
translated = -1;
|
||||||
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;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_META_RIGHT:
|
case KeyEvent.KEYCODE_META_RIGHT:
|
||||||
translated = 0x5c;
|
translated = VK_WINDOWS;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_MINUS:
|
case KeyEvent.KEYCODE_MINUS:
|
||||||
translated = 0xbd;
|
translated = 0xbd;
|
||||||
break;
|
break;
|
||||||
@@ -186,11 +189,8 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||||
translated = 0xA0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||||
translated = 0xA1;
|
translated = VK_SHIFT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SLASH:
|
case KeyEvent.KEYCODE_SLASH:
|
||||||
@@ -237,26 +237,6 @@ public class KeyboardTranslator {
|
|||||||
case KeyEvent.KEYCODE_BREAK:
|
case KeyEvent.KEYCODE_BREAK:
|
||||||
translated = VK_PAUSE;
|
translated = VK_PAUSE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
|
||||||
translated = 0x6F;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
|
||||||
translated = 0x6A;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
|
||||||
translated = 0x6D;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
|
||||||
translated = 0x6B;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
|
||||||
translated = 0x6E;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
System.out.println("No key for "+keycode);
|
System.out.println("No key for "+keycode);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
public class AndroidCaptureProvider extends InputCaptureProvider {
|
||||||
private ViewGroup rootViewGroup;
|
private ViewGroup rootViewGroup;
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
public AndroidPointerIconCaptureProvider(Activity activity) {
|
public AndroidCaptureProvider(Activity activity) {
|
||||||
this.context = activity;
|
this.context = activity;
|
||||||
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||||
}
|
}
|
||||||
@@ -33,13 +33,11 @@ public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableCapture() {
|
public void enableCapture() {
|
||||||
super.enableCapture();
|
|
||||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableCapture() {
|
public void disableCapture() {
|
||||||
super.disableCapture();
|
|
||||||
setPointerIconOnAllViews(null);
|
setPointerIconOnAllViews(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
|
||||||
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
|
||||||
|
|
||||||
private View targetView;
|
|
||||||
|
|
||||||
public AndroidNativePointerCaptureProvider(View targetView) {
|
|
||||||
this.targetView = targetView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enableCapture() {
|
|
||||||
super.enableCapture();
|
|
||||||
targetView.requestPointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {
|
|
||||||
super.disableCapture();
|
|
||||||
targetView.releasePointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCapturingActive() {
|
|
||||||
return targetView.hasPointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
return event.getX();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
return event.getY();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,32 +3,25 @@ package com.limelight.binding.input.capture;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.LimelightBuildProps;
|
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
|
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
|
||||||
public class InputCaptureManager {
|
public class InputCaptureManager {
|
||||||
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||||
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
// Shield capture is preferred because it can capture when the cursor is over
|
||||||
LimeLog.info("Using Android O+ native mouse capture");
|
// the system UI. Android N native capture can only capture over views owned
|
||||||
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
// by the application.
|
||||||
}
|
if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||||
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
|
|
||||||
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
|
|
||||||
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
|
|
||||||
LimeLog.info("Using NVIDIA mouse capture extension");
|
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||||
return new ShieldCaptureProvider(activity);
|
return new ShieldCaptureProvider(activity);
|
||||||
}
|
}
|
||||||
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
|
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
|
||||||
LimeLog.info("Using Evdev mouse capture");
|
LimeLog.info("Using Evdev mouse capture");
|
||||||
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
|
return new EvdevCaptureProvider(activity, rootListener);
|
||||||
}
|
}
|
||||||
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
|
||||||
// Android N's native capture can't capture over system UI elements
|
LimeLog.info("Using Android N+ native mouse capture");
|
||||||
// so we want to only use it if there's no other option.
|
return new AndroidCaptureProvider(activity);
|
||||||
LimeLog.info("Using Android N+ pointer hiding");
|
|
||||||
return new AndroidPointerIconCaptureProvider(activity);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LimeLog.info("Mouse capture not available");
|
LimeLog.info("Mouse capture not available");
|
||||||
|
|||||||
@@ -3,25 +3,10 @@ package com.limelight.binding.input.capture;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
public abstract class InputCaptureProvider {
|
public abstract class InputCaptureProvider {
|
||||||
protected boolean isCapturing;
|
public abstract void enableCapture();
|
||||||
|
public abstract void disableCapture();
|
||||||
public void enableCapture() {
|
|
||||||
isCapturing = true;
|
|
||||||
}
|
|
||||||
public void disableCapture() {
|
|
||||||
isCapturing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void destroy() {}
|
public void destroy() {}
|
||||||
|
|
||||||
public boolean isCapturingEnabled() {
|
|
||||||
return isCapturing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCapturingActive() {
|
|
||||||
return isCapturing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
package com.limelight.binding.input.capture;
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
|
||||||
public class NullCaptureProvider extends InputCaptureProvider {}
|
public class NullCaptureProvider extends InputCaptureProvider {
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {}
|
||||||
|
}
|
||||||
@@ -63,13 +63,11 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableCapture() {
|
public void enableCapture() {
|
||||||
super.enableCapture();
|
|
||||||
setCursorVisibility(false);
|
setCursorVisibility(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableCapture() {
|
public void disableCapture() {
|
||||||
super.disableCapture();
|
|
||||||
setCursorVisibility(true);
|
setCursorVisibility(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,10 @@ import android.hardware.usb.UsbDeviceConnection;
|
|||||||
import android.hardware.usb.UsbManager;
|
import android.hardware.usb.UsbManager;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@@ -28,7 +24,6 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
"com.limelight.USB_PERMISSION";
|
"com.limelight.USB_PERMISSION";
|
||||||
|
|
||||||
private UsbManager usbManager;
|
private UsbManager usbManager;
|
||||||
private PreferenceConfiguration prefConfig;
|
|
||||||
|
|
||||||
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
||||||
private final UsbDriverBinder binder = new UsbDriverBinder();
|
private final UsbDriverBinder binder = new UsbDriverBinder();
|
||||||
@@ -77,22 +72,10 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Initial attachment broadcast
|
// Initial attachment broadcast
|
||||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||||
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
// shouldClaimDevice() looks at the kernel's enumerated input
|
// Continue the state machine
|
||||||
// devices to make its decision about whether to prompt to take
|
handleUsbDeviceState(device);
|
||||||
// control of the device. The kernel bringing up the input stack
|
|
||||||
// may race with this callback and cause us to prompt when the
|
|
||||||
// kernel is capable of running the device. Let's post a delayed
|
|
||||||
// message to process this state change to allow the kernel
|
|
||||||
// some time to bring up the stack.
|
|
||||||
new Handler().postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Continue the state machine
|
|
||||||
handleUsbDeviceState(device);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
// Subsequent permission dialog completion intent
|
// Subsequent permission dialog completion intent
|
||||||
else if (action.equals(ACTION_USB_PERMISSION)) {
|
else if (action.equals(ACTION_USB_PERMISSION)) {
|
||||||
@@ -121,21 +104,11 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
private void handleUsbDeviceState(UsbDevice device) {
|
private void handleUsbDeviceState(UsbDevice device) {
|
||||||
// Are we able to operate it?
|
// Are we able to operate it?
|
||||||
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
|
if (shouldClaimDevice(device)) {
|
||||||
// Do we have permission yet?
|
// Do we have permission yet?
|
||||||
if (!usbManager.hasPermission(device)) {
|
if (!usbManager.hasPermission(device)) {
|
||||||
// Let's ask for permission
|
// Let's ask for permission
|
||||||
try {
|
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
||||||
// This function is not documented as throwing any exceptions (denying access
|
|
||||||
// is indicated by calling the PendingIntent with a false result). However,
|
|
||||||
// Samsung Knox has some policies which block this request, but rather than
|
|
||||||
// just returning a false result or returning 0 enumerated devices,
|
|
||||||
// they throw an undocumented SecurityException from this call, crashing
|
|
||||||
// the whole app. :(
|
|
||||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,17 +144,13 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isRecognizedInputDevice(UsbDevice device) {
|
private boolean isRecognizedInputDevice(UsbDevice device) {
|
||||||
// On KitKat and later, we can determine if this VID and PID combo
|
// On KitKat and later, we can determine if this VID and PID combo
|
||||||
// matches an existing input device and defer to the built-in controller
|
// matches an existing input device and defer to the built-in controller
|
||||||
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
for (int id : InputDevice.getDeviceIds()) {
|
for (int id : InputDevice.getDeviceIds()) {
|
||||||
InputDevice inputDev = InputDevice.getDevice(id);
|
InputDevice inputDev = InputDevice.getDevice(id);
|
||||||
if (inputDev == null) {
|
|
||||||
// Device was removed while looping
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputDev.getVendorId() == device.getVendorId() &&
|
if (inputDev.getVendorId() == device.getVendorId() &&
|
||||||
inputDev.getProductId() == device.getProductId()) {
|
inputDev.getProductId() == device.getProductId()) {
|
||||||
@@ -196,17 +165,16 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
private boolean shouldClaimDevice(UsbDevice device) {
|
||||||
// We always bind to XB1 controllers but only bind to XB360 controllers
|
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||||
// if we know the kernel isn't already driving this device.
|
// if we know the kernel isn't already driving this device.
|
||||||
return XboxOneController.canClaimDevice(device) ||
|
return XboxOneController.canClaimDevice(device) ||
|
||||||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
(!isRecognizedInputDevice(device) && Xbox360Controller.canClaimDevice(device));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||||
this.prefConfig = PreferenceConfiguration.readPreferences(this);
|
|
||||||
|
|
||||||
// Register for USB attach broadcasts and permission completions
|
// Register for USB attach broadcasts and permission completions
|
||||||
IntentFilter filter = new IntentFilter();
|
IntentFilter filter = new IntentFilter();
|
||||||
@@ -216,7 +184,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Enumerate existing devices
|
// Enumerate existing devices
|
||||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||||
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
|
if (shouldClaimDevice(dev)) {
|
||||||
// Start the process of claiming this device
|
// Start the process of claiming this device
|
||||||
handleUsbDeviceState(dev);
|
handleUsbDeviceState(dev);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,22 +17,18 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
0x044f, // Thrustmaster
|
0x044f, // Thrustmaster
|
||||||
0x045e, // Microsoft
|
0x045e, // Microsoft
|
||||||
0x046d, // Logitech
|
0x046d, // Logitech
|
||||||
0x056e, // Elecom
|
|
||||||
0x06a3, // Saitek
|
|
||||||
0x0738, // Mad Catz
|
0x0738, // Mad Catz
|
||||||
0x07ff, // Mad Catz
|
|
||||||
0x0e6f, // Unknown
|
0x0e6f, // Unknown
|
||||||
0x0f0d, // Hori
|
|
||||||
0x11c9, // Nacon
|
|
||||||
0x12ab, // Unknown
|
0x12ab, // Unknown
|
||||||
0x1430, // RedOctane
|
0x1430, // RedOctane
|
||||||
0x146b, // BigBen
|
0x146b, // BigBen
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x24c6, // PowerA
|
||||||
0x1532, // Razer Sabertooth
|
0x1532, // Razer Sabertooth
|
||||||
0x15e4, // Numark
|
0x15e4, // Numark
|
||||||
0x162e, // Joytech
|
0x162e, // Joytech
|
||||||
0x1689, // Razer Onza
|
|
||||||
0x1bad, // Harmonix
|
|
||||||
0x24c6, // PowerA
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
0x0738, // Mad Catz
|
0x0738, // Mad Catz
|
||||||
0x0e6f, // Unknown
|
0x0e6f, // Unknown
|
||||||
0x0f0d, // Hori
|
0x0f0d, // Hori
|
||||||
0x1532, // Razer Wildcat
|
|
||||||
0x24c6, // PowerA
|
0x24c6, // PowerA
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.os.Looper;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
|
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
@@ -185,6 +186,10 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
|||||||
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return LimelightBuildProps.ROOT_BUILD;
|
||||||
|
}
|
||||||
|
|
||||||
private void reportDeviceNotRooted() {
|
private void reportDeviceNotRooted() {
|
||||||
activity.runOnUiThread(new Runnable() {
|
activity.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -216,7 +221,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableCapture() {
|
public void enableCapture() {
|
||||||
super.enableCapture();
|
|
||||||
if (!started) {
|
if (!started) {
|
||||||
// Start the handler thread if it's our first time
|
// Start the handler thread if it's our first time
|
||||||
// capturing
|
// capturing
|
||||||
@@ -243,7 +247,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableCapture() {
|
public void disableCapture() {
|
||||||
super.disableCapture();
|
|
||||||
// This may be called on the main thread
|
// This may be called on the main thread
|
||||||
runInNetworkSafeContextSynchronously(new Runnable() {
|
runInNetworkSafeContextSynchronously(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.limelight.LimelightBuildProps;
|
|
||||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
|
||||||
|
|
||||||
public class EvdevCaptureProviderShim {
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return LimelightBuildProps.ROOT_BUILD;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to construct our capture provider using reflection because it isn't included in non-root builds
|
|
||||||
public static InputCaptureProvider createEvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
|
||||||
try {
|
|
||||||
Class providerClass = Class.forName("com.limelight.binding.input.evdev.EvdevCaptureProvider");
|
|
||||||
return (InputCaptureProvider) providerClass.getConstructors()[0].newInstance(activity, listener);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
public interface EvdevListener {
|
public interface EvdevListener {
|
||||||
int BUTTON_LEFT = 1;
|
public static final int BUTTON_LEFT = 1;
|
||||||
int BUTTON_MIDDLE = 2;
|
public static final int BUTTON_MIDDLE = 2;
|
||||||
int BUTTON_RIGHT = 3;
|
public static final int BUTTON_RIGHT = 3;
|
||||||
|
|
||||||
void mouseMove(int deltaX, int deltaY);
|
public void mouseMove(int deltaX, int deltaY);
|
||||||
void mouseButtonEvent(int buttonId, boolean down);
|
public void mouseButtonEvent(int buttonId, boolean down);
|
||||||
void mouseScroll(byte amount);
|
public void mouseScroll(byte amount);
|
||||||
void keyboardEvent(boolean buttonDown, short keyCode);
|
public void keyboardEvent(boolean buttonDown, short keyCode);
|
||||||
}
|
}
|
||||||
@@ -164,8 +164,8 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public AnalogStick(VirtualController controller, Context context, int elementId) {
|
public AnalogStick(VirtualController controller, Context context) {
|
||||||
super(controller, context, elementId);
|
super(controller, context);
|
||||||
// reset stick position
|
// reset stick position
|
||||||
position_stick_x = getWidth() / 2;
|
position_stick_x = getWidth() / 2;
|
||||||
position_stick_y = getHeight() / 2;
|
position_stick_y = getHeight() / 2;
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ public class DigitalButton extends VirtualControllerElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DigitalButton(VirtualController controller, int elementId, int layer, Context context) {
|
public DigitalButton(VirtualController controller, int layer, Context context) {
|
||||||
super(controller, context, elementId);
|
super(controller, context);
|
||||||
this.layer = layer;
|
this.layer = layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class DigitalPad extends VirtualControllerElement {
|
|||||||
private final Paint paint = new Paint();
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
public DigitalPad(VirtualController controller, Context context) {
|
public DigitalPad(VirtualController controller, Context context) {
|
||||||
super(controller, context, EID_DPAD);
|
super(controller, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addDigitalPadListener(DigitalPadListener listener) {
|
public void addDigitalPadListener(DigitalPadListener listener) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.limelight.nvstream.input.ControllerPacket;
|
|||||||
|
|
||||||
public class LeftAnalogStick extends AnalogStick {
|
public class LeftAnalogStick extends AnalogStick {
|
||||||
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
||||||
super(controller, context, EID_LS);
|
super(controller, context);
|
||||||
|
|
||||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import android.content.Context;
|
|||||||
|
|
||||||
public class LeftTrigger extends DigitalButton {
|
public class LeftTrigger extends DigitalButton {
|
||||||
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
super(controller, EID_LT, layer, context);
|
super(controller, layer, context);
|
||||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.limelight.nvstream.input.ControllerPacket;
|
|||||||
|
|
||||||
public class RightAnalogStick extends AnalogStick {
|
public class RightAnalogStick extends AnalogStick {
|
||||||
public RightAnalogStick(final VirtualController controller, final Context context) {
|
public RightAnalogStick(final VirtualController controller, final Context context) {
|
||||||
super(controller, context, EID_RS);
|
super(controller, context);
|
||||||
|
|
||||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import android.content.Context;
|
|||||||
|
|
||||||
public class RightTrigger extends DigitalButton {
|
public class RightTrigger extends DigitalButton {
|
||||||
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
super(controller, EID_RT, layer, context);
|
super(controller, layer, context);
|
||||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class VirtualController {
|
|||||||
ControllerMode currentMode = ControllerMode.Active;
|
ControllerMode currentMode = ControllerMode.Active;
|
||||||
ControllerInputContext inputContext = new ControllerInputContext();
|
ControllerInputContext inputContext = new ControllerInputContext();
|
||||||
|
|
||||||
|
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
||||||
private Button buttonConfigure = null;
|
private Button buttonConfigure = null;
|
||||||
|
|
||||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||||
@@ -59,7 +60,6 @@ public class VirtualController {
|
|||||||
frame_layout.addView(relative_layout);
|
frame_layout.addView(relative_layout);
|
||||||
|
|
||||||
buttonConfigure = new Button(context);
|
buttonConfigure = new Button(context);
|
||||||
buttonConfigure.setAlpha(0.25f);
|
|
||||||
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -68,7 +68,6 @@ public class VirtualController {
|
|||||||
|
|
||||||
if (currentMode == ControllerMode.Configuration) {
|
if (currentMode == ControllerMode.Configuration) {
|
||||||
currentMode = ControllerMode.Active;
|
currentMode = ControllerMode.Active;
|
||||||
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
|
||||||
message = "Exiting configuration mode";
|
message = "Exiting configuration mode";
|
||||||
} else {
|
} else {
|
||||||
currentMode = ControllerMode.Configuration;
|
currentMode = ControllerMode.Configuration;
|
||||||
@@ -117,19 +116,11 @@ public class VirtualController {
|
|||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
int buttonSize = (int)(screen.heightPixels*0.06f);
|
int buttonSize = (int)(screen.heightPixels*0.05f);
|
||||||
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
layoutParamsButtonConfigure = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
||||||
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
|
relative_layout.addView(buttonConfigure, layoutParamsButtonConfigure);
|
||||||
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
|
|
||||||
params.leftMargin = 15;
|
|
||||||
params.topMargin = 15;
|
|
||||||
relative_layout.addView(buttonConfigure, params);
|
|
||||||
|
|
||||||
// Start with the default layout
|
|
||||||
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
||||||
|
|
||||||
// Apply user preferences onto the default layout
|
|
||||||
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ControllerMode getControllerMode() {
|
public ControllerMode getControllerMode() {
|
||||||
|
|||||||
@@ -4,19 +4,13 @@
|
|||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public class VirtualControllerConfigurationLoader {
|
public class VirtualControllerConfigurationLoader {
|
||||||
public static final String OSC_PREFERENCE = "OSC";
|
private static final String PROFILE_PATH = "profiles";
|
||||||
|
|
||||||
private static int getPercent(
|
private static int getPercent(
|
||||||
int percent,
|
int percent,
|
||||||
@@ -64,7 +58,6 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static DigitalButton createDigitalButton(
|
private static DigitalButton createDigitalButton(
|
||||||
final int elementId,
|
|
||||||
final int keyShort,
|
final int keyShort,
|
||||||
final int keyLong,
|
final int keyLong,
|
||||||
final int layer,
|
final int layer,
|
||||||
@@ -72,7 +65,7 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
final int icon,
|
final int icon,
|
||||||
final VirtualController controller,
|
final VirtualController controller,
|
||||||
final Context context) {
|
final Context context) {
|
||||||
DigitalButton button = new DigitalButton(controller, elementId, layer, context);
|
DigitalButton button = new DigitalButton(controller, layer, context);
|
||||||
button.setText(text);
|
button.setText(text);
|
||||||
button.setIcon(icon);
|
button.setIcon(icon);
|
||||||
|
|
||||||
@@ -153,176 +146,140 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
|
||||||
|
|
||||||
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
||||||
// into a single call. Due to floating point rounding, this isn't actually possible.
|
// into a single call. Due to floating point rounding, this isn't actually possible.
|
||||||
|
|
||||||
if (!config.onlyL3R3)
|
controller.addElement(createDigitalPad(controller, context),
|
||||||
{
|
getPercent(5, screen.widthPixels),
|
||||||
controller.addElement(createDigitalPad(controller, context),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
getPercent(5, screen.widthPixels),
|
getPercent(30, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
getPercent(40, screen.heightPixels)
|
||||||
getPercent(30, screen.widthPixels),
|
);
|
||||||
getPercent(40, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_A,
|
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_B,
|
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_X,
|
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_Y,
|
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createLeftTrigger(
|
controller.addElement(createLeftTrigger(
|
||||||
0, "LT", -1, controller, context),
|
0, "LT", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createRightTrigger(
|
controller.addElement(createRightTrigger(
|
||||||
0, "RT", -1, controller, context),
|
0, "RT", -1, controller, context),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
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),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
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),
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createLeftStick(controller, context),
|
controller.addElement(createLeftStick(controller, context),
|
||||||
getPercent(5, screen.widthPixels),
|
getPercent(5, screen.widthPixels),
|
||||||
getPercent(50, screen.heightPixels),
|
getPercent(50, screen.heightPixels),
|
||||||
getPercent(40, screen.widthPixels),
|
getPercent(40, screen.widthPixels),
|
||||||
getPercent(50, screen.heightPixels)
|
getPercent(50, screen.heightPixels)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createRightStick(controller, context),
|
controller.addElement(createRightStick(controller, context),
|
||||||
getPercent(55, screen.widthPixels),
|
getPercent(55, screen.widthPixels),
|
||||||
getPercent(50, screen.heightPixels),
|
getPercent(50, screen.heightPixels),
|
||||||
getPercent(40, screen.widthPixels),
|
getPercent(40, screen.widthPixels),
|
||||||
getPercent(50, screen.heightPixels)
|
getPercent(50, screen.heightPixels)
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
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),
|
||||||
getPercent(40, screen.widthPixels),
|
getPercent(90, screen.heightPixels),
|
||||||
getPercent(90, screen.heightPixels),
|
getPercent(10, screen.widthPixels),
|
||||||
getPercent(10, screen.widthPixels),
|
getPercent(10, screen.heightPixels)
|
||||||
getPercent(10, screen.heightPixels)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
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),
|
||||||
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
|
getPercent(90, screen.heightPixels),
|
||||||
getPercent(90, screen.heightPixels),
|
getPercent(10, screen.widthPixels),
|
||||||
getPercent(10, screen.widthPixels),
|
getPercent(10, screen.heightPixels)
|
||||||
getPercent(10, screen.heightPixels)
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_LSB,
|
|
||||||
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
|
||||||
getPercent(2, screen.widthPixels),
|
|
||||||
getPercent(80, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_RSB,
|
|
||||||
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
|
||||||
getPercent(89, screen.widthPixels),
|
|
||||||
getPercent(80, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void saveProfile(final VirtualController controller,
|
/*
|
||||||
|
NOT IMPLEMENTED YET,
|
||||||
|
this should later be used to store and load a profile for the virtual controller
|
||||||
|
public static void saveProfile(final String name,
|
||||||
|
final VirtualController controller,
|
||||||
final Context context) {
|
final Context context) {
|
||||||
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
|
|
||||||
|
|
||||||
|
SharedPreferences preferences = context.getSharedPreferences(PROFILE_PATH + "/" +
|
||||||
|
name, Activity.MODE_PRIVATE);
|
||||||
|
|
||||||
|
JSONArray elementConfigurations = new JSONArray();
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
for (VirtualControllerElement element : controller.getElements()) {
|
||||||
String prefKey = ""+element.elementId;
|
JSONObject elementConfiguration = new JSONObject();
|
||||||
try {
|
try {
|
||||||
prefEditor.putString(prefKey, element.getConfiguration().toString());
|
elementConfiguration.put("TYPE", element.getClass().getName());
|
||||||
} catch (JSONException e) {
|
elementConfiguration.put("CONFIGURATION", element.getConfiguration());
|
||||||
|
elementConfigurations.put(elementConfiguration);
|
||||||
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prefEditor.apply();
|
SharedPreferences.Editor editor= preferences.edit();
|
||||||
|
editor.putString("ELEMENTS", elementConfigurations.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
public static void loadFromPreferences(final VirtualController controller) {
|
||||||
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
|
||||||
|
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
|
||||||
String prefKey = ""+element.elementId;
|
|
||||||
|
|
||||||
String jsonConfig = pref.getString(prefKey, null);
|
|
||||||
if (jsonConfig != null) {
|
|
||||||
try {
|
|
||||||
element.loadConfiguration(new JSONObject(jsonConfig));
|
|
||||||
} catch (JSONException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
|
|
||||||
// Remove the corrupt element from the preferences
|
|
||||||
pref.edit().remove(prefKey).apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
@@ -14,30 +14,10 @@ import android.view.MotionEvent;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public abstract class VirtualControllerElement extends View {
|
public abstract class VirtualControllerElement extends View {
|
||||||
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
public static final int EID_DPAD = 1;
|
|
||||||
public static final int EID_LT = 2;
|
|
||||||
public static final int EID_RT = 3;
|
|
||||||
public static final int EID_LB = 4;
|
|
||||||
public static final int EID_RB = 5;
|
|
||||||
public static final int EID_A = 6;
|
|
||||||
public static final int EID_B = 7;
|
|
||||||
public static final int EID_X = 8;
|
|
||||||
public static final int EID_Y = 9;
|
|
||||||
public static final int EID_BACK = 10;
|
|
||||||
public static final int EID_START = 11;
|
|
||||||
public static final int EID_LS = 12;
|
|
||||||
public static final int EID_RS = 13;
|
|
||||||
public static final int EID_LSB = 14;
|
|
||||||
public static final int EID_RSB = 15;
|
|
||||||
|
|
||||||
protected VirtualController virtualController;
|
protected VirtualController virtualController;
|
||||||
protected final int elementId;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
@@ -60,11 +40,10 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
|
|
||||||
private Mode currentMode = Mode.Normal;
|
private Mode currentMode = Mode.Normal;
|
||||||
|
|
||||||
protected VirtualControllerElement(VirtualController controller, Context context, int elementId) {
|
protected VirtualControllerElement(VirtualController controller, Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
|
|
||||||
this.virtualController = controller;
|
this.virtualController = controller;
|
||||||
this.elementId = elementId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
||||||
@@ -299,28 +278,13 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
return getWidth() > getHeight() ? getHeight() : getWidth();
|
return getWidth() > getHeight() ? getHeight() : getWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
public JSONObject getConfiguration () {
|
||||||
|
JSONObject configuration = new JSONObject();
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
public JSONObject getConfiguration() throws JSONException {
|
public void loadConfiguration (JSONObject configuration) {
|
||||||
JSONObject configuration = new JSONObject();
|
}
|
||||||
|
*/
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
configuration.put("LEFT", layoutParams.leftMargin);
|
|
||||||
configuration.put("TOP", layoutParams.topMargin);
|
|
||||||
configuration.put("WIDTH", layoutParams.width);
|
|
||||||
configuration.put("HEIGHT", layoutParams.height);
|
|
||||||
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadConfiguration(JSONObject configuration) throws JSONException {
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
layoutParams.leftMargin = configuration.getInt("LEFT");
|
|
||||||
layoutParams.topMargin = configuration.getInt("TOP");
|
|
||||||
layoutParams.width = configuration.getInt("WIDTH");
|
|
||||||
layoutParams.height = configuration.getInt("HEIGHT");
|
|
||||||
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
public interface CrashListener {
|
|
||||||
void notifyCrash(Exception e);
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,9 @@ import java.util.Collections;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.ConfigurationInfo;
|
import android.content.pm.ConfigurationInfo;
|
||||||
@@ -28,18 +27,13 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
private static final List<String> blacklistedDecoderPrefixes;
|
private static final List<String> blacklistedDecoderPrefixes;
|
||||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
|
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||||
private static final List<String> deprioritizedHevcDecoders;
|
|
||||||
private static final List<String> baselineProfileHackPrefixes;
|
private static final List<String> baselineProfileHackPrefixes;
|
||||||
private static final List<String> directSubmitPrefixes;
|
private static final List<String> directSubmitPrefixes;
|
||||||
private static final List<String> constrainedHighProfilePrefixes;
|
private static final List<String> constrainedHighProfilePrefixes;
|
||||||
private static final List<String> whitelistedHevcDecoders;
|
private static final List<String> whitelistedHevcDecoders;
|
||||||
private static final List<String> refFrameInvalidationAvcPrefixes;
|
private static final List<String> refFrameInvalidationAvcPrefixes;
|
||||||
private static final List<String> refFrameInvalidationHevcPrefixes;
|
private static final List<String> refFrameInvalidationHevcPrefixes;
|
||||||
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragon = false;
|
|
||||||
private static boolean initialized = false;
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
directSubmitPrefixes = new LinkedList<>();
|
directSubmitPrefixes = new LinkedList<>();
|
||||||
@@ -69,22 +63,15 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
|
// Software decoders that don't support H264 high profile
|
||||||
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
|
|
||||||
// Blacklist software decoders that don't support H264 high profile,
|
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
|
||||||
// but exclude the official AOSP emulator from this restriction.
|
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
||||||
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
|
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Never use ffmpeg decoders since they're software decoders
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
|
|
||||||
|
|
||||||
// Force these decoders disabled because:
|
|
||||||
// 1) They are software decoders, so the performance is terrible
|
|
||||||
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@@ -98,13 +85,11 @@ public class MediaCodecHelper {
|
|||||||
baselineProfileHackPrefixes = new LinkedList<>();
|
baselineProfileHackPrefixes = new LinkedList<>();
|
||||||
baselineProfileHackPrefixes.add("omx.intel");
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
|
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
|
||||||
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
|
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||||
// if adaptive playback was enabled so let's avoid it to be safe.
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||||
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
|
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||||
// on some Android TV devices with H.265 only.
|
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
|
|
||||||
|
|
||||||
constrainedHighProfilePrefixes = new LinkedList<>();
|
constrainedHighProfilePrefixes = new LinkedList<>();
|
||||||
constrainedHighProfilePrefixes.add("omx.intel");
|
constrainedHighProfilePrefixes.add("omx.intel");
|
||||||
@@ -113,22 +98,11 @@ public class MediaCodecHelper {
|
|||||||
static {
|
static {
|
||||||
whitelistedHevcDecoders = new LinkedList<>();
|
whitelistedHevcDecoders = new LinkedList<>();
|
||||||
|
|
||||||
// Allow software HEVC decoding in the official AOSP emulator
|
|
||||||
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.google");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exynos seems to be the only HEVC decoder that works reliably
|
// Exynos seems to be the only HEVC decoder that works reliably
|
||||||
whitelistedHevcDecoders.add("omx.exynos");
|
whitelistedHevcDecoders.add("omx.exynos");
|
||||||
|
|
||||||
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
|
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
||||||
// For some reason, other X1 implementations require bitstream fixups.
|
//whitelistedHevcDecoders.add("omx.nvidia");
|
||||||
if (Build.DEVICE.equalsIgnoreCase("darcy")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.nvidia");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
||||||
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
|
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
|
||||||
@@ -146,90 +120,20 @@ public class MediaCodecHelper {
|
|||||||
// during initialization to avoid SoCs with broken HEVC decoders.
|
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
public static void initializeWithContext(Context context) {
|
||||||
deprioritizedHevcDecoders = new LinkedList<>();
|
|
||||||
|
|
||||||
// These are decoders that work but aren't used by default for various reasons.
|
|
||||||
|
|
||||||
// Qualcomm is currently the only decoders in this group.
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// We see a bunch of crashes on MediaTek Android TVs running
|
|
||||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
|
||||||
// these devices and hope they fix it in Oreo.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getAdrenoVersionString(String glRenderer) {
|
|
||||||
glRenderer = glRenderer.toLowerCase().trim();
|
|
||||||
|
|
||||||
if (!glRenderer.contains("adreno")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
|
|
||||||
|
|
||||||
Matcher matcher = modelNumberPattern.matcher(glRenderer);
|
|
||||||
if (!matcher.matches()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String modelNumber = matcher.group(2);
|
|
||||||
LimeLog.info("Found Adreno GPU: "+modelNumber);
|
|
||||||
return modelNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
|
|
||||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
|
||||||
if (modelNumber == null) {
|
|
||||||
// Not an Adreno GPU
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
|
|
||||||
return modelNumber.charAt(1) == '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a workaround for some broken devices that report
|
|
||||||
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
|
|
||||||
// An example of such a device is the Huawei Honor 5x with the
|
|
||||||
// Snapdragon 616 SoC (Adreno 405).
|
|
||||||
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
|
|
||||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
|
||||||
if (modelNumber == null) {
|
|
||||||
// Not an Adreno GPU
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapdragon 4xx and higher support GLES 3.1
|
|
||||||
return modelNumber.charAt(0) >= '4';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initialize(Context context, String glRenderer) {
|
|
||||||
if (initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityManager activityManager =
|
ActivityManager activityManager =
|
||||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
||||||
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
||||||
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
||||||
|
|
||||||
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
|
|
||||||
|
|
||||||
// Tegra K1 and later can do reference frame invalidation properly
|
// Tegra K1 and later can do reference frame invalidation properly
|
||||||
if (configInfo.reqGlEsVersion >= 0x30000) {
|
if (configInfo.reqGlEsVersion >= 0x30000) {
|
||||||
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
||||||
|
|
||||||
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
|
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
||||||
|
|
||||||
// Prior to M, we were tricking the decoder into using baseline profile, which
|
// Prior to M, we were tricking the decoder into using baseline profile, which
|
||||||
// won't support RFI properly.
|
// won't support RFI properly.
|
||||||
@@ -237,37 +141,26 @@ public class MediaCodecHelper {
|
|||||||
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
||||||
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
||||||
// 3xx - bad
|
// 3xx - bad
|
||||||
// 4xx - good
|
// 4xx - good
|
||||||
//
|
//
|
||||||
// The "good" GPUs support GLES 3.1, but we can't just check that directly
|
// Unfortunately, it's not that easy to get that information here, so I'll use an
|
||||||
// (see comment on isGLES31SnapdragonRenderer).
|
// approximation by checking the GLES level (<= 3.0 is bad).
|
||||||
//
|
if (configInfo.reqGlEsVersion > 0x30000) {
|
||||||
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
// FIXME: We prefer reference frame invalidation support (which is only doable on AVC on
|
||||||
// We prefer reference frame invalidation support (which is only doable on AVC on
|
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
||||||
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
// to force HEVC on.
|
||||||
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
|
//LimeLog.info("Added omx.qcom to supported HEVC decoders based on GLES 3.1+ support");
|
||||||
// HEVC anyway.
|
//whitelistedHevcDecoders.add("omx.qcom");
|
||||||
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
|
}
|
||||||
deprioritizedHevcDecoders.add("omx.qcom");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
if (!initialized) {
|
|
||||||
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String badPrefix : decoderList) {
|
for (String badPrefix : decoderList) {
|
||||||
if (decoderName.length() >= badPrefix.length()) {
|
if (decoderName.length() >= badPrefix.length()) {
|
||||||
String prefix = decoderName.substring(0, badPrefix.length());
|
String prefix = decoderName.substring(0, badPrefix.length());
|
||||||
@@ -283,15 +176,20 @@ public class MediaCodecHelper {
|
|||||||
public static long getMonotonicMillis() {
|
public static long getMonotonicMillis() {
|
||||||
return System.nanoTime() / 1000000L;
|
return System.nanoTime() / 1000000L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
|
||||||
|
/*
|
||||||
|
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
||||||
|
so we'll keep it off for now, since we don't know whether other devices also do the same
|
||||||
|
|
||||||
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
|
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||||
|
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Possibly enable adaptive playback on KitKat and above
|
// Possibly enable adaptive playback on KitKat and above
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
|
|
||||||
LimeLog.info("Decoder blacklisted for adaptive playback");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (decoderInfo.getCapabilitiesForType("video/avc").
|
if (decoderInfo.getCapabilitiesForType("video/avc").
|
||||||
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
||||||
@@ -303,7 +201,7 @@ public class MediaCodecHelper {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Tolerate buggy codecs
|
// Tolerate buggy codecs
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -324,22 +222,7 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderBlacklistedFor49Fps(String decoderName) {
|
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName) {
|
||||||
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
|
||||||
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
|
||||||
if (videoHeight > 720 && isLowEndSnapdragon) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This device seems to crash constantly at 720p, so try disabling
|
|
||||||
// RFI to see if we can get that under control.
|
|
||||||
if (Build.PRODUCT.equalsIgnoreCase("b3_att_us")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +230,7 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
|
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
|
||||||
// TODO: Shield Tablet K1/LTE?
|
// TODO: Shield Tablet K1/LTE?
|
||||||
//
|
//
|
||||||
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
||||||
@@ -379,15 +262,6 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some devices have HEVC decoders that we prefer not to use
|
|
||||||
// typically because it can't support reference frame invalidation.
|
|
||||||
// However, we will use it for HDR and for streaming over mobile networks
|
|
||||||
// since it works fine otherwise.
|
|
||||||
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
|
||||||
LimeLog.info("Selected deprioritized decoder");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,10 +309,6 @@ public class MediaCodecHelper {
|
|||||||
// This is a different algorithm than the other findXXXDecoder functions,
|
// This is a different algorithm than the other findXXXDecoder functions,
|
||||||
// because we want to evaluate the decoders in our list's order
|
// because we want to evaluate the decoders in our list's order
|
||||||
// rather than MediaCodecList's order
|
// rather than MediaCodecList's order
|
||||||
|
|
||||||
if (!initialized) {
|
|
||||||
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String preferredDecoder : preferredDecoders) {
|
for (String preferredDecoder : preferredDecoders) {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ public class ComputerDatabaseManager {
|
|||||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||||
private static final String MAC_COLUMN_NAME = "Mac";
|
private static final String MAC_COLUMN_NAME = "Mac";
|
||||||
|
|
||||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
public ComputerDatabaseManager(Context c) {
|
public ComputerDatabaseManager(Context c) {
|
||||||
@@ -62,74 +60,50 @@ public class ComputerDatabaseManager {
|
|||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||||
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
|
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
||||||
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
|
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
||||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
|
|
||||||
details.name = c.getString(0);
|
|
||||||
|
|
||||||
String uuidStr = c.getString(1);
|
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// An earlier schema defined addresses as byte blobs. We'll
|
|
||||||
// gracefully migrate those to strings so we can store DNS names
|
|
||||||
// too. To disambiguate, we'll need to prefix them with a string
|
|
||||||
// greater than the allowable IP address length.
|
|
||||||
try {
|
|
||||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
|
||||||
LimeLog.warning("DB: Legacy local address for "+details.name);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// This is probably a hostname/address with the prefix string
|
|
||||||
String stringData = c.getString(2);
|
|
||||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
|
||||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
|
||||||
LimeLog.warning("DB: Legacy remote address for "+details.name);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// This is probably a hostname/address with the prefix string
|
|
||||||
String stringData = c.getString(3);
|
|
||||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
|
||||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ComputerDetails> getAllComputers() {
|
public List<ComputerDetails> getAllComputers() {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
|
details.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
// If a field is corrupt or missing, skip the database entry
|
// If a field is corrupt or missing, skip the database entry
|
||||||
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
details.macAddress == null) {
|
details.macAddress == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -145,17 +119,46 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
public ComputerDetails getComputerByName(String name) {
|
public ComputerDetails getComputerByName(String name) {
|
||||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
if (!c.moveToFirst()) {
|
if (!c.moveToFirst()) {
|
||||||
// No matching computer
|
// No matching computer
|
||||||
c.close();
|
c.close();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
details.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
|
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
// If a field is corrupt or missing, delete the database entry
|
// If a field is corrupt or missing, delete the database entry
|
||||||
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
details.macAddress == null) {
|
details.macAddress == null) {
|
||||||
deleteComputer(details.name);
|
deleteComputer(details.name);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package com.limelight.computers;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
public interface ComputerManagerListener {
|
public interface ComputerManagerListener {
|
||||||
void notifyComputerUpdated(ComputerDetails details);
|
public void notifyComputerUpdated(ComputerDetails details);
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import java.io.StringReader;
|
|||||||
import java.net.InetAddress;
|
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.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -21,7 +20,6 @@ import com.limelight.nvstream.http.NvHTTP;
|
|||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -37,7 +35,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||||
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
private static final int FAST_POLL_TIMEOUT = 1000;
|
private static final int FAST_POLL_TIMEOUT = 500;
|
||||||
private static final int OFFLINE_POLL_TRIES = 5;
|
private static final int OFFLINE_POLL_TRIES = 5;
|
||||||
private static final int INITIAL_POLL_TRIES = 2;
|
private static final int INITIAL_POLL_TRIES = 2;
|
||||||
private static final int EMPTY_LIST_THRESHOLD = 3;
|
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||||
@@ -134,7 +132,7 @@ public class ComputerManagerService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
int offlineCount = 0;
|
int offlineCount = 0;
|
||||||
while (!isInterrupted() && pollingActive && tuple.thread == this) {
|
while (!isInterrupted() && pollingActive) {
|
||||||
try {
|
try {
|
||||||
// Only allow one request to the machine at a time
|
// Only allow one request to the machine at a time
|
||||||
synchronized (tuple.networkLock) {
|
synchronized (tuple.networkLock) {
|
||||||
@@ -156,7 +154,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Polling thread for " + tuple.computer.localAddress);
|
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +210,8 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(String name) {
|
||||||
@@ -291,7 +289,7 @@ public class ComputerManagerService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
// Kick off a serverinfo poll on this machine
|
// Kick off a serverinfo poll on this machine
|
||||||
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
addComputerBlocking(computer.getAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -307,22 +305,15 @@ public class ComputerManagerService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTuple(ComputerDetails details, boolean manuallyAdded) {
|
private void addTuple(ComputerDetails details) {
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
// Check if this is the same computer
|
// Check if this is the same computer
|
||||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||||
if (manuallyAdded) {
|
// Update details anyway in case this machine has been re-added by IP
|
||||||
// Update details anyway in case this machine has been re-added by IP
|
// after not being reachable by our existing information
|
||||||
// after not being reachable by our existing information
|
tuple.computer.localIp = details.localIp;
|
||||||
tuple.computer.localAddress = details.localAddress;
|
tuple.computer.remoteIp = details.remoteIp;
|
||||||
tuple.computer.remoteAddress = details.remoteAddress;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// This indicates that mDNS discovered this address, so we
|
|
||||||
// should only apply the local address.
|
|
||||||
tuple.computer.localAddress = details.localAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a polling thread if polling is active
|
// Start a polling thread if polling is active
|
||||||
if (pollingActive && tuple.thread == null) {
|
if (pollingActive && tuple.thread == null) {
|
||||||
@@ -347,11 +338,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
// Setup a placeholder
|
// Setup a placeholder
|
||||||
ComputerDetails fakeDetails = new ComputerDetails();
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
fakeDetails.localAddress = addr;
|
fakeDetails.localIp = addr;
|
||||||
fakeDetails.remoteAddress = addr;
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
try {
|
try {
|
||||||
@@ -365,14 +356,10 @@ public class ComputerManagerService extends Service {
|
|||||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||||
|
|
||||||
// Start a polling thread for this machine
|
// Start a polling thread for this machine
|
||||||
addTuple(fakeDetails, manuallyAdded);
|
addTuple(fakeDetails);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!manuallyAdded) {
|
|
||||||
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +379,6 @@ public class ComputerManagerService extends Service {
|
|||||||
if (tuple.thread != null) {
|
if (tuple.thread != null) {
|
||||||
// Interrupt the thread on this entry
|
// Interrupt the thread on this entry
|
||||||
tuple.thread.interrupt();
|
tuple.thread.interrupt();
|
||||||
tuple.thread = null;
|
|
||||||
}
|
}
|
||||||
pollingTuples.remove(tuple);
|
pollingTuples.remove(tuple);
|
||||||
break;
|
break;
|
||||||
@@ -418,14 +404,14 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
|
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
||||||
// Fast poll this address first to determine if we can connect at the TCP layer
|
// Fast poll this address first to determine if we can connect at the TCP layer
|
||||||
if (!fastPollIp(address)) {
|
if (!fastPollIp(ipAddr)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
ComputerDetails newDetails = http.getComputerDetails();
|
ComputerDetails newDetails = http.getComputerDetails();
|
||||||
@@ -446,10 +432,10 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
// Just try to establish a TCP connection to speculatively detect a running
|
// Just try to establish a TCP connection to speculatively detect a running
|
||||||
// GFE server
|
// GFE server
|
||||||
private boolean fastPollIp(String address) {
|
private boolean fastPollIp(InetAddress addr) {
|
||||||
Socket s = new Socket();
|
Socket s = new Socket();
|
||||||
try {
|
try {
|
||||||
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||||
s.close();
|
s.close();
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -457,11 +443,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startFastPollThread(final String address, final boolean[] info) {
|
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean pollRes = fastPollIp(address);
|
boolean pollRes = fastPollIp(addr);
|
||||||
|
|
||||||
synchronized (info) {
|
synchronized (info) {
|
||||||
info[0] = true; // Done
|
info[0] = true; // Done
|
||||||
@@ -471,16 +457,16 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Fast Poll - "+address);
|
t.setName("Fast Poll - "+addr.getHostAddress());
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) throws InterruptedException {
|
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
||||||
final boolean[] remoteInfo = new boolean[2];
|
final boolean[] remoteInfo = new boolean[2];
|
||||||
final boolean[] localInfo = new boolean[2];
|
final boolean[] localInfo = new boolean[2];
|
||||||
|
|
||||||
startFastPollThread(localAddress, localInfo);
|
startFastPollThread(local, localInfo);
|
||||||
startFastPollThread(remoteAddress, remoteInfo);
|
startFastPollThread(remote, remoteInfo);
|
||||||
|
|
||||||
// Check local first
|
// Check local first
|
||||||
synchronized (localInfo) {
|
synchronized (localInfo) {
|
||||||
@@ -507,31 +493,19 @@ public class ComputerManagerService extends Service {
|
|||||||
return ComputerDetails.Reachability.OFFLINE;
|
return ComputerDetails.Reachability.OFFLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isAddressLikelyLocal(String str) {
|
|
||||||
try {
|
|
||||||
// This will tend to be wrong for IPv6 but falling back to
|
|
||||||
// remote will be fine in that case. For IPv4, it should be
|
|
||||||
// pretty accurate due to NAT prevalence.
|
|
||||||
InetAddress addr = InetAddress.getByName(str);
|
|
||||||
return addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
||||||
ComputerDetails polledDetails;
|
ComputerDetails polledDetails;
|
||||||
ComputerDetails.Reachability reachability;
|
ComputerDetails.Reachability reachability;
|
||||||
|
|
||||||
if (details.localAddress.equals(details.remoteAddress)) {
|
// If the local address is routable across the Internet,
|
||||||
reachability = isAddressLikelyLocal(details.localAddress) ?
|
// always consider this PC remote to be conservative
|
||||||
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
|
if (details.localIp.equals(details.remoteIp)) {
|
||||||
|
reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +")");
|
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
||||||
reachability = fastPollPc(details.localAddress, details.remoteAddress);
|
reachability = fastPollPc(details.localIp, details.remoteIp);
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
// If no connection could be established to either IP address, there's nothing we can do
|
||||||
@@ -543,46 +517,39 @@ public class ComputerManagerService extends Service {
|
|||||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
|
||||||
if (localFirst) {
|
if (localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localAddress);
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
polledDetails = tryPollIp(details, details.remoteAddress);
|
polledDetails = tryPollIp(details, details.remoteIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
String reachableAddr = null;
|
InetAddress reachableAddr = null;
|
||||||
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
|
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||||
// Failed, so let's try the fallback
|
// Failed, so let's try the fallback
|
||||||
if (!localFirst) {
|
if (!localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localAddress);
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
polledDetails = tryPollIp(details, details.remoteAddress);
|
polledDetails = tryPollIp(details, details.remoteIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (polledDetails != null) {
|
if (polledDetails != null) {
|
||||||
// The fallback poll worked
|
// The fallback poll worked
|
||||||
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
|
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (polledDetails != null) {
|
else if (polledDetails != null) {
|
||||||
reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
|
reachableAddr = localFirst ? details.localIp : details.remoteIp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reachableAddr == null) {
|
if (reachableAddr == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If both addresses are the same, guess whether we're local based on
|
if (polledDetails.remoteIp.equals(reachableAddr)) {
|
||||||
// IP address heuristics.
|
|
||||||
if (reachableAddr.equals(polledDetails.localAddress) &&
|
|
||||||
reachableAddr.equals(polledDetails.remoteAddress)) {
|
|
||||||
polledDetails.reachability = isAddressLikelyLocal(reachableAddr) ?
|
|
||||||
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
|
||||||
else if (polledDetails.remoteAddress.equals(reachableAddr)) {
|
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else if (polledDetails.localAddress.equals(reachableAddr)) {
|
else if (polledDetails.localIp.equals(reachableAddr)) {
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -604,7 +571,7 @@ public class ComputerManagerService extends Service {
|
|||||||
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
||||||
if (confirmationReachTuple == null) {
|
if (confirmationReachTuple == null) {
|
||||||
// Neither of those seem to work, so we'll hold onto the address that did work
|
// Neither of those seem to work, so we'll hold onto the address that did work
|
||||||
initialReachTuple.computer.localAddress = initialReachTuple.reachableAddress;
|
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
|
||||||
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -614,11 +581,8 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save some details about the old state of the PC that we may wish
|
// Save the old MAC address
|
||||||
// to restore later.
|
|
||||||
String savedMacAddress = details.macAddress;
|
String savedMacAddress = details.macAddress;
|
||||||
String savedLocalAddress = details.localAddress;
|
|
||||||
String savedRemoteAddress = details.remoteAddress;
|
|
||||||
|
|
||||||
// If we got here, it's reachable
|
// If we got here, it's reachable
|
||||||
details.update(initialReachTuple.computer);
|
details.update(initialReachTuple.computer);
|
||||||
@@ -629,33 +593,6 @@ public class ComputerManagerService extends Service {
|
|||||||
details.macAddress = savedMacAddress;
|
details.macAddress = savedMacAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We never want to lose IP addresses by polling server info. If we get a poll back
|
|
||||||
// where localAddress == remoteAddress but savedLocalAddress != savedRemoteAddress,
|
|
||||||
// then we've lost an address in the polling and we should restore the one that's missing.
|
|
||||||
if (details.localAddress.equals(details.remoteAddress) &&
|
|
||||||
!savedLocalAddress.equals(savedRemoteAddress)) {
|
|
||||||
if (details.localAddress.equals(savedLocalAddress)) {
|
|
||||||
// Local addresses are identical, so put the old remote address back
|
|
||||||
details.remoteAddress = savedRemoteAddress;
|
|
||||||
}
|
|
||||||
else if (details.remoteAddress.equals(savedRemoteAddress)) {
|
|
||||||
// Remote addresses are identical, so put the old local address back
|
|
||||||
details.localAddress = savedLocalAddress;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Neither IP address match. Let's restore the remote address to be safe.
|
|
||||||
details.remoteAddress = savedRemoteAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now update the reachability so the correct address is used
|
|
||||||
if (details.localAddress.equals(initialReachTuple.reachableAddress)) {
|
|
||||||
details.reachability = ComputerDetails.Reachability.LOCAL;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
details.reachability = ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,7 +616,7 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||||
// Add tuples for each computer
|
// Add tuples for each computer
|
||||||
addTuple(computer, true);
|
addTuple(computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
@@ -757,6 +694,8 @@ public class ComputerManagerService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
int emptyAppListResponses = 0;
|
int emptyAppListResponses = 0;
|
||||||
do {
|
do {
|
||||||
|
InetAddress selectedAddr;
|
||||||
|
|
||||||
// Can't poll if it's not online
|
// Can't poll if it's not online
|
||||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
@@ -770,12 +709,19 @@ public class ComputerManagerService extends Service {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
selectedAddr = computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedAddr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
||||||
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
PollingTuple tuple = getPollingTuple(computer);
|
PollingTuple tuple = getPollingTuple(computer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
|
||||||
|
|
||||||
String appList;
|
String appList;
|
||||||
if (tuple != null) {
|
if (tuple != null) {
|
||||||
// If we're polling this machine too, grab the network lock
|
// If we're polling this machine too, grab the network lock
|
||||||
@@ -841,7 +787,6 @@ public class ComputerManagerService extends Service {
|
|||||||
} while (waitPollingDelay());
|
} while (waitPollingDelay());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
thread.setName("App list polling thread for " + computer.localAddress);
|
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,10 +816,10 @@ class PollingTuple {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ReachabilityTuple {
|
class ReachabilityTuple {
|
||||||
public final String reachableAddress;
|
public final InetAddress reachableAddress;
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
|
|
||||||
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
|
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.reachableAddress = reachableAddress;
|
this.reachableAddress = reachableAddress;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
convertView = inflater.inflate(layoutId, viewGroup, false);
|
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageView imgView = convertView.findViewById(R.id.grid_image);
|
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
||||||
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
|
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
||||||
TextView txtView = convertView.findViewById(R.id.grid_text);
|
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
||||||
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner);
|
||||||
|
|
||||||
if (imgView != null) {
|
if (imgView != null) {
|
||||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ import java.io.OutputStream;
|
|||||||
|
|
||||||
public class DiskAssetLoader {
|
public class DiskAssetLoader {
|
||||||
// 5 MB
|
// 5 MB
|
||||||
private static final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
||||||
|
|
||||||
// Standard box art is 300x400
|
|
||||||
private static final int STANDARD_ASSET_WIDTH = 300;
|
|
||||||
private static final int STANDARD_ASSET_HEIGHT = 400;
|
|
||||||
|
|
||||||
private final File cacheDir;
|
private final File cacheDir;
|
||||||
|
|
||||||
@@ -29,65 +25,33 @@ public class DiskAssetLoader {
|
|||||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
InputStream in = null;
|
||||||
// Raw height and width of image
|
Bitmap bmp = null;
|
||||||
final int height = options.outHeight;
|
try {
|
||||||
final int width = options.outWidth;
|
// Make sure the cached asset doesn't exceed the maximum size
|
||||||
int inSampleSize = 1;
|
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
||||||
|
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
||||||
|
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (height > reqHeight || width > reqWidth) {
|
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
final int halfHeight = height / 2;
|
options.inSampleSize = sampleSize;
|
||||||
final int halfWidth = width / 2;
|
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||||
|
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||||
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
|
} catch (IOException ignored) {
|
||||||
// height and width larger than the requested height and width.
|
} finally {
|
||||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
if (in != null) {
|
||||||
inSampleSize *= 2;
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return inSampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
|
||||||
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
|
|
||||||
// Don't bother with anything if it doesn't exist
|
|
||||||
if (!file.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the cached asset doesn't exceed the maximum size
|
|
||||||
if (file.length() > MAX_ASSET_SIZE) {
|
|
||||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
|
||||||
file.delete();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup bounds of the downloaded image
|
|
||||||
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
|
|
||||||
decodeOnlyOptions.inJustDecodeBounds = true;
|
|
||||||
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
|
|
||||||
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
|
|
||||||
// Dimensions set to -1 on error. Return value always null.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
|
|
||||||
|
|
||||||
// Load the image scaled to the appropriate size
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
|
|
||||||
STANDARD_ASSET_WIDTH / sampleSize,
|
|
||||||
STANDARD_ASSET_HEIGHT / sampleSize);
|
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
|
||||||
options.inDither = true;
|
|
||||||
Bitmap 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("Disk cache hit for tuple: "+tuple);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bmp;
|
return bmp;
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import android.content.Context;
|
|||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
public class NetworkAssetLoader {
|
public class NetworkAssetLoader {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@@ -20,9 +21,10 @@ public class NetworkAssetLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||||
|
|
||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
|
||||||
in = http.getBoxArt(tuple.app);
|
in = http.getBoxArt(tuple.app);
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
@@ -35,4 +37,13 @@ public class NetworkAssetLoader {
|
|||||||
|
|
||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
return computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return computer.remoteIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InterfaceAddress;
|
|
||||||
import java.net.NetworkInterface;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Collections;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -21,6 +17,7 @@ import android.content.ComponentName;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@@ -46,84 +43,42 @@ public class AddComputerManually extends Activity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private boolean isWrongSubnetSiteLocalAddress(String address) {
|
|
||||||
try {
|
|
||||||
InetAddress targetAddress = InetAddress.getByName(address);
|
|
||||||
if (!(targetAddress instanceof Inet4Address) || !targetAddress.isSiteLocalAddress()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a site-local address. Look for a matching local interface.
|
|
||||||
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
|
||||||
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
|
|
||||||
if (!(addr.getAddress() instanceof Inet4Address) || !addr.getAddress().isSiteLocalAddress()) {
|
|
||||||
// Skip non-site-local or non-IPv4 addresses
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] targetAddrBytes = targetAddress.getAddress();
|
|
||||||
byte[] ifaceAddrBytes = addr.getAddress().getAddress();
|
|
||||||
|
|
||||||
// Compare prefix to ensure it's the same
|
|
||||||
boolean addressMatches = true;
|
|
||||||
for (int i = 0; i < addr.getNetworkPrefixLength(); i++) {
|
|
||||||
if ((ifaceAddrBytes[i / 8] & (1 << (i % 8))) != (targetAddrBytes[i / 8] & (1 << (i % 8)))) {
|
|
||||||
addressMatches = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addressMatches) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couldn't find a matching interface
|
|
||||||
return true;
|
|
||||||
} catch (SocketException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doAddPc(String host) {
|
private void doAddPc(String host) {
|
||||||
String msg;
|
String msg;
|
||||||
boolean wrongSiteLocal = false;
|
boolean finish = false;
|
||||||
boolean success;
|
|
||||||
|
|
||||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||||
getResources().getString(R.string.msg_add_pc), false);
|
getResources().getString(R.string.msg_add_pc), false);
|
||||||
|
|
||||||
success = managerBinder.addComputerBlocking(host, true);
|
try {
|
||||||
if (!success){
|
InetAddress addr = InetAddress.getByName(host);
|
||||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
|
||||||
|
if (!managerBinder.addComputerBlocking(addr)){
|
||||||
|
msg = getResources().getString(R.string.addpc_fail);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg = getResources().getString(R.string.addpc_success);
|
||||||
|
finish = true;
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
msg = getResources().getString(R.string.addpc_unknown_host);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
|
||||||
if (wrongSiteLocal) {
|
final boolean toastFinish = finish;
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
final String toastMsg = msg;
|
||||||
}
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
else if (!success) {
|
@Override
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
|
public void run() {
|
||||||
}
|
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
||||||
else {
|
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_success), Toast.LENGTH_LONG).show();
|
|
||||||
|
|
||||||
if (!isFinishing()) {
|
if (toastFinish && !isFinishing()) {
|
||||||
// Close the activity
|
// Close the activity
|
||||||
AddComputerManually.this.finish();
|
AddComputerManually.this.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startAddThread() {
|
private void startAddThread() {
|
||||||
@@ -187,7 +142,7 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
this.hostText = findViewById(R.id.hostTextView);
|
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||||
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.DialogPreference;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
|
|
||||||
import static com.limelight.binding.input.virtual_controller.VirtualControllerConfigurationLoader.OSC_PREFERENCE;
|
|
||||||
|
|
||||||
public class ConfirmDeleteOscPreference extends DialogPreference {
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public ConfirmDeleteOscPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
|
||||||
getContext().getSharedPreferences(OSC_PREFERENCE, Context.MODE_PRIVATE).edit().clear().apply();
|
|
||||||
Toast.makeText(getContext(), R.string.toast_reset_osc_success, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
public class GlPreferences {
|
|
||||||
private static final String PREF_NAME = "GlPreferences";
|
|
||||||
|
|
||||||
private static final String FINGERPRINT_PREF_STRING = "Fingerprint";
|
|
||||||
private static final String GL_RENDERER_PREF_STRING = "Renderer";
|
|
||||||
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
public String glRenderer;
|
|
||||||
public String savedFingerprint;
|
|
||||||
|
|
||||||
private GlPreferences(SharedPreferences prefs) {
|
|
||||||
this.prefs = prefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GlPreferences readPreferences(Context context) {
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, 0);
|
|
||||||
GlPreferences glPrefs = new GlPreferences(prefs);
|
|
||||||
|
|
||||||
glPrefs.glRenderer = prefs.getString(GL_RENDERER_PREF_STRING, "");
|
|
||||||
glPrefs.savedFingerprint = prefs.getString(FINGERPRINT_PREF_STRING, "");
|
|
||||||
|
|
||||||
return glPrefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean writePreferences() {
|
|
||||||
return prefs.edit()
|
|
||||||
.putString(GL_RENDERER_PREF_STRING, glRenderer)
|
|
||||||
.putString(FINGERPRINT_PREF_STRING, savedFingerprint)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,13 @@ package com.limelight.preferences;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
public class PreferenceConfiguration {
|
||||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
||||||
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
|
||||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||||
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
||||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||||
@@ -23,22 +23,13 @@ public class PreferenceConfiguration {
|
|||||||
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
||||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
||||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
||||||
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
|
||||||
private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
|
|
||||||
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
|
||||||
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
|
||||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
|
||||||
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
|
||||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
|
||||||
|
|
||||||
private static final int BITRATE_DEFAULT_360_30 = 1000;
|
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||||
private static final int BITRATE_DEFAULT_360_60 = 2000;
|
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||||
private static final int BITRATE_DEFAULT_720_30 = 5000;
|
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10000;
|
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
||||||
private static final int BITRATE_DEFAULT_1080_30 = 10000;
|
private static final int BITRATE_DEFAULT_4K_30 = 40;
|
||||||
private static final int BITRATE_DEFAULT_1080_60 = 20000;
|
private static final int BITRATE_DEFAULT_4K_60 = 80;
|
||||||
private static final int BITRATE_DEFAULT_4K_30 = 40000;
|
|
||||||
private static final int BITRATE_DEFAULT_4K_60 = 80000;
|
|
||||||
|
|
||||||
private static final String DEFAULT_RES_FPS = "720p60";
|
private static final String DEFAULT_RES_FPS = "720p60";
|
||||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||||
@@ -54,13 +45,6 @@ public class PreferenceConfiguration {
|
|||||||
private static final boolean DEFAULT_USB_DRIVER = true;
|
private static final boolean DEFAULT_USB_DRIVER = true;
|
||||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||||
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
|
||||||
private static final boolean DEFAULT_BATTERY_SAVER = false;
|
|
||||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
|
||||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
|
||||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
|
||||||
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
|
||||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -74,22 +58,9 @@ public class PreferenceConfiguration {
|
|||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||||
public boolean onscreenController;
|
public boolean onscreenController;
|
||||||
public boolean onlyL3R3;
|
|
||||||
public boolean batterySaver;
|
|
||||||
public boolean disableFrameDrop;
|
|
||||||
public boolean enableHdr;
|
|
||||||
public boolean enablePip;
|
|
||||||
public boolean bindAllUsb;
|
|
||||||
public boolean mouseEmulation;
|
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
public static int getDefaultBitrate(String resFpsString) {
|
||||||
if (resFpsString.equals("360p30")) {
|
if (resFpsString.equals("720p30")) {
|
||||||
return BITRATE_DEFAULT_360_30;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("360p60")) {
|
|
||||||
return BITRATE_DEFAULT_360_60;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("720p30")) {
|
|
||||||
return BITRATE_DEFAULT_720_30;
|
return BITRATE_DEFAULT_720_30;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("720p60")) {
|
else if (resFpsString.equals("720p60")) {
|
||||||
@@ -157,39 +128,13 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resetStreamingSettings(Context context) {
|
|
||||||
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
prefs.edit()
|
|
||||||
.remove(BITRATE_PREF_STRING)
|
|
||||||
.remove(BITRATE_PREF_OLD_STRING)
|
|
||||||
.remove(RES_FPS_PREF_STRING)
|
|
||||||
.remove(VIDEO_FORMAT_PREF_STRING)
|
|
||||||
.remove(ENABLE_HDR_PREF_STRING)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PreferenceConfiguration readPreferences(Context context) {
|
public static PreferenceConfiguration readPreferences(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||||
|
|
||||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
||||||
if (config.bitrate == 0) {
|
|
||||||
config.bitrate = getDefaultBitrate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||||
if (str.equals("360p30")) {
|
if (str.equals("720p30")) {
|
||||||
config.width = 640;
|
|
||||||
config.height = 360;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("360p60")) {
|
|
||||||
config.width = 640;
|
|
||||||
config.height = 360;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("720p30")) {
|
|
||||||
config.width = 1280;
|
config.width = 1280;
|
||||||
config.height = 720;
|
config.height = 720;
|
||||||
config.fps = 30;
|
config.fps = 30;
|
||||||
@@ -243,13 +188,6 @@ public class PreferenceConfiguration {
|
|||||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
||||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
||||||
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
|
||||||
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
|
|
||||||
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
|
||||||
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
|
||||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
|
||||||
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
|
||||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import android.widget.TextView;
|
|||||||
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
||||||
public class SeekBarPreference extends DialogPreference
|
public class SeekBarPreference extends DialogPreference
|
||||||
{
|
{
|
||||||
private static final String ANDROID_SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
||||||
private static final String SEEKBAR_SCHEMA_URL = "http://schemas.moonlight-stream.com/apk/res/seekbar";
|
|
||||||
|
|
||||||
private SeekBar seekBar;
|
private SeekBar seekBar;
|
||||||
private TextView valueText;
|
private TextView valueText;
|
||||||
@@ -28,7 +27,6 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
private final int defaultValue;
|
private final int defaultValue;
|
||||||
private final int maxValue;
|
private final int maxValue;
|
||||||
private final int minValue;
|
private final int minValue;
|
||||||
private final int stepSize;
|
|
||||||
private int currentValue;
|
private int currentValue;
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
@@ -36,28 +34,27 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
// Read the message from XML
|
// Read the message from XML
|
||||||
int dialogMessageId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "dialogMessage", 0);
|
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
|
||||||
if (dialogMessageId == 0) {
|
if (dialogMessageId == 0) {
|
||||||
dialogMessage = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "dialogMessage");
|
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dialogMessage = context.getString(dialogMessageId);
|
dialogMessage = context.getString(dialogMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the suffix for the number displayed in the dialog
|
// Get the suffix for the number displayed in the dialog
|
||||||
int suffixId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "text", 0);
|
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
|
||||||
if (suffixId == 0) {
|
if (suffixId == 0) {
|
||||||
suffix = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "text");
|
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
suffix = context.getString(suffixId);
|
suffix = context.getString(suffixId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get default, min, and max seekbar values
|
// Get default, min, and max seekbar values
|
||||||
defaultValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||||
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
||||||
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
minValue = 1;
|
||||||
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,12 +89,6 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int roundedValue = ((value + (stepSize - 1))/stepSize)*stepSize;
|
|
||||||
if (roundedValue != value) {
|
|
||||||
seekBar.setProgress(roundedValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String t = String.valueOf(value);
|
String t = String.valueOf(value);
|
||||||
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,21 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.media.MediaCodecInfo;
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceCategory;
|
import android.preference.PreferenceCategory;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.util.Range;
|
|
||||||
import android.view.Display;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
@@ -57,37 +53,6 @@ public class StreamSettings extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
|
|
||||||
private static void removeResolution(ListPreference pref, String prefix) {
|
|
||||||
int matchingCount = 0;
|
|
||||||
|
|
||||||
// Count the number of matching entries we'll be removing
|
|
||||||
for (CharSequence seq : pref.getEntryValues()) {
|
|
||||||
if (seq.toString().startsWith(prefix)) {
|
|
||||||
matchingCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the new arrays
|
|
||||||
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
|
|
||||||
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
|
||||||
int outIndex = 0;
|
|
||||||
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
|
||||||
if (pref.getEntryValues()[i].toString().startsWith(prefix)) {
|
|
||||||
// Skip matching prefixes
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries[outIndex] = pref.getEntries()[i];
|
|
||||||
entryValues[outIndex] = pref.getEntryValues()[i];
|
|
||||||
outIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the preference with the new list
|
|
||||||
pref.setEntries(entries);
|
|
||||||
pref.setEntryValues(entryValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -103,130 +68,6 @@ public class StreamSettings extends Activity {
|
|||||||
screen.removePreference(category);
|
screen.removePreference(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove PiP mode on devices pre-Oreo
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide non-supported resolution/FPS combinations
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
|
||||||
|
|
||||||
int maxSupportedResW = 0;
|
|
||||||
|
|
||||||
// Always allow resolutions that are smaller or equal to the active
|
|
||||||
// display resolution because decoders can report total non-sense to us.
|
|
||||||
// For example, a p201 device reports:
|
|
||||||
// AVC Decoder: OMX.amlogic.avc.decoder.awesome
|
|
||||||
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
|
|
||||||
// AVC supported width range: 64 - 384
|
|
||||||
// HEVC supported width range: 64 - 544
|
|
||||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
|
||||||
// Some devices report their dimensions in the portrait orientation
|
|
||||||
// where height > width. Normalize these to the conventional width > height
|
|
||||||
// arrangement before we process them.
|
|
||||||
|
|
||||||
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
|
||||||
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
|
||||||
|
|
||||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This must be called to do runtime initialization before calling functions that evaluate
|
|
||||||
// decoder lists.
|
|
||||||
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
|
|
||||||
|
|
||||||
MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1);
|
|
||||||
MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
|
||||||
|
|
||||||
if (avcDecoder != null) {
|
|
||||||
Range<Integer> avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
|
||||||
|
|
||||||
LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper());
|
|
||||||
|
|
||||||
// If 720p is not reported as supported, ignore all results from this API
|
|
||||||
if (avcWidthRange.contains(1280)) {
|
|
||||||
if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
else if (maxSupportedResW < 1280) {
|
|
||||||
maxSupportedResW = 1280;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hevcDecoder != null) {
|
|
||||||
Range<Integer> hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
|
||||||
|
|
||||||
LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper());
|
|
||||||
|
|
||||||
// If 720p is not reported as supported, ignore all results from this API
|
|
||||||
if (hevcWidthRange.contains(1280)) {
|
|
||||||
if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
else if (maxSupportedResW < 1280) {
|
|
||||||
maxSupportedResW = 1280;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
|
||||||
|
|
||||||
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
|
|
||||||
if (maxSupportedResW != 0) {
|
|
||||||
if (maxSupportedResW < 3840) {
|
|
||||||
// 4K is unsupported
|
|
||||||
removeResolution(resPref, "4K");
|
|
||||||
}
|
|
||||||
if (maxSupportedResW < 1920) {
|
|
||||||
// 1080p is unsupported
|
|
||||||
removeResolution(resPref, "1080p");
|
|
||||||
}
|
|
||||||
// Never remove 720p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove HDR preference for devices below Nougat
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
||||||
LimeLog.info("Excluding HDR toggle based on OS");
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_hdr"));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
|
||||||
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
|
|
||||||
|
|
||||||
// We must now ensure our display is compatible with HDR10
|
|
||||||
boolean foundHdr10 = false;
|
|
||||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
|
||||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
|
||||||
foundHdr10 = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundHdr10) {
|
|
||||||
LimeLog.info("Excluding HDR toggle based on display capabilities");
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_hdr"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a listener to the FPS and resolution preference
|
// Add a listener to the FPS and resolution preference
|
||||||
// so the bitrate can be auto-adjusted
|
// so the bitrate can be auto-adjusted
|
||||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package com.limelight.ui;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
|
|
||||||
public interface AdapterFragmentCallbacks {
|
public interface AdapterFragmentCallbacks {
|
||||||
int getAdapterFragmentLayoutId();
|
public int getAdapterFragmentLayoutId();
|
||||||
void receiveAbsListView(AbsListView gridView);
|
public void receiveAbsListView(AbsListView gridView);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.limelight.ui;
|
package com.limelight.ui;
|
||||||
|
|
||||||
public interface GameGestures {
|
public interface GameGestures {
|
||||||
void showKeyboard();
|
public void showKeyboard();
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ import java.io.OutputStream;
|
|||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
||||||
public class CacheHelper {
|
public class CacheHelper {
|
||||||
public static File openPath(boolean createPath, File root, String... path) {
|
private static File openPath(boolean createPath, File root, String... path) {
|
||||||
File f = root;
|
File f = root;
|
||||||
for (int i = 0; i < path.length; i++) {
|
for (int i = 0; i < path.length; i++) {
|
||||||
String component = path[i];
|
String component = path[i];
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ public class Dialog implements Runnable {
|
|||||||
private final String title;
|
private final String title;
|
||||||
private final String message;
|
private final String message;
|
||||||
private final Activity activity;
|
private final Activity activity;
|
||||||
private final Runnable runOnDismiss;
|
private final boolean endAfterDismiss;
|
||||||
|
|
||||||
private AlertDialog alert;
|
private AlertDialog alert;
|
||||||
|
|
||||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
||||||
|
|
||||||
private Dialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.runOnDismiss = runOnDismiss;
|
this.endAfterDismiss = endAfterDismiss;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs()
|
||||||
@@ -40,21 +40,9 @@ public class Dialog implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayDialog(final Activity activity, String title, String message, final boolean endAfterDismiss)
|
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
activity.runOnUiThread(new Dialog(activity, title, message, new Runnable() {
|
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (endAfterDismiss) {
|
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void displayDialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
|
||||||
{
|
|
||||||
activity.runOnUiThread(new Dialog(activity, title, message, runOnDismiss));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -77,7 +65,9 @@ public class Dialog implements Runnable {
|
|||||||
alert.dismiss();
|
alert.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
runOnDismiss.run();
|
if (endAfterDismiss) {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
||||||
@@ -87,7 +77,9 @@ public class Dialog implements Runnable {
|
|||||||
alert.dismiss();
|
alert.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
runOnDismiss.run();
|
if (endAfterDismiss) {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
|
||||||
HelpLauncher.launchTroubleshooting(activity);
|
HelpLauncher.launchTroubleshooting(activity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,24 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.HelpActivity;
|
import com.limelight.HelpActivity;
|
||||||
|
|
||||||
public class HelpLauncher {
|
public class HelpLauncher {
|
||||||
|
|
||||||
private static boolean isKnownBrowser(Context context, Intent i) {
|
|
||||||
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
|
|
||||||
if (resolvedActivity == null) {
|
|
||||||
// No browser
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String name = resolvedActivity.activityInfo.name;
|
|
||||||
if (name == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
name = name.toLowerCase();
|
|
||||||
return name.contains("chrome") || name.contains("firefox");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void launchUrl(Context context, String url) {
|
private static void launchUrl(Context context, String url) {
|
||||||
// Try to launch the default browser
|
// Try to launch the default browser
|
||||||
try {
|
try {
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
// Fire TV devices will lie and say they do have a browser
|
||||||
i.setData(Uri.parse(url));
|
// even though the OS just shows an error dialog if we
|
||||||
|
// try to use it.
|
||||||
// Several Android TV devices will lie and say they do have a browser
|
if (!"Amazon".equalsIgnoreCase(Build.MANUFACTURER)) {
|
||||||
// even though the OS just shows an error dialog if we try to use it. We need to
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
// be a bit more clever on these devices and detect if the browser is a legitimate
|
i.setData(Uri.parse(url));
|
||||||
// browser or just a fake error message activity.
|
|
||||||
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
|
|
||||||
isKnownBrowser(context, i)) {
|
|
||||||
context.startActivity(i);
|
context.startActivity(i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,21 +14,23 @@ import com.limelight.nvstream.http.NvApp;
|
|||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
public class ServerHelper {
|
public class ServerHelper {
|
||||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
computer.localAddress : computer.remoteAddress;
|
computer.localIp : computer.remoteIp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
Intent intent = new Intent(parent, Game.class);
|
Intent intent = new Intent(parent, Game.class);
|
||||||
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
|
intent.putExtra(Game.EXTRA_HOST,
|
||||||
|
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||||
@@ -43,7 +45,7 @@ public class ServerHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
public static void doQuit(final Activity parent,
|
||||||
final String address,
|
final InetAddress address,
|
||||||
final NvApp app,
|
final NvApp app,
|
||||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
final Runnable onComplete) {
|
final Runnable onComplete) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.app.AlertDialog;
|
|||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -59,44 +58,6 @@ public class UiHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showDecoderCrashDialog(Activity activity) {
|
|
||||||
final SharedPreferences prefs = activity.getSharedPreferences("DecoderTombstone", 0);
|
|
||||||
final int crashCount = prefs.getInt("CrashCount", 0);
|
|
||||||
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
|
|
||||||
|
|
||||||
// Remember the last crash count we notified at, so we don't
|
|
||||||
// display the crash dialog every time the app is started until
|
|
||||||
// they stream again
|
|
||||||
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
|
|
||||||
if (crashCount % 3 == 0) {
|
|
||||||
// At 3 consecutive crashes, we'll forcefully reset their settings
|
|
||||||
PreferenceConfiguration.resetStreamingSettings(activity);
|
|
||||||
Dialog.displayDialog(activity,
|
|
||||||
activity.getResources().getString(R.string.title_decoding_reset),
|
|
||||||
activity.getResources().getString(R.string.message_decoding_reset),
|
|
||||||
new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Mark notification as acknowledged on dismissal
|
|
||||||
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Dialog.displayDialog(activity,
|
|
||||||
activity.getResources().getString(R.string.title_decoding_error),
|
|
||||||
activity.getResources().getString(R.string.message_decoding_error),
|
|
||||||
new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Mark notification as acknowledged on dismissal
|
|
||||||
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
||||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
# Application.mk for Moonlight
|
# Application.mk for Limelight
|
||||||
|
|
||||||
# Our minimum version is Android 4.1
|
# Our minimum version is Android 4.1
|
||||||
APP_PLATFORM := android-16
|
APP_PLATFORM := android-16
|
||||||
|
|
||||||
|
# Support all modern ABIs
|
||||||
|
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips mips64
|
||||||
|
|
||||||
|
# We want an optimized build
|
||||||
|
APP_OPTIM := release
|
||||||
@@ -5,32 +5,28 @@ include $(call all-subdir-makefiles)
|
|||||||
|
|
||||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||||
|
|
||||||
# Only build evdev_reader for the rooted APK flavor
|
include $(CLEAR_VARS)
|
||||||
ifeq (root,$(PRODUCT_FLAVOR))
|
LOCAL_MODULE := evdev_reader
|
||||||
include $(CLEAR_VARS)
|
LOCAL_SRC_FILES := evdev_reader.c
|
||||||
LOCAL_MODULE := evdev_reader
|
LOCAL_LDLIBS := -llog
|
||||||
LOCAL_SRC_FILES := evdev_reader.c
|
|
||||||
LOCAL_LDLIBS := -llog
|
|
||||||
|
|
||||||
|
|
||||||
# This next portion of the makefile is mostly copied from build-executable.mk but
|
# This next portion of the makefile is mostly copied from build-executable.mk but
|
||||||
# creates a binary with the libXXX.so form so the APK will install and drop
|
# creates a binary with the libXXX.so form so the APK will install and drop
|
||||||
# the binary correctly.
|
# the binary correctly.
|
||||||
|
|
||||||
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
||||||
LOCAL_MAKEFILE := $(local-makefile)
|
LOCAL_MAKEFILE := $(local-makefile)
|
||||||
|
|
||||||
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
||||||
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
||||||
$(call check-LOCAL_MODULE_FILENAME)
|
$(call check-LOCAL_MODULE_FILENAME)
|
||||||
|
|
||||||
# we are building target objects
|
# we are building target objects
|
||||||
my := TARGET_
|
my := TARGET_
|
||||||
|
|
||||||
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
||||||
$(call handle-module-built)
|
$(call handle-module-built)
|
||||||
|
|
||||||
LOCAL_MODULE_CLASS := EXECUTABLE
|
|
||||||
include $(BUILD_SYSTEM)/build-module.mk
|
|
||||||
endif
|
|
||||||
|
|
||||||
|
LOCAL_MODULE_CLASS := EXECUTABLE
|
||||||
|
include $(BUILD_SYSTEM)/build-module.mk
|
||||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 65 KiB |
@@ -6,7 +6,7 @@
|
|||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
tools:context=".preferences.AddComputerManually" >
|
tools:context=".AddComputerManually" >
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manuallyAddPcText"
|
android:id="@+id/manuallyAddPcText"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/activity_help"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
|
tools:context="com.limelight.HelpActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentStart="true" />
|
||||||
|
</RelativeLayout>
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
android:id="@+id/stream_settings"
|
android:id="@+id/stream_settings"
|
||||||
tools:context=".preferences.StreamSettings">
|
tools:context=".StreamSettings">
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_pc_scut_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_pc_scut_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 715 B |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 471 B |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 816 B |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -89,7 +89,7 @@
|
|||||||
<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="suffix_seekbar_bitrate">Mbps</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>
|
||||||
@@ -109,8 +109,6 @@
|
|||||||
<string name="category_on_screen_controls_settings">Configuración de controles en pantalla</string>
|
<string name="category_on_screen_controls_settings">Configuración de controles en pantalla</string>
|
||||||
<string name="title_checkbox_show_onscreen_controls">Mostrar controles en pantalla</string>
|
<string name="title_checkbox_show_onscreen_controls">Mostrar controles en pantalla</string>
|
||||||
<string name="summary_checkbox_show_onscreen_controls">Muestra controles virtuales superpuestos en la pantalla táctil</string>
|
<string name="summary_checkbox_show_onscreen_controls">Muestra controles virtuales superpuestos en la pantalla táctil</string>
|
||||||
<string name="title_only_l3r3">Solo muestra L3 y R3</string>
|
|
||||||
<string name="summary_only_l3r3">Ocultar todo excepto L3 y R3</string>
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
<string name="summary_resolution_list">Le réglage de valeurs trop élevées pour votre appareil peut provoquer un retard ou un plantage</string>
|
<string name="summary_resolution_list">Le réglage de valeurs trop élevées pour votre appareil peut provoquer un retard ou un plantage</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="suffix_seekbar_bitrate">Mbps</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="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string>
|
||||||
@@ -120,8 +120,6 @@
|
|||||||
<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_only_l3r3">Montre seulement L3 et R3</string>
|
|
||||||
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</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>
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Shortcut strings -->
|
|
||||||
<string name="scut_deleted_pc">PC eliminato</string>
|
|
||||||
<string name="scut_not_paired">PC non accoppiato</string>
|
|
||||||
|
|
||||||
<!-- Help strings -->
|
|
||||||
<string name="help_loading_title">Visualizza assistenza</string>
|
|
||||||
<string name="help_loading_msg">Caricamento pagina di assistenza…</string>
|
|
||||||
|
|
||||||
<!-- PC view menu entries -->
|
<!-- PC view menu entries -->
|
||||||
<string name="pcview_menu_app_list">Lista applicazioni</string>
|
<string name="pcview_menu_app_list">Lista applicazioni</string>
|
||||||
<string name="pcview_menu_pair_pc">Accoppia PC</string>
|
<string name="pcview_menu_pair_pc">Accoppia PC</string>
|
||||||
@@ -17,22 +10,21 @@
|
|||||||
|
|
||||||
<!-- Pair messages -->
|
<!-- Pair messages -->
|
||||||
<string name="pairing">Accoppiamento…</string>
|
<string name="pairing">Accoppiamento…</string>
|
||||||
<string name="pair_pc_offline">Il PC è offline</string>
|
<string name="pair_pc_offline">PC offline</string>
|
||||||
<string name="pair_pc_ingame">PC con applicazione avviata. Devi chiudere l\'applicazione prima dell\'accoppiamento.</string>
|
<string name="pair_pc_ingame">PC con applicazione avviata. Devi chiudere l\'applicazione prima dell\'accoppiamento.</string>
|
||||||
<string name="pair_pairing_title">Accoppiamento</string>
|
<string name="pair_pairing_title">Accoppiamento</string>
|
||||||
<string name="pair_pairing_msg">Inserisci il seguente PIN sul PC:</string>
|
<string name="pair_pairing_msg">Inserisci il seguente PIN sul PC:</string>
|
||||||
<string name="pair_incorrect_pin">PIN non corretto</string>
|
<string name="pair_incorrect_pin">PIN non corretto</string>
|
||||||
<string name="pair_fail">Accoppiamento fallito</string>
|
<string name="pair_fail">Accoppiamento fallito</string>
|
||||||
<string name="pair_already_in_progress">Accoppiamento già in corso</string>
|
|
||||||
|
|
||||||
<!-- WOL messages -->
|
<!-- WOL messages -->
|
||||||
<string name="wol_pc_online">PC già avviato</string>
|
<string name="wol_pc_online">PC già avviato</string>
|
||||||
<string name="wol_no_mac">Impossibile risvegliare il PC perché GFE non ha inviato nessun indirizzo MAC</string>
|
<string name="wol_no_mac">Impossibile risvegliare il PC perchè GFE non ha inviato nessun indirizzo MAC</string>
|
||||||
<string name="wol_waking_pc">Risveglio PC…</string>
|
<string name="wol_waking_pc">Risveglio PC…</string>
|
||||||
<string name="wol_waking_msg">Il PC potrebbe impiegare qualche secondo per risvegliarsi.
|
<string name="wol_waking_msg">Il PC potrebbe impiegare qualche secondo per risvegliarsi.
|
||||||
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
|
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
|
||||||
</string>
|
</string>
|
||||||
<string name="wol_fail">Invio pacchetti Wake-On-LAN fallito</string>
|
<string name="wol_fail">Invio pacchetto Wake-On-LAN fallito</string>
|
||||||
|
|
||||||
<!-- Unpair messages -->
|
<!-- Unpair messages -->
|
||||||
<string name="unpairing">Disaccoppiamento…</string>
|
<string name="unpairing">Disaccoppiamento…</string>
|
||||||
@@ -45,41 +37,33 @@
|
|||||||
<string name="error_manager_not_running">Il servizio ComputerManager non è avviato. Attendi qualche secondo o riavvia l\'applicazione.</string>
|
<string name="error_manager_not_running">Il servizio ComputerManager non è avviato. Attendi qualche secondo o riavvia l\'applicazione.</string>
|
||||||
<string name="error_unknown_host">Risoluzione nome host fallita</string>
|
<string name="error_unknown_host">Risoluzione nome host fallita</string>
|
||||||
<string name="error_404">GFE ha ritornato un errore HTTP 404 error. Assicurati che il PC stia usando una GPU supportata.
|
<string name="error_404">GFE ha ritornato un errore HTTP 404 error. Assicurati che il PC stia usando una GPU supportata.
|
||||||
Usare un software di desktop remoto può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
|
Usare un software di remote-desktop può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
|
||||||
</string>
|
</string>
|
||||||
<string name="title_decoding_error">Il decodificatore video ha smesso di funzionare</string>
|
|
||||||
<string name="message_decoding_error">Moonlight ha smesso di funzionare per colpa di un problema al decodificatore video di questo dispositivo. Prova a modificare le impostazioni di stream se il problema persiste.</string>
|
|
||||||
<string name="title_decoding_reset">Ripristino impostazioni video</string>
|
|
||||||
<string name="message_decoding_reset">Il decodificatore video del dispositivo continua a funzionare in modo anomalo con le impostazioni di streaming selezionate. Le impostazioni di streaming sono state ripristinate ai valori predefiniti.</string>
|
|
||||||
<string name="error_usb_prohibited">L\'accesso USB è vietato dall\'amministratore del dispositivo. Verifica le impostazioni Knox o MDM.</string>
|
|
||||||
|
|
||||||
<!-- Start application messages -->
|
<!-- Start application messages -->
|
||||||
<string name="conn_establishing_title">Connessione in corso</string>
|
<string name="conn_establishing_title">Connessione</string>
|
||||||
<string name="conn_establishing_msg">Avvio connessione</string>
|
<string name="conn_establishing_msg">Connessione in corso</string>
|
||||||
<string name="conn_metered">Attenzione: la rete attiva prevede costi aggiuntivi in base all\'utilizzo!</string>
|
<string name="conn_metered">Attenzione: la rete attiva prevede costi aggiuntivi in base all\'utilizzo!</string>
|
||||||
<string name="conn_client_latency">Latenza decodifica fotogrammi media:</string>
|
<string name="conn_client_latency">Latenza frame media client-side:</string>
|
||||||
<string name="conn_client_latency_hw">latenza decodificatore hardware:</string>
|
<string name="conn_client_latency_hw">latenza decoder hardware:</string>
|
||||||
<string name="conn_hardware_latency">Latenza decodificatore hardware media:</string>
|
<string name="conn_hardware_latency">Latenza decoder hardware media:</string>
|
||||||
<string name="conn_starting">Avvio in corso…</string>
|
<string name="conn_starting">Avvio in corso…</string>
|
||||||
<string name="conn_error_title">Errore connessione</string>
|
<string name="conn_error_title">Errore connessione</string>
|
||||||
<string name="conn_error_msg">Avvio fallito</string>
|
<string name="conn_error_msg">Avvio fallito</string>
|
||||||
<string name="conn_terminated_title">Connessione interrotta</string>
|
<string name="conn_terminated_title">Connessione terminata</string>
|
||||||
<string name="conn_terminated_msg">La connessione è stata interrotta</string>
|
<string name="conn_terminated_msg">La connessione è stata interrotta</string>
|
||||||
|
|
||||||
<!-- General strings -->
|
<!-- General strings -->
|
||||||
<string name="ip_hint">Indirizzo IP del PC</string>
|
<string name="ip_hint">Indirizzo IP del PC</string>
|
||||||
<string name="searching_pc">Ricerca di PC con GameStream avviato…\n\n
|
<string name="searching_pc">Ricerca PC in corso…</string>
|
||||||
Assicurati che GameStream sia abilitato nelle impostazioni SHIELD di GeForce Experience.</string>
|
|
||||||
<string name="yes">Sì</string>
|
<string name="yes">Sì</string>
|
||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
<string name="lost_connection">Connessione con il PC persa</string>
|
<string name="lost_connection">Connessione con il PC persa</string>
|
||||||
<string name="help">Assistenza</string>
|
|
||||||
|
|
||||||
<!-- AppList activity -->
|
<!-- AppList activity -->
|
||||||
<string name="title_applist">Applicazioni su</string>
|
<string name="title_applist">Applicazioni su</string>
|
||||||
<string name="applist_connect_msg">Connessione al PC in corso…</string>
|
<string name="applist_menu_resume">Riprendi Sessione</string>
|
||||||
<string name="applist_menu_resume">Riprendi sessione</string>
|
<string name="applist_menu_quit">Chiudi Sessione</string>
|
||||||
<string name="applist_menu_quit">Chiudi sessione</string>
|
|
||||||
<string name="applist_menu_quit_and_start">Chiudi sessione corrente e avvia</string>
|
<string name="applist_menu_quit_and_start">Chiudi sessione corrente e avvia</string>
|
||||||
<string name="applist_menu_cancel">Annulla</string>
|
<string name="applist_menu_cancel">Annulla</string>
|
||||||
<string name="applist_refresh_title">Lista applicazioni</string>
|
<string name="applist_refresh_title">Lista applicazioni</string>
|
||||||
@@ -92,76 +76,43 @@
|
|||||||
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
|
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
|
||||||
|
|
||||||
<!-- Add computer manually activity -->
|
<!-- Add computer manually activity -->
|
||||||
<string name="title_add_pc">Aggiungi PC manualmente</string>
|
<string name="title_add_pc">Aggiungi PC Manualmente</string>
|
||||||
<string name="msg_add_pc">Connessione al PC in corso…</string>
|
<string name="msg_add_pc">Connessione al PC in corso…</string>
|
||||||
<string name="addpc_fail">Impossibile connettersi al PC specificato. Assicurati che le porte nel firewall del PC siano configurate correttamente.</string>
|
<string name="addpc_fail">Impossibile connettersi al PC. Assicurati che il firewall del PC sia configurato correttamente.</string>
|
||||||
<string name="addpc_success">PC aggiunto con successo</string>
|
<string name="addpc_success">PC aggiunto con successo</string>
|
||||||
<string name="addpc_unknown_host">Impossibile risovere l\'indirizzo del PC. Assicurati di aver scritto correttamente l\'indirizzo.</string>
|
<string name="addpc_unknown_host">Impossibile risovere l\'indirizzo del PC. Assicurati di aver scritto correttamente l\'indirizzo.</string>
|
||||||
<string name="addpc_enter_ip">Devi inserire un indirizzo IP</string>
|
<string name="addpc_enter_ip">Devi inserire un indirizzo IP</string>
|
||||||
<string name="addpc_wrong_sitelocal">Quell\'indirizzo non sembra corretto. È necessario utilizzare l\'indirizzo IP pubblico del router per lo streaming su Internet.</string>
|
|
||||||
|
|
||||||
<!-- Preferences -->
|
<!-- Preferences -->
|
||||||
<string name="category_basic_settings">Impostazioni generali</string>
|
<string name="category_basic_settings">Impostazioni Base</string>
|
||||||
<string name="title_resolution_list">Risoluzione e FPS</string>
|
<string name="title_resolution_list">Risoluzione e FPS</string>
|
||||||
<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">Bitrate 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 il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
|
||||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
<string name="suffix_seekbar_bitrate">Mbps</string>
|
||||||
<string name="title_checkbox_stretch_video">Forza video a schermo intero</string>
|
<string name="title_checkbox_stretch_video">Forza video in full-screen</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>
|
||||||
<string name="title_checkbox_battery_saver">Risparmio batteria</string>
|
|
||||||
<string name="summary_checkbox_battery_saver">Usa meno batteria, ma può aumentare lo stuttering</string>
|
|
||||||
<string name="title_checkbox_enable_pip">Abilita modalità spettatore Picture-in-Picture</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_gamepad_settings">Impostazioni Gamepad</string>
|
||||||
<string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string>
|
<string name="title_checkbox_multi_controller">Supporto controller multipli</string>
|
||||||
<string name="summary_checkbox_51_surround">Se riscontri problemi, disabilitalo. Richiede GFE 2.7 o versioni sucessive.</string>
|
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
|
||||||
|
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
|
||||||
<string name="category_gamepad_settings">Impostazioni 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="title_seekbar_deadzone">Regola i punti morti degli stick analogici</string>
|
|
||||||
<string name="suffix_seekbar_deadzone">%</string>
|
<string name="suffix_seekbar_deadzone">%</string>
|
||||||
<string name="title_checkbox_xb1_driver">Driver del controller Xbox 360/One</string>
|
|
||||||
<string name="summary_checkbox_xb1_driver">Abilita un driver USB integrato per dispositivi senza supporto al controller Xbox nativo</string>
|
|
||||||
<string name="title_checkbox_usb_bind_all">Sovrascrivi il supporto ai controller su Android</string>
|
|
||||||
<string name="summary_checkbox_usb_bind_all">Forza i driver USB di Moonlight di assumere il controllo su tutti i controller Xbox supportati</string>
|
|
||||||
<string name="title_checkbox_mouse_emulation">Emulazione del mouse tramite controller</string>
|
|
||||||
<string name="summary_checkbox_mouse_emulation">Tenendo premuto il pulsante Start, il controller passerà alla modalità mouse</string>
|
|
||||||
|
|
||||||
<string name="category_on_screen_controls_settings">Impostazioni dei controlli a schermo</string>
|
|
||||||
<string name="title_checkbox_show_onscreen_controls">Mostra controlli a schermo</string>
|
|
||||||
<string name="summary_checkbox_show_onscreen_controls">Mostra l\'overlay virtuale del controller su schermo</string>
|
|
||||||
<string name="title_only_l3r3">Mostra solo L3 e R3</string>
|
|
||||||
<string name="summary_only_l3r3">Nasconde tutti i pulsanti virtuali tranne L3 e R3</string>
|
|
||||||
<string name="title_reset_osc">Ripristina il layout personalizzato dei controlli a schermo</string>
|
|
||||||
<string name="summary_reset_osc">Ripristina tutti i controlli a schermo nelle loro posizioni e dimensioni predefinite</string>
|
|
||||||
<string name="dialog_title_reset_osc">Ripristino del layout</string>
|
|
||||||
<string name="dialog_text_reset_osc">Sei sicuro di voler ripristinare ai valori predefiniti i layout dei controlli a schermo?</string>
|
|
||||||
<string name="toast_reset_osc_success">I controlli a schermo sono stati ripristinati</string>
|
|
||||||
|
|
||||||
<string name="category_ui_settings">Impostazioni dell\'interfaccia</string>
|
<string name="category_ui_settings">Impostazioni 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="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="summary_checkbox_list_mode">Visualizza applicazioni e computers 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 vista a griglia per avere più oggetti sullo schermo</string>
|
||||||
|
|
||||||
<string name="category_host_settings">Impostazioni del PC host</string>
|
<string name="category_host_settings">Impostazioni Host</string>
|
||||||
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
|
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
|
||||||
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
|
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
|
||||||
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
|
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
|
||||||
<string name="summary_checkbox_host_audio">Riproduce l\'audio sul PC e su questo dispositivo</string>
|
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
|
||||||
|
|
||||||
<string name="category_advanced_settings">Impostazioni avanzate</string>
|
<string name="category_advanced_settings">Impostazioni Avanzate</string>
|
||||||
<string name="title_disable_frame_drop">Non saltare i fotogrammi</string>
|
|
||||||
<string name="summary_disable_frame_drop">Potrebbe ridurre il micro-stuttering su alcuni dispositivi, ma può aumentare la latenza</string>
|
|
||||||
<string name="title_video_format">Modifica impostazioni H.265</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="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>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||