Compare commits

...

70 Commits

Author SHA1 Message Date
Cameron Gutman
cc7ac79fa6 Version 9.6.4 2020-07-10 18:31:41 -07:00
Cameron Gutman
4c5c27dfc1 Re-enable the max operating rate trick on Android 10 except on the Mi 10 Lite 5G
It still provides nice performance gains on Pixel 2 running Android 10
2020-07-10 18:29:29 -07:00
Cameron Gutman
4aabfbd52e Add missing jlong cast to fix Lint warning 2020-07-07 01:10:10 -05:00
Cameron Gutman
6eab842361 Fix Lint error due to extra translated strings 2020-07-07 01:05:10 -05:00
a6969
b729dfd702 Added Ukrainian language (#857)
* Added Ukrainian language strings.xml

Translated the application into Ukrainian language.
2020-07-07 00:59:42 -05:00
Cameron Gutman
6366840781 Update common-c to remove FEC validation assert that fails on GFE 3.20.4 2020-07-07 00:58:44 -05:00
Cameron Gutman
704a2ee90b Propagate exceptions caused by GFE response parsing errors 2020-07-07 00:57:37 -05:00
Cameron Gutman
484be9bfe6 Wrap and propagate unexpected exceptions 2020-07-07 00:52:11 -05:00
Cameron Gutman
a99e070c26 Fix missing return causing invalid parameters to be passed to LiStartConnection() 2020-07-07 00:47:12 -05:00
Cameron Gutman
bf803f88af Refactor TLS initialization code 2020-07-06 02:32:06 -05:00
Cameron Gutman
9af6febca5 Fix pairing issue due to picking up a final local variable instead of a class member 2020-07-06 02:30:49 -05:00
Cameron Gutman
0101d0a1bd Fix TLS error when connecting to GFE 3.20.4 on Android 4.x 2020-07-06 01:44:35 -05:00
Cameron Gutman
266874609d Fix hostname validation for CA-issued certificates 2020-07-04 20:09:06 -05:00
Cameron Gutman
2ba7feedfc Fix several Lint warnings 2020-07-04 15:41:41 -05:00
Cameron Gutman
43c67b4939 Avoid using max operating rate on Android Q and non-Qualcomm devices 2020-07-01 11:26:40 -05:00
Cameron Gutman
2d9915e43a Enable GWP-ASan on Android 11 2020-07-01 11:07:53 -05:00
Cameron Gutman
2329b41bce Rethrow the original validation error if the cert isn't pinned or self-signed 2020-06-29 11:29:33 -07:00
Cameron Gutman
536496184e Use the default X509TrustManager to validate non-pinned certificates
This allows the certificate to be rotated without re-adding the PC.
2020-06-29 11:20:14 -07:00
Cameron Gutman
429c32477c Version 9.6.1 2020-06-25 22:15:24 -07:00
Cameron Gutman
f5d51b2061 Disable PiP option on Fire OS due to Amazon guidelines 2020-06-24 17:26:58 -07:00
Zero O
2ad1aaa277 Update strings.xml (#850)
update translation
2020-06-24 17:21:54 -07:00
Zero O
3afd32dbc1 Update strings.xml (#851)
update translation
2020-06-24 17:21:45 -07:00
Cameron Gutman
092830ed07 Remove button emulation
It was never well documented to users and it really only makes sense
with much older controllers that don't have Start or Select buttons.
2020-06-23 22:00:56 -07:00
Cameron Gutman
d118a6d3ff Prevent edges of analog sticks from being clipped 2020-06-23 21:48:50 -07:00
Cameron Gutman
fe97ffdc2f Slightly reduce size of analog sticks to allow a gap before the edge of the screen
This reduces false analog stick releases caused when the finger goes off the display's touch area.
2020-06-23 21:36:33 -07:00
Cameron Gutman
964d2ce59c Version 9.6 2020-06-18 23:11:35 -07:00
Cameron Gutman
dc52684cbc Update moonlight-common-c to fix QoS-related connection issues 2020-06-12 22:08:01 -07:00
Cameron Gutman
191bedc56f Improve behavior and description of small box art checkbox 2020-06-11 22:01:48 -07:00
Cameron Gutman
47b2ace7fd New app grid UI 2020-06-11 21:51:07 -07:00
Cameron Gutman
9fb7359a3e Use startAnimation() instead of setAnimation() 2020-06-11 21:47:28 -07:00
Cameron Gutman
4a5de26406 Remove the small PC grid UI 2020-06-11 21:32:39 -07:00
Cameron Gutman
6fa18e126f Remove list view in preparation for grid redesign 2020-06-11 21:21:37 -07:00
Cameron Gutman
1149002e0c Improve PC and game details dialogs 2020-06-11 20:36:59 -07:00
Cameron Gutman
d704cb0b50 Use SoftReferences instead of WeakReferences for the eviction cache 2020-06-11 19:10:43 -07:00
Cameron Gutman
d59e5ae9cf Store the original bitmap dimensions for the box art 2020-06-11 19:08:25 -07:00
Cameron Gutman
4587c1550d Cache WeakReferences to our box art bitmaps after LRU evictions 2020-06-10 23:13:07 -07:00
Cameron Gutman
b5bd329ada Fade in the box art when loading from the network 2020-06-10 22:52:37 -07:00
Cameron Gutman
beccd7a4ac Fade in the box art as we load it 2020-06-10 22:37:54 -07:00
Cameron Gutman
61262fa939 Refactor grid adapters for new grid UI 2020-06-10 22:13:02 -07:00
Cameron Gutman
7c6b006631 Remove OSC rumble option if a vibrator isn't present 2020-06-10 21:15:21 -07:00
Cameron Gutman
dbd149354a Change "crashes" to "instability" 2020-06-10 21:09:24 -07:00
Cameron Gutman
4306ba5004 Add a mapping for the Nintendo Switch Pro controller
Fixes #842
2020-06-10 21:05:08 -07:00
Cameron Gutman
6de370b82f Update for Android 11 2020-06-10 20:31:32 -07:00
Cameron Gutman
45781666b8 Disable the latency toast by default
It causes crashes on the MiBox
2020-06-06 18:24:34 -07:00
Cameron Gutman
538231eb6f Attempt to appease Amazon content review 2020-06-06 17:53:09 -07:00
Cameron Gutman
eb74f87f2c Move PiP and unlock FPS options out of basic settings 2020-06-06 17:44:38 -07:00
Cameron Gutman
59d71ffdcf Don't show PiP option on devices where PiP is disabled 2020-06-06 17:32:26 -07:00
Cameron Gutman
d1b93d4011 Remove vibration option if the device can't vibrate 2020-06-06 17:25:01 -07:00
Cameron Gutman
d8ddf2e740 Update NDK for Travis CI 2020-05-28 22:19:58 -07:00
Cameron Gutman
581327dc8e Improve resolution preference storage to remove 16:9 assumptions 2020-05-28 22:05:57 -07:00
Cameron Gutman
76e4512a0c Update for Android Studio 4.0 2020-05-28 21:50:28 -07:00
Cameron Gutman
efdd55beca Add Download links 2020-05-27 19:47:30 -07:00
Cameron Gutman
2c115649b9 Update README 2020-05-27 19:44:56 -07:00
Cameron Gutman
2ddcc31a93 Update metadata for Quadro streaming 2020-05-27 18:34:59 -07:00
Cameron Gutman
3bcce5b749 Version 9.5.1 2020-05-27 18:34:26 -07:00
Cameron Gutman
80dac27214 Update moonlight-common-c 2020-05-27 00:02:33 -07:00
Cameron Gutman
4a1177d048 Use a better workaround for the GFE 3.20.3 high FPS bug 2020-05-25 19:28:00 -07:00
Cameron Gutman
4725d8f270 Revert "Disable SOPS for streams over 60 FPS for GFE 3.20.3"
This reverts commit 63072aa8e1.
2020-05-25 19:24:33 -07:00
Zero O
07b3528515 Update strings.xml (#833)
update translation
2020-05-20 19:27:47 -07:00
Zero O
d2d1b1ea26 Update strings.xml (#834)
update translation
2020-05-20 19:27:32 -07:00
Cameron Gutman
232b897abc Version 9.5 2020-05-16 21:40:41 -07:00
Cameron Gutman
efd076bc6c Ignore absolute touch events from outside the stream view 2020-05-12 00:20:07 -07:00
Cameron Gutman
cc877480ff Add an option for absolute touch mode 2020-05-11 23:53:49 -07:00
Christoph Papke
363145a284 Optimize button mapping for 8BitDo controllers (#826)
* Optimize button mapping for 8BitDo controllers #825
2020-05-05 16:04:31 -07:00
Cameron Gutman
755571ad33 Switch on-screen control buttons when flip face buttons is enabled 2020-05-04 22:23:03 -07:00
Eero Kelly
39edb55721 Add option to invert A/B X/Y (#824)
* Add option to invert A/B X/Y

* Remove redundant prefConfig
2020-05-04 22:10:35 -07:00
Cameron Gutman
15aa7ecc2e Add a friendly error message when no video traffic is received 2020-05-01 21:54:26 -07:00
Cameron Gutman
ce9e91153e Add special error text for the -1 launch error code 2020-04-25 16:10:44 -07:00
Cameron Gutman
9ee0a46606 Add new init packet to switch out of BT mode 2020-04-24 17:47:31 -07:00
Cameron Gutman
20dc351f4c Fix parsing rare GFE status code of 0xFFFFFFFF 2020-04-23 18:47:01 -07:00
72 changed files with 1377 additions and 825 deletions

View File

@@ -8,11 +8,11 @@ android:
components:
- tools
- platform-tools
- build-tools-29.0.3
- android-29
- build-tools-30.0.0
- android-30
before_install:
- sdkmanager --list
install:
- yes | sdkmanager "ndk;20.0.5594570"
- yes | sdkmanager "ndk;21.0.6113669"

View File

@@ -2,55 +2,26 @@
[![Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-android.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-android)
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
[Moonlight for Android](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
Moonlight for Android will allow you to stream your full collection of games from your Windows PC to your Android device,
whether in your own home or over the internet.
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [iOS/tvOS client](https://github.com/moonlight-stream/moonlight-ios).
## Features
Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed project information, setup guide, or troubleshooting steps.
* Streams any of your games from your PC to your Android device
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network
## Installation
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/com.limelight/), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
## Requirements
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
* Android device running 4.1 (Jelly Bean) or higher
* High-end wireless router (802.11n dual-band recommended)
## Usage
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* When on the same network as your PC, open Moonlight and tap on your PC in the list
* Accept the pairing confirmation on your PC and add the PIN if needed
* Tap your PC again to view the list of apps to stream
* Play games!
## Contribute
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
1. Fork us
2. Write code
3. Send Pull Requests
## Downloads
* [Google Play Store](https://play.google.com/store/apps/details?id=com.limelight)
* [Amazon App Store](https://www.amazon.com/gp/product/B00JK4MFN2)
* [F-Droid](https://f-droid.org/packages/com.limelight)
* [APK](https://github.com/moonlight-stream/moonlight-android/releases)
## Building
* Install Android Studio and the Android NDK
* Run git submodule update --init --recursive from within moonlight-android/
* In moonlight-android/, create a file called local.properties. Add an ndk.dir= property to the local.properties file and set it equal to your NDK directory.
* Build the APK using Android Studio
* Build the APK using Android Studio or gradle
## Authors

View File

@@ -1,14 +1,14 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
targetSdkVersion 30
versionName "9.2.1"
versionCode = 223
versionName "9.6.4"
versionCode = 237
}
flavorDimensions "root"

View File

@@ -42,6 +42,7 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:installLocation="auto"
android:gwpAsanMode="always"
android:theme="@style/AppTheme">
<provider
android:name=".PosterContentProvider"
@@ -120,7 +121,8 @@
android:resizeableActivity="true"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:theme="@style/StreamTheme">
android:theme="@style/StreamTheme"
android:preferMinimalPostProcessing="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.AppView" />

View File

@@ -434,8 +434,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
return true;
case VIEW_DETAILS_ID:
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details), app.app.toString(), false);
return true;
case CREATE_SHORTCUT_ID:
@@ -565,9 +564,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view);
return PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view;
}
@Override
@@ -592,7 +590,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
listView.requestFocus();
}
public class AppObject {
public static class AppObject {
public final NvApp app;
public boolean isRunning;

View File

@@ -6,9 +6,11 @@ import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.binding.input.capture.InputCaptureManager;
import com.limelight.binding.input.capture.InputCaptureProvider;
import com.limelight.binding.input.TouchContext;
import com.limelight.binding.input.touch.AbsoluteTouchContext;
import com.limelight.binding.input.touch.RelativeTouchContext;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.binding.input.evdev.EvdevListener;
import com.limelight.binding.input.touch.TouchContext;
import com.limelight.binding.input.virtual_controller.VirtualController;
import com.limelight.binding.video.CrashListener;
import com.limelight.binding.video.MediaCodecDecoderRenderer;
@@ -61,6 +63,7 @@ import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.View.OnGenericMotionListener;
@@ -210,11 +213,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
prefConfig = PreferenceConfiguration.readPreferences(this);
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && prefConfig.stretchVideo) {
if (prefConfig.stretchVideo) {
// Allow the activity to layout under notches if the fill-screen option
// was turned on by the user
getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
}
// Listen for events on the game surface
@@ -474,9 +483,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i,
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
streamView);
if (!prefConfig.touchscreenTrackpad) {
touchContextMap[i] = new AbsoluteTouchContext(conn, i, streamView);
}
else {
touchContextMap[i] = new RelativeTouchContext(conn, i,
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
streamView);
}
}
// Use sustained performance mode on N+ to ensure consistent
@@ -602,29 +616,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
// FIXME: Remove when Android R SDK is finalized
private static void setPreferMinimalPostProcessingWithReflection(WindowManager.LayoutParams windowLayoutParams, boolean isPreferred) {
// Build.VERSION.PREVIEW_SDK_INT was added in M
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && Build.VERSION.PREVIEW_SDK_INT == 0) {
// Don't attempt this reflection unless on Android R Developer Preview
return;
}
}
else {
return;
}
try {
Field field = windowLayoutParams.getClass().getDeclaredField("preferMinimalPostProcessing");
field.set(windowLayoutParams, isPreferred);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private float prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
@@ -702,7 +693,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Enable HDMI ALLM (game mode) on Android R
setPreferMinimalPostProcessingWithReflection(windowLayoutParams, true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowLayoutParams.preferMinimalPostProcessing = true;
}
// Apply the display mode change
getWindow().setAttributes(windowLayoutParams);
@@ -858,34 +851,36 @@ public class Game extends Activity implements SurfaceHolder.Callback,
displayedFailureDialog = true;
stopConnection();
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
String message = null;
if (averageEndToEndLat > 0) {
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
if (averageDecoderLat > 0) {
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
if (prefConfig.enableLatencyToast) {
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
String message = null;
if (averageEndToEndLat > 0) {
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
if (averageDecoderLat > 0) {
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
}
}
else if (averageDecoderLat > 0) {
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
}
}
else if (averageDecoderLat > 0) {
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
}
// Add the video codec to the post-stream toast
if (message != null) {
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
message += " [H.265 HDR]";
// Add the video codec to the post-stream toast
if (message != null) {
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
message += " [H.265 HDR]";
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
message += " [H.265]";
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
message += " [H.264]";
}
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
message += " [H.265]";
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
message += " [H.264]";
}
}
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
// Clear the tombstone count if we terminated normally
@@ -1303,6 +1298,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
if (view == null && !prefConfig.touchscreenTrackpad) {
// Absolute touch events should be dropped outside our view.
return true;
}
int actionIndex = event.getActionIndex();
int eventX = (int)event.getX(actionIndex);
@@ -1332,7 +1332,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
context.touchDownEvent(eventX, eventY);
for (TouchContext touchContext : touchContextMap) {
touchContext.setPointerCount(event.getPointerCount());
}
context.touchDownEvent(eventX, eventY, true);
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
@@ -1345,9 +1348,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
context.touchUpEvent(eventX, eventY);
for (TouchContext touchContext : touchContextMap) {
touchContext.setPointerCount(event.getPointerCount() - 1);
}
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
// The original secondary touch now becomes primary
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
context.touchDownEvent((int)event.getX(1), (int)event.getY(1), false);
}
break;
case MotionEvent.ACTION_MOVE:
@@ -1379,6 +1385,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case MotionEvent.ACTION_CANCEL:
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
aTouchContext.setPointerCount(0);
}
break;
default:
@@ -1538,9 +1545,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Display the error dialog if it was an unexpected termination.
// Otherwise, just finish the activity immediately.
if (errorCode != 0) {
if (errorCode != MoonBridge.ML_ERROR_GRACEFUL_TERMINATION) {
String message;
switch (errorCode) {
case MoonBridge.ML_ERROR_NO_VIDEO_TRAFFIC:
message = getResources().getString(R.string.no_video_received_error);
break;
default:
message = getResources().getString(R.string.conn_terminated_msg);
break;
}
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
message, true);
}
else {
finish();
@@ -1660,6 +1679,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void surfaceCreated(SurfaceHolder holder) {
surfaceCreated = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Tell the OS about our frame rate to allow it to adapt the display refresh rate appropriately
holder.getSurface().setFrameRate(prefConfig.fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
}
}
@Override

View File

@@ -692,9 +692,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
return R.layout.pc_grid_view;
}
@Override
@@ -721,7 +719,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
registerForContextMenu(listView);
}
public class ComputerObject {
public static class ComputerObject {
public ComputerDetails details;
public ComputerObject(ComputerDetails details) {

View File

@@ -102,9 +102,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
@@ -124,10 +122,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
Date now = new Date();
@@ -152,8 +148,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
throw new RuntimeException(e);
}

View File

@@ -40,12 +40,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
private static final int EMULATING_SPECIAL = 0x1;
private static final int EMULATING_SELECT = 0x2;
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
private final Vector2d inputVector = new Vector2d();
private final SparseArray<InputDeviceContext> inputDeviceContexts = new SparseArray<>();
@@ -827,6 +821,43 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
// Override mode button for 8BitDo controllers
if (context.vendorId == 0x2dc8 && event.getScanCode() == 306) {
return KeyEvent.KEYCODE_BUTTON_MODE;
}
if ((context.vendorId == 0x057e && context.productId == 0x2009) || // Switch Pro controller
(context.vendorId == 0x0f0d && context.productId == 0x00c1)) { // HORIPAD for Switch
switch (event.getScanCode()) {
case 0x130:
return KeyEvent.KEYCODE_BUTTON_A;
case 0x131:
return KeyEvent.KEYCODE_BUTTON_B;
case 0x132:
return KeyEvent.KEYCODE_BUTTON_X;
case 0x133:
return KeyEvent.KEYCODE_BUTTON_Y;
case 0x134:
return KeyEvent.KEYCODE_BUTTON_L1;
case 0x135:
return KeyEvent.KEYCODE_BUTTON_R1;
case 0x136:
return KeyEvent.KEYCODE_BUTTON_L2;
case 0x137:
return KeyEvent.KEYCODE_BUTTON_R2;
case 0x138:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 0x139:
return KeyEvent.KEYCODE_BUTTON_START;
case 0x13A:
return KeyEvent.KEYCODE_BUTTON_THUMBL;
case 0x13B:
return KeyEvent.KEYCODE_BUTTON_THUMBR;
case 0x13D:
return KeyEvent.KEYCODE_BUTTON_MODE;
}
}
if (context.usesLinuxGamepadStandardFaceButtons) {
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
switch (event.getScanCode()) {
@@ -991,6 +1022,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return keyCode;
}
private int handleFlipFaceButtons(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_A:
return KeyEvent.KEYCODE_BUTTON_B;
case KeyEvent.KEYCODE_BUTTON_B:
return KeyEvent.KEYCODE_BUTTON_A;
case KeyEvent.KEYCODE_BUTTON_X:
return KeyEvent.KEYCODE_BUTTON_Y;
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_X;
default:
return keyCode;
}
}
private Vector2d populateCachedVector(float x, float y) {
// Reinitialize our cached Vector2d object
inputVector.initialize(x, y);
@@ -1256,6 +1302,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
int keyCode = handleRemapping(context, event);
if (prefConfig.flipFaceButtons) {
keyCode = handleFlipFaceButtons(keyCode);
}
if (keyCode == 0) {
return true;
}
@@ -1365,41 +1416,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return false;
}
// Check if we're emulating the select button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{
// If either start or LB is up, select comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
{
context.inputMap &= ~ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
} catch (InterruptedException ignored) {}
}
}
// Check if we're emulating the special button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{
// If either start or select and RB is up, the special button comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
{
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
} catch (InterruptedException ignored) {}
}
}
sendControllerInputPacket(context);
if (context.pendingExit && context.inputMap == 0) {
@@ -1417,6 +1433,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
int keyCode = handleRemapping(context, event);
if (prefConfig.flipFaceButtons) {
keyCode = handleFlipFaceButtons(keyCode);
}
if (keyCode == 0) {
return true;
}
@@ -1514,29 +1535,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.pendingExit = true;
}
// Start+LB acts like select for controllers with one button
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
context.inputMap |= ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
}
// We detect select+start or start+RB as the special button combo
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG) ||
context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
// We don't need to send repeat key down events, but the platform
// sends us events that claim to be repeats but they're from different
// devices, so we just send them all and deal with some duplicates.
@@ -1618,7 +1616,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
usbDeviceContexts.put(controller.getControllerId(), context);
}
class GenericControllerContext {
static class GenericControllerContext {
public int id;
public boolean external;
@@ -1682,8 +1680,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public boolean hasJoystickAxes;
public boolean pendingExit;
public int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive

View File

@@ -25,6 +25,7 @@ public class XboxOneController extends AbstractXboxController {
};
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
private static final byte[] ONE_S_INIT = {0x05, 0x20, 0x00, 0x0f, 0x06};
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
0x00, 0x00, 0x00, (byte)0x80, 0x00};
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
@@ -38,6 +39,8 @@ public class XboxOneController extends AbstractXboxController {
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
new InitPacket(0x0000, 0x0000, FW2015_INIT),
new InitPacket(0x045e, 0x02ea, ONE_S_INIT),
new InitPacket(0x045e, 0x0b00, ONE_S_INIT),
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),

View File

@@ -0,0 +1,272 @@
package com.limelight.binding.input.touch;
import android.os.SystemClock;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class AbsoluteTouchContext implements TouchContext {
private int lastTouchDownX = 0;
private int lastTouchDownY = 0;
private long lastTouchDownTime = 0;
private int lastTouchUpX = 0;
private int lastTouchUpY = 0;
private long lastTouchUpTime = 0;
private int lastTouchLocationX = 0;
private int lastTouchLocationY = 0;
private boolean cancelled;
private boolean confirmedLongPress;
private boolean confirmedTap;
private Timer longPressTimer;
private Timer tapDownTimer;
private float accumulatedScrollDelta;
private final NvConnection conn;
private final int actionIndex;
private final View targetView;
private static final int SCROLL_SPEED_DIVISOR = 20;
private static final int LONG_PRESS_TIME_THRESHOLD = 650;
private static final int LONG_PRESS_DISTANCE_THRESHOLD = 30;
private static final int DOUBLE_TAP_TIME_THRESHOLD = 250;
private static final int DOUBLE_TAP_DISTANCE_THRESHOLD = 60;
private static final int TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD = 100;
private static final int TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD = 20;
public AbsoluteTouchContext(NvConnection conn, int actionIndex, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.targetView = view;
}
@Override
public int getActionIndex()
{
return actionIndex;
}
@Override
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
{
if (!isNewFinger) {
// We don't handle finger transitions for absolute mode
return true;
}
lastTouchLocationX = lastTouchDownX = eventX;
lastTouchLocationY = lastTouchDownY = eventY;
lastTouchDownTime = SystemClock.uptimeMillis();
cancelled = confirmedTap = confirmedLongPress = false;
accumulatedScrollDelta = 0;
if (actionIndex == 0) {
// Start the timers
startTapDownTimer();
startLongPressTimer();
}
return true;
}
private boolean distanceExceeds(int deltaX, int deltaY, double limit) {
return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) > limit;
}
private void updatePosition(int eventX, int eventY) {
// We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT.
// Normalize these to the view size. We can't just drop them because we won't always get an event
// right at the boundary of the view, so dropping them would result in our cursor never really
// reaching the sides of the screen.
eventX = Math.min(Math.max(eventX, 0), targetView.getWidth());
eventY = Math.min(Math.max(eventY, 0), targetView.getHeight());
conn.sendMousePosition((short)eventX, (short)eventY, (short)targetView.getWidth(), (short)targetView.getHeight());
}
@Override
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
return;
}
if (actionIndex == 0) {
// Cancel the timers
cancelLongPressTimer();
cancelTapDownTimer();
// Raise the mouse buttons that we currently have down
if (confirmedLongPress) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
else if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
else {
// If we get here, this means that the tap completed within the touch down
// deadzone time. We'll need to send the touch down and up events now at the
// original touch down position.
tapConfirmed();
try {
// FIXME: Sleeping on the main thread sucks
Thread.sleep(50);
} catch (InterruptedException ignored) {}
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
lastTouchLocationX = lastTouchUpX = eventX;
lastTouchLocationY = lastTouchUpY = eventY;
lastTouchUpTime = SystemClock.uptimeMillis();
}
private synchronized void startLongPressTimer() {
longPressTimer = new Timer(true);
longPressTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (longPressTimer == null) {
return;
}
// Uncancellable now
longPressTimer = null;
// This timer should have already expired, but cancel it just in case
cancelTapDownTimer();
// Switch from a left click to a right click after a long press
confirmedLongPress = true;
if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
}
}, LONG_PRESS_TIME_THRESHOLD);
}
private synchronized void cancelLongPressTimer() {
if (longPressTimer != null) {
longPressTimer.cancel();
longPressTimer = null;
}
}
private synchronized void startTapDownTimer() {
tapDownTimer = new Timer(true);
tapDownTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (tapDownTimer == null) {
return;
}
// Uncancellable now
tapDownTimer = null;
// Start our tap
tapConfirmed();
}
}
}, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
}
private synchronized void cancelTapDownTimer() {
if (tapDownTimer != null) {
tapDownTimer.cancel();
tapDownTimer = null;
}
}
private void tapConfirmed() {
if (confirmedTap || confirmedLongPress) {
return;
}
confirmedTap = true;
cancelTapDownTimer();
// Left button down at original position
if (lastTouchDownTime - lastTouchUpTime > DOUBLE_TAP_TIME_THRESHOLD ||
distanceExceeds(lastTouchDownX - lastTouchUpX, lastTouchDownY - lastTouchUpY, DOUBLE_TAP_DISTANCE_THRESHOLD)) {
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
updatePosition(lastTouchDownX, lastTouchDownY);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
@Override
public boolean touchMoveEvent(int eventX, int eventY)
{
if (cancelled) {
return true;
}
if (actionIndex == 0) {
if (distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, LONG_PRESS_DISTANCE_THRESHOLD)) {
// Moved too far since touch down. Cancel the long press timer.
cancelLongPressTimer();
}
// Ignore motion within the deadzone period after touch down
if (confirmedTap || distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD)) {
tapConfirmed();
updatePosition(eventX, eventY);
}
}
else if (actionIndex == 1) {
accumulatedScrollDelta += (eventY - lastTouchLocationY) / (float)SCROLL_SPEED_DIVISOR;
if ((short)accumulatedScrollDelta != 0) {
conn.sendMouseHighResScroll((short)accumulatedScrollDelta);
accumulatedScrollDelta -= (short)accumulatedScrollDelta;
}
}
lastTouchLocationX = eventX;
lastTouchLocationY = eventY;
return true;
}
@Override
public void cancelTouch() {
cancelled = true;
// Cancel the timers
cancelLongPressTimer();
cancelTapDownTimer();
// Raise the mouse buttons
if (confirmedLongPress) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
else if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setPointerCount(int pointerCount) {
if (actionIndex == 0 && pointerCount > 1) {
cancelTouch();
}
}
}

View File

@@ -1,5 +1,6 @@
package com.limelight.binding.input;
package com.limelight.binding.input.touch;
import android.os.SystemClock;
import android.view.View;
import com.limelight.nvstream.NvConnection;
@@ -8,7 +9,7 @@ import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class TouchContext {
public class RelativeTouchContext implements TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
@@ -32,8 +33,8 @@ public class TouchContext {
private static final int TAP_TIME_THRESHOLD = 250;
private static final int DRAG_TIME_THRESHOLD = 650;
public TouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight, View view)
public RelativeTouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
@@ -42,6 +43,7 @@ public class TouchContext {
this.targetView = view;
}
@Override
public int getActionIndex()
{
return actionIndex;
@@ -57,7 +59,7 @@ public class TouchContext {
private boolean isTap()
{
long timeDelta = System.currentTimeMillis() - originalTouchTime;
long timeDelta = SystemClock.uptimeMillis() - originalTouchTime;
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
}
@@ -72,7 +74,8 @@ public class TouchContext {
}
}
public boolean touchDownEvent(int eventX, int eventY)
@Override
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
{
// Get the view dimensions to scale inputs on this touch
xFactor = referenceWidth / (double)targetView.getWidth();
@@ -80,7 +83,7 @@ public class TouchContext {
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
originalTouchTime = SystemClock.uptimeMillis();
cancelled = confirmedDrag = confirmedMove = false;
distanceMoved = 0;
@@ -92,6 +95,7 @@ public class TouchContext {
return true;
}
@Override
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
@@ -128,7 +132,7 @@ public class TouchContext {
dragTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (TouchContext.this) {
synchronized (RelativeTouchContext.this) {
// Check if someone already set move
if (confirmedMove) {
return;
@@ -179,6 +183,7 @@ public class TouchContext {
}
}
@Override
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
@@ -223,6 +228,7 @@ public class TouchContext {
return true;
}
@Override
public void cancelTouch() {
cancelled = true;
@@ -235,7 +241,11 @@ public class TouchContext {
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setPointerCount(int pointerCount) {}
}

View File

@@ -0,0 +1,11 @@
package com.limelight.binding.input.touch;
public interface TouchContext {
int getActionIndex();
void setPointerCount(int pointerCount);
boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger);
boolean touchMoveEvent(int eventX, int eventY);
void touchUpEvent(int eventX, int eventY);
void cancelTouch();
boolean isCancelled();
}

View File

@@ -210,7 +210,7 @@ public class AnalogStick extends VirtualControllerElement {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// calculate new radius sizes depending
radius_complete = getPercent(getCorrectWidth() / 2, 100);
radius_complete = getPercent(getCorrectWidth() / 2, 100) - 2 * getDefaultStrokeWidth();
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);

View File

@@ -22,7 +22,7 @@ import java.util.Timer;
import java.util.TimerTask;
public class VirtualController {
public class ControllerInputContext {
public static class ControllerInputContext {
public short inputMap = 0x0000;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;

View File

@@ -167,11 +167,11 @@ public class VirtualControllerConfigurationLoader {
private static final int DPAD_BASE_Y = 41;
private static final int DPAD_SIZE = 30;
private static final int ANALOG_L_BASE_X = 4;
private static final int ANALOG_L_BASE_Y = 1;
private static final int ANALOG_R_BASE_X = 96;
private static final int ANALOG_L_BASE_X = 6;
private static final int ANALOG_L_BASE_Y = 4;
private static final int ANALOG_R_BASE_X = 98;
private static final int ANALOG_R_BASE_Y = 42;
private static final int ANALOG_SIZE = 28;
private static final int ANALOG_SIZE = 26;
private static final int L3_R3_BASE_Y = 60;
@@ -205,7 +205,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_A,
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
!config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
!config.flipFaceButtons ? "A" : "B", -1, controller, context),
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + 2 * BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -214,7 +215,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_B,
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
config.flipFaceButtons ? "A" : "B", -1, controller, context),
screenScale(BUTTON_BASE_X + BUTTON_SIZE, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -223,7 +225,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_X,
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
!config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
!config.flipFaceButtons ? "X" : "Y", -1, controller, context),
screenScale(BUTTON_BASE_X - BUTTON_SIZE, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -232,7 +235,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_Y,
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
config.flipFaceButtons ? "X" : "Y", -1, controller, context),
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y, height),
screenScale(BUTTON_SIZE, height),

View File

@@ -318,8 +318,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
if (lowLatency) {
videoFormat.setInteger(MediaCodecHelper.KEY_LOW_LATENCY, 1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && lowLatency) {
videoFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Set the Qualcomm vendor low latency extension if the Android R option is unavailable
@@ -334,11 +334,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
}
// Operate at maximum rate to lower latency as much as possible on
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
// but that will actually result in the decoder crashing if it can't satisfy
// our (ludicrous) operating rate requirement.
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
if (MediaCodecHelper.decoderSupportsMaxOperatingRate(selectedDecoderName)) {
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
}
}
configuredFormat = videoFormat;

View File

@@ -40,10 +40,6 @@ public class MediaCodecHelper {
private static final List<String> blacklisted59FpsDecoderPrefixes;
private static final List<String> qualcommDecoderPrefixes;
// FIXME: Remove when Android R SDK is finalized
public static final String FEATURE_LowLatency = "low-latency";
public static final String KEY_LOW_LATENCY = "low-latency";
private static boolean isLowEndSnapdragon = false;
private static boolean initialized = false;
@@ -343,10 +339,9 @@ public class MediaCodecHelper {
}
public static boolean decoderSupportsLowLatency(MediaCodecInfo decoderInfo, String mimeType) {
// KitKat added CodecCapabilities.isFeatureSupported()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
if (decoderInfo.getCapabilitiesForType(mimeType).isFeatureSupported(FEATURE_LowLatency)) {
if (decoderInfo.getCapabilitiesForType(mimeType).isFeatureSupported(CodecCapabilities.FEATURE_LowLatency)) {
LimeLog.info("Low latency decoding mode supported (FEATURE_LowLatency)");
return true;
}
@@ -359,6 +354,21 @@ public class MediaCodecHelper {
return false;
}
public static boolean decoderSupportsMaxOperatingRate(String decoderName) {
// Operate at maximum rate to lower latency as much as possible on
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
// but that will actually result in the decoder crashing if it can't satisfy
// our (ludicrous) operating rate requirement. This seems to cause reliable
// crashes on the Xiaomi Mi 10 lite 5G on Android 10, so we'll disable it
// on that device and all non-Qualcomm devices to be safe.
//
// NB: Even on Android 10, this optimization still provides significant
// performance gains on Pixel 2.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
isDecoderInList(qualcommDecoderPrefixes, decoderName) &&
!Build.DEVICE.equalsIgnoreCase("monet");
}
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo, String mimeType) {
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {

View File

@@ -2,6 +2,7 @@ package com.limelight.grid;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -40,10 +41,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
}
else if (prefs.smallIconMode) {
if (prefs.smallIconMode) {
return R.layout.app_grid_item_small;
}
else {
@@ -113,30 +111,17 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}
@Override
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
public void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView, prgView);
return true;
}
loader.populateImageView(obj.app, imgView, txtView);
@Override
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
// Select the text view so it starts marquee mode
txtView.setSelected(true);
// Return false to use the app's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
if (obj.isRunning) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.ic_play);
return true;
overlayView.setVisibility(View.VISIBLE);
}
else {
overlayView.setVisibility(View.GONE);
}
// No overlay
return false;
}
}

View File

@@ -10,7 +10,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.ArrayList;
@@ -55,9 +54,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
return i;
}
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
public abstract boolean populateTextView(TextView txtView, T obj);
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
public abstract void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, T obj);
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
@@ -70,22 +67,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
TextView txtView = convertView.findViewById(R.id.grid_text);
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
if (imgView != null) {
if (!populateImageView(imgView, prgView, itemList.get(i))) {
imgView.setImageBitmap(null);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
}
populateView(imgView, prgView, txtView, overlayView, itemList.get(i));
return convertView;
}

View File

@@ -22,15 +22,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
}
else if (prefs.smallIconMode) {
return R.layout.pc_grid_item_small;
}
else {
return R.layout.pc_grid_item;
}
return R.layout.pc_grid_item;
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
@@ -57,7 +49,8 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
}
@Override
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
public void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, PcView.ComputerObject obj) {
imgView.setImageResource(R.drawable.ic_computer);
if (obj.details.state == ComputerDetails.State.ONLINE) {
imgView.setAlpha(1.0f);
}
@@ -72,12 +65,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
prgView.setVisibility(View.INVISIBLE);
}
imgView.setImageResource(R.drawable.ic_computer);
return true;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
txtView.setText(obj.details.name);
if (obj.details.state == ComputerDetails.State.ONLINE) {
txtView.setAlpha(1.0f);
}
@@ -85,16 +73,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
txtView.setAlpha(0.4f);
}
// Return false to use the computer's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.OFFLINE) {
overlayView.setImageResource(R.drawable.ic_pc_offline);
overlayView.setAlpha(0.4f);
return true;
overlayView.setVisibility(View.VISIBLE);
}
// We must check if the status is exactly online and unpaired
// to avoid colliding with the loading spinner when status is unknown
@@ -102,8 +84,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
overlayView.setImageResource(R.drawable.ic_lock);
overlayView.setAlpha(1.0f);
return true;
overlayView.setVisibility(View.VISIBLE);
}
else {
overlayView.setVisibility(View.GONE);
}
return false;
}
}

View File

@@ -6,9 +6,12 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
@@ -89,7 +92,7 @@ public class CachedAppAssetLoader {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
private ScaledBitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
@@ -110,7 +113,7 @@ public class CachedAppAssetLoader {
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
return bmp;
}
@@ -132,29 +135,29 @@ public class CachedAppAssetLoader {
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private class LoaderTask extends AsyncTask<LoaderTuple, Void, ScaledBitmap> {
private final WeakReference<ImageView> imageViewRef;
private final WeakReference<ProgressBar> progressViewRef;
private final WeakReference<TextView> textViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
public LoaderTask(ImageView imageView, TextView textView, boolean diskOnly) {
this.imageViewRef = new WeakReference<>(imageView);
this.progressViewRef = new WeakReference<>(prgView);
this.textViewRef = new WeakReference<>(textView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
protected ScaledBitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the views are gone
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
if (isCancelled() || imageViewRef.get() == null || textViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
@@ -183,45 +186,61 @@ public class CachedAppAssetLoader {
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
final ProgressBar prgView = progressViewRef.get();
final TextView textView = textViewRef.get();
if (getLoaderTask(imageView) == this) {
// Now display the progress bar since we have to hit the network
if (prgView != null) {
prgView.setVisibility(View.VISIBLE);
}
// Set off another loader task on the network executor. This time our AsyncDrawable
// will use the app image placeholder bitmap, rather than an empty bitmap.
LoaderTask task = new LoaderTask(imageView, prgView, false);
LoaderTask task = new LoaderTask(imageView, textView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
imageView.setVisibility(View.VISIBLE);
imageView.setImageDrawable(asyncDrawable);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
imageView.setVisibility(View.VISIBLE);
textView.setVisibility(View.VISIBLE);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
protected void onPostExecute(final ScaledBitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
final ProgressBar prgView = progressViewRef.get();
final TextView textView = textViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
// Fade in the box art
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Show the text if it's a placeholder
textView.setVisibility(isBitmapPlaceholder(bitmap) ? View.VISIBLE : View.GONE);
// Hide the progress bar
if (prgView != null) {
prgView.setVisibility(View.INVISIBLE);
}
if (imageView.getVisibility() == View.VISIBLE) {
// Fade out the placeholder first
Animation fadeOutAnimation = AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadeout);
fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
// Show the view
imageView.setVisibility(View.VISIBLE);
@Override
public void onAnimationEnd(Animation animation) {
// Fade in the new box art
imageView.setImageBitmap(bitmap.bitmap);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
imageView.startAnimation(fadeOutAnimation);
}
else {
// View is invisible already, so just fade in the new art
imageView.setImageBitmap(bitmap.bitmap);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
imageView.setVisibility(View.VISIBLE);
}
}
}
}
}
@@ -299,7 +318,13 @@ public class CachedAppAssetLoader {
});
}
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
private boolean isBitmapPlaceholder(ScaledBitmap bitmap) {
return (bitmap == null) ||
(bitmap.originalWidth == 130 && bitmap.originalHeight == 180) || // GFE 2.0
(bitmap.originalWidth == 628 && bitmap.originalHeight == 888); // GFE 3.0
}
public boolean populateImageView(NvApp app, ImageView imgView, TextView textView) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
@@ -309,22 +334,26 @@ public class CachedAppAssetLoader {
return true;
}
// Hide the progress bar always on initial load
prgView.setVisibility(View.INVISIBLE);
// Always set the name text so we have it if needed later
textView.setText(app.getAppName());
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
ScaledBitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
imgView.setVisibility(View.VISIBLE);
imgView.setImageBitmap(bmp);
imgView.setImageBitmap(bmp.bitmap);
// Show the text if it's a placeholder bitmap
textView.setVisibility(isBitmapPlaceholder(bmp) ? View.VISIBLE : View.GONE);
return true;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(imgView, prgView, true);
final LoaderTask task = new LoaderTask(imgView, textView, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
textView.setVisibility(View.INVISIBLE);
imgView.setVisibility(View.INVISIBLE);
imgView.setImageDrawable(asyncDrawable);
@@ -333,7 +362,7 @@ public class CachedAppAssetLoader {
return false;
}
public class LoaderTuple {
public static class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;

View File

@@ -64,7 +64,7 @@ public class DiskAssetLoader {
return inSampleSize;
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
// Don't bother with anything if it doesn't exist
@@ -110,27 +110,33 @@ public class DiskAssetLoader {
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bmp != null) {
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
return new ScaledBitmap(decodeOnlyOptions.outWidth, decodeOnlyOptions.outHeight, bmp);
}
}
else {
// On P, we can get a bitmap back in one step with ImageDecoder
final ScaledBitmap scaledBitmap = new ScaledBitmap();
try {
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
scaledBitmap.bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
@Override
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
scaledBitmap.originalWidth = imageInfo.getSize().getWidth();
scaledBitmap.originalHeight = imageInfo.getSize().getHeight();
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
if (isLowRamDevice) {
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
}
}
});
return scaledBitmap;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return bmp;
return null;
}
public File getFile(String computerUuid, int appId) {

View File

@@ -1,37 +1,74 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.util.LruCache;
import com.limelight.LimeLog;
import java.lang.ref.SoftReference;
import java.util.HashMap;
public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
private static final LruCache<String, ScaledBitmap> memoryCache = new LruCache<String, ScaledBitmap>(maxMemory / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
protected int sizeOf(String key, ScaledBitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
return bitmap.bitmap.getByteCount() / 1024;
}
@Override
protected void entryRemoved(boolean evicted, String key, ScaledBitmap oldValue, ScaledBitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
if (evicted) {
// Keep a soft reference around to the bitmap as long as we can
evictionCache.put(key, new SoftReference<>(oldValue));
}
}
};
private static final HashMap<String, SoftReference<ScaledBitmap>> evictionCache = new HashMap<>();
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
final String key = constructKey(tuple);
ScaledBitmap bmp = memoryCache.get(key);
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
LimeLog.info("LRU cache hit for tuple: "+tuple);
return bmp;
}
return bmp;
SoftReference<ScaledBitmap> bmpRef = evictionCache.get(key);
if (bmpRef != null) {
bmp = bmpRef.get();
if (bmp != null) {
LimeLog.info("Eviction cache hit for tuple: "+tuple);
// Put this entry back into the LRU cache
evictionCache.remove(key);
memoryCache.put(key, bmp);
return bmp;
}
else {
// The data is gone, so remove the dangling SoftReference now
evictionCache.remove(key);
}
}
return null;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, ScaledBitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
// We must evict first because that will push all items into the eviction cache
memoryCache.evictAll();
evictionCache.clear();
}
}

View File

@@ -0,0 +1,18 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
public class ScaledBitmap {
public int originalWidth;
public int originalHeight;
public Bitmap bitmap;
public ScaledBitmap() {}
public ScaledBitmap(int originalWidth, int originalHeight, Bitmap bitmap) {
this.originalWidth = originalWidth;
this.originalHeight = originalHeight;
this.bitmap = bitmap;
}
}

View File

@@ -43,25 +43,26 @@ public class NvConnection {
this.context = new ConnectionContext();
this.context.streamConfig = config;
this.context.serverCert = serverCert;
try {
// This is unique per connection
this.context.riKey = generateRiAesKey();
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
}
this.context.riKeyId = generateRiKeyId();
// This is unique per connection
this.context.riKey = generateRiAesKey();
context.riKeyId = generateRiKeyId();
this.isMonkey = ActivityManager.isUserAMonkey();
}
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
// RI keys are 128 bits
keyGen.init(128);
return keyGen.generateKey();
private static SecretKey generateRiAesKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
// RI keys are 128 bits
keyGen.init(128);
return keyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static int generateRiKeyId() {
@@ -238,6 +239,7 @@ public class NvConnection {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, e.getErrorCode());
return;
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
@@ -348,6 +350,12 @@ public class NvConnection {
}
}
public void sendMouseHighResScroll(final short scrollAmount) {
if (!isMonkey) {
MoonBridge.sendMouseHighResScroll(scrollAmount);
}
}
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
}

View File

@@ -69,9 +69,9 @@ public class ComputerDetails {
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("Name: ").append(name).append("\n");
str.append("State: ").append(state).append("\n");
str.append("Active Address: ").append(activeAddress).append("\n");
str.append("Name: ").append(name).append("\n");
str.append("UUID: ").append(uuid).append("\n");
str.append("Local Address: ").append(localAddress).append("\n");
str.append("Remote Address: ").append(remoteAddress).append("\n");

View File

@@ -58,4 +58,13 @@ public class NvApp {
public boolean isInitialized() {
return this.initialized;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("Name: ").append(appName).append("\n");
str.append("HDR: ").append(hdrSupported ? "Yes" : "No").append("\n");
str.append("ID: ").append(appId).append("\n");
return str.toString();
}
}

View File

@@ -1,20 +1,26 @@
package com.limelight.nvstream.http;
import android.os.Build;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
@@ -24,11 +30,16 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
@@ -42,7 +53,6 @@ import com.limelight.nvstream.ConnectionContext;
import com.limelight.nvstream.http.PairingManager.PairState;
import okhttp3.ConnectionPool;
import okhttp3.Handshake;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@@ -66,34 +76,36 @@ public class NvHTTP {
private OkHttpClient httpClient;
private OkHttpClient httpClientWithReadTimeout;
private X509TrustManager defaultTrustManager;
private X509TrustManager trustManager;
private X509KeyManager keyManager;
private X509Certificate serverCert;
void setServerCert(X509Certificate serverCert) {
this.serverCert = serverCert;
trustManager = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
throw new IllegalStateException("Should never be called");
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
// Check the server certificate if we've paired to this host
if (!certs[0].equals(NvHTTP.this.serverCert)) {
throw new CertificateException("Certificate mismatch");
}
}
};
}
private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) {
// Set up TrustManager
setServerCert(serverCert);
private static X509TrustManager getDefaultTrustManager() {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
return (X509TrustManager) tm;
}
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
throw new IllegalStateException("No X509 trust manager found");
}
private void initializeHttpState(final LimelightCryptoProvider cryptoProvider) {
keyManager = new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { return "Limelight-RSA"; }
@@ -109,9 +121,51 @@ public class NvHTTP {
public String[] getServerAliases(String keyType, Principal[] issuers) { return null; }
};
// Ignore differences between given hostname and certificate hostname
defaultTrustManager = getDefaultTrustManager();
trustManager = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
throw new IllegalStateException("Should never be called");
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
try {
// Try the default trust manager first to allow pairing with certificates
// that chain up to a trusted root CA. This will raise CertificateException
// if the certificate is not trusted (expected for GFE's self-signed certs).
defaultTrustManager.checkServerTrusted(certs, authType);
} catch (CertificateException e) {
// Check the server certificate if we've paired to this host
if (certs.length == 1 && NvHTTP.this.serverCert != null) {
if (!certs[0].equals(NvHTTP.this.serverCert)) {
throw new CertificateException("Certificate mismatch");
}
}
else {
// The cert chain doesn't look like a self-signed cert or we don't have
// a certificate pinned, so re-throw the original validation error.
throw e;
}
}
}
};
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) { return true; }
public boolean verify(String hostname, SSLSession session) {
try {
Certificate[] certificates = session.getPeerCertificates();
if (certificates.length == 1 && certificates[0].equals(NvHTTP.this.serverCert)) {
// Allow any hostname if it's our pinned cert
return true;
}
} catch (SSLPeerUnverifiedException e) {
e.printStackTrace();
}
// Fall back to default HostnameVerifier for validating CA-issued certs
return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
}
};
httpClient = new OkHttpClient.Builder()
@@ -131,7 +185,9 @@ public class NvHTTP {
// started by other Moonlight clients.
this.uniqueId = "0123456789ABCDEF";
initializeHttpState(serverCert, cryptoProvider);
this.serverCert = serverCert;
initializeHttpState(cryptoProvider);
try {
// The URI constructor takes care of escaping IPv6 literals
@@ -186,20 +242,20 @@ public class NvHTTP {
}
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
String statusCodeText = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code");
if (statusCodeText == null) {
throw new GfeHttpResponseException(418, "Status code is missing");
}
try {
int statusCode = Integer.parseInt(statusCodeText);
if (statusCode != 200) {
throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message"));
// We use Long.parseLong() because in rare cases GFE can send back a status code of
// 0xFFFFFFFF, which will cause Integer.parseInt() to throw a NumberFormatException due
// to exceeding Integer.MAX_VALUE. We'll get the desired error code of -1 by just casting
// the resulting long into an int.
int statusCode = (int)Long.parseLong(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
if (statusCode != 200) {
String statusMsg = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message");
if (statusCode == -1 && "Invalid".equals(statusMsg)) {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
statusCode = 418;
statusMsg = "Missing audio capture device. Reinstall GeForce Experience.";
}
}
catch (NumberFormatException e) {
// It seems like GFE 3.20.3.63 is returning garbage for status_code in rare cases.
// Surface this in a more friendly way rather than crashing.
throw new GfeHttpResponseException(418, "Status code is not a number: "+statusCodeText);
throw new GfeHttpResponseException(statusCode, statusMsg);
}
}
@@ -270,11 +326,7 @@ public class NvHTTP {
// This has some extra logic to always report unpaired if the pinned cert isn't there
details.pairState = getPairState(serverInfo);
try {
details.runningGameId = getCurrentGame(serverInfo);
} catch (NumberFormatException e) {
details.runningGameId = 0;
}
details.runningGameId = getCurrentGame(serverInfo);
// We could reach it so it's online
details.state = ComputerDetails.State.ONLINE;
@@ -290,24 +342,21 @@ public class NvHTTP {
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom());
return client.newBuilder().sslSocketFactory(sc.getSocketFactory(), trustManager).build();
// TLS 1.2 is not enabled by default prior to Android 5.0, so we'll need a custom
// SSLSocketFactory in order to connect to GFE 3.20.4 which requires TLSv1.2 or later.
// We don't just always use TLSv12SocketFactory because explicitly specifying TLS versions
// prevents later TLS versions from being negotiated even if client and server otherwise
// support them.
return client.newBuilder().sslSocketFactory(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ?
sc.getSocketFactory() : new TLSv12SocketFactory(sc),
trustManager).build();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
public X509Certificate getCertificateIfTrusted() {
try {
Response resp = httpClient.newCall(new Request.Builder().url(baseUrlHttps).get().build()).execute();
Handshake handshake = resp.handshake();
if (handshake != null) {
return (X509Certificate)handshake.peerCertificates().get(0);
}
} catch (IOException ignored) {}
return null;
}
// Read timeout should be enabled for any HTTP query that requires no outside action
// on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit.
// The initial pair query does require outside action (user entering a PIN) but subsequent pairing
@@ -316,10 +365,6 @@ public class NvHTTP {
Request request = new Request.Builder().url(url).get().build();
Response response;
if (serverCert == null && !url.startsWith(baseUrlHttp)) {
throw new IllegalStateException("Attempted HTTPS fetch without pinned cert");
}
if (enableReadTimeout) {
response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute();
}
@@ -379,11 +424,6 @@ public class NvHTTP {
}
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
// If we don't have a server cert, we can't be paired even if the host thinks we are
if (serverCert == null) {
return PairState.NOT_PAIRED;
}
if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) {
return PairState.NOT_PAIRED;
}
@@ -394,11 +434,7 @@ public class NvHTTP {
public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "MaxLumaPixelsH264");
if (str != null) {
try {
return Long.parseLong(str);
} catch (NumberFormatException e) {
return 0;
}
return Long.parseLong(str);
} else {
return 0;
}
@@ -407,11 +443,7 @@ public class NvHTTP {
public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC");
if (str != null) {
try {
return Long.parseLong(str);
} catch (NumberFormatException e) {
return 0;
}
return Long.parseLong(str);
} else {
return 0;
}
@@ -428,11 +460,7 @@ public class NvHTTP {
public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "ServerCodecModeSupport");
if (str != null) {
try {
return Long.parseLong(str);
} catch (NumberFormatException e) {
return 0;
}
return Long.parseLong(str);
} else {
return 0;
}
@@ -588,36 +616,23 @@ public class NvHTTP {
}
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
int[] appVersionQuad = getServerAppVersionQuad(serverInfo);
if (appVersionQuad != null) {
return appVersionQuad[0];
}
else {
return 0;
}
return getServerAppVersionQuad(serverInfo)[0];
}
public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException {
try {
String serverVersion = getServerVersion(serverInfo);
if (serverVersion == null) {
LimeLog.warning("Missing server version field");
return null;
}
String[] serverVersionSplit = serverVersion.split("\\.");
if (serverVersionSplit.length != 4) {
LimeLog.warning("Malformed server version field");
return null;
}
int[] ret = new int[serverVersionSplit.length];
for (int i = 0; i < ret.length; i++) {
ret[i] = Integer.parseInt(serverVersionSplit[i]);
}
return ret;
} catch (NumberFormatException e) {
e.printStackTrace();
return null;
String serverVersion = getServerVersion(serverInfo);
if (serverVersion == null) {
throw new IllegalArgumentException("Missing server version field");
}
String[] serverVersionSplit = serverVersion.split("\\.");
if (serverVersionSplit.length != 4) {
throw new IllegalArgumentException("Malformed server version field: "+serverVersion);
}
int[] ret = new int[serverVersionSplit.length];
for (int i = 0; i < ret.length; i++) {
ret[i] = Integer.parseInt(serverVersionSplit[i]);
}
return ret;
}
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
@@ -632,6 +647,12 @@ public class NvHTTP {
}
public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException {
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
int fps = context.streamConfig.getLaunchRefreshRate() > 60 ? 0 : context.streamConfig.getLaunchRefreshRate();
// Using an unsupported resolution (not 720p, 1080p, or 4K) causes
// GFE to force SOPS to 720p60. This is fine for < 720p resolutions like
// 360p or 480p, but it is not ideal for 1440p and other resolutions.
@@ -645,20 +666,10 @@ public class NvHTTP {
enableSops = false;
}
// Using SOPS with FPS values over 60 causes GFE to fall back
// to 720p60. On previous GFE versions, we could avoid this by
// forcing the FPS value to 60 when launching the stream, but
// now on GFE 3.20.3 that seems to trigger some sort of
// frame rate limiter that locks the game to 60 FPS.
if (context.streamConfig.getLaunchRefreshRate() > 60) {
LimeLog.info("Disabling SOPS due to high frame rate: "+context.streamConfig.getLaunchRefreshRate());
enableSops = false;
}
String xmlStr = openHttpConnectionToString(baseUrlHttps +
"/launch?" + buildUniqueIdUuidString() +
"&appid=" + appId +
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + context.streamConfig.getLaunchRefreshRate() +
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + fps +
"&additionalStates=1&sops=" + (enableSops ? 1 : 0) +
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
"&rikeyid="+context.riKeyId +
@@ -699,4 +710,62 @@ public class NvHTTP {
return true;
}
// Based on example code from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
private static class TLSv12SocketFactory extends SSLSocketFactory {
private SSLSocketFactory internalSSLSocketFactory;
public TLSv12SocketFactory(SSLContext context) {
internalSSLSocketFactory = context.getSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return internalSSLSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket() throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket());
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
private Socket enableTLSv12OnSocket(Socket socket) {
if (socket instanceof SSLSocket) {
// TLS 1.2 is not enabled by default prior to Android 5.0. We must enable it
// explicitly to ensure we can communicate with GFE 3.20.4 which blocks TLS 1.0.
((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"});
}
return socket;
}
}
}

View File

@@ -55,6 +55,10 @@ public class PairingManager {
private static byte[] hexToBytes(String s) {
int len = s.length();
if (len % 2 != 0) {
throw new IllegalArgumentException("Illegal string length: "+len);
}
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
@@ -74,7 +78,7 @@ public class PairingManager {
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
} catch (CertificateException e) {
e.printStackTrace();
return null;
throw new RuntimeException(e);
}
}
else {
@@ -314,9 +318,8 @@ public class PairingManager {
return md.digest(data);
}
catch (NoSuchAlgorithmException e) {
// Shouldn't ever happen
e.printStackTrace();
return null;
throw new RuntimeException(e);
}
}
}
@@ -332,9 +335,8 @@ public class PairingManager {
return md.digest(data);
}
catch (NoSuchAlgorithmException e) {
// Shouldn't ever happen
e.printStackTrace();
return null;
throw new RuntimeException(e);
}
}
}

View File

@@ -33,6 +33,9 @@ public class MoonBridge {
public static final int CONN_STATUS_OKAY = 0;
public static final int CONN_STATUS_POOR = 1;
public static final int ML_ERROR_GRACEFUL_TERMINATION = 0;
public static final int ML_ERROR_NO_VIDEO_TRAFFIC = -100;
private static AudioRenderer audioRenderer;
private static VideoDecoderRenderer videoRenderer;
private static NvConnectionListener connectionListener;
@@ -258,6 +261,8 @@ public class MoonBridge {
public static native void sendMouseScroll(byte scrollClicks);
public static native void sendMouseHighResScroll(short scrollAmount);
public static native String getStageName(int stage);
public static native String findExternalAddressIP4(String stunHostName, int stunPort);

View File

@@ -104,12 +104,6 @@ public class AddComputerManually extends Activity {
try {
ComputerDetails details = new ComputerDetails();
details.manualAddress = host;
try {
NvHTTP http = new NvHTTP(host, managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(this));
details.serverCert = http.getCertificateIfTrusted();
} catch (IOException ignored) {}
success = managerBinder.addComputerBlocking(details);
} catch (IllegalArgumentException e) {
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.

View File

@@ -23,7 +23,6 @@ public class PreferenceConfiguration {
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String OSC_OPACITY_PREF_STRING = "seekbar_osc_opacity";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
static final String AUDIO_CONFIG_PREF_STRING = "list_audio_config";
@@ -41,8 +40,11 @@ public class PreferenceConfiguration {
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
private static final String FLIP_FACE_BUTTONS_PREF_STRING = "checkbox_flip_face_buttons";
private static final String TOUCHSCREEN_TRACKPAD_PREF_STRING = "checkbox_touchscreen_trackpad";
private static final String LATENCY_TOAST_PREF_STRING = "checkbox_enable_post_stream_toast";
static final String DEFAULT_RESOLUTION = "720p";
static final String DEFAULT_RESOLUTION = "1280x720";
static final String DEFAULT_FPS = "60";
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
@@ -51,7 +53,6 @@ public class PreferenceConfiguration {
private static final int DEFAULT_DEADZONE = 15;
private static final int DEFAULT_OPACITY = 90;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto";
@@ -67,12 +68,22 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_UNLOCK_FPS = false;
private static final boolean DEFAULT_VIBRATE_OSC = true;
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
private static final boolean DEFAULT_FLIP_FACE_BUTTONS = false;
private static final boolean DEFAULT_TOUCHSCREEN_TRACKPAD = true;
private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo
private static final boolean DEFAULT_LATENCY_TOAST = false;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
public static final int FORCE_H265_OFF = 1;
public static final String RES_360P = "640x360";
public static final String RES_480P = "854x480";
public static final String RES_720P = "1280x720";
public static final String RES_1080P = "1920x1080";
public static final String RES_1440P = "2560x1440";
public static final String RES_4K = "3840x2160";
public int width, height, fps;
public int bitrate;
public int videoFormat;
@@ -80,73 +91,71 @@ public class PreferenceConfiguration {
public int oscOpacity;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController, usbDriver;
public boolean smallIconMode, multiController, usbDriver, flipFaceButtons;
public boolean onscreenController;
public boolean onlyL3R3;
public boolean disableFrameDrop;
public boolean enableHdr;
public boolean enablePip;
public boolean enablePerfOverlay;
public boolean enableLatencyToast;
public boolean bindAllUsb;
public boolean mouseEmulation;
public boolean mouseNavButtons;
public boolean unlockFps;
public boolean vibrateOsc;
public boolean vibrateFallbackToDevice;
public boolean touchscreenTrackpad;
public MoonBridge.AudioConfiguration audioConfiguration;
private static int getHeightFromResolutionString(String resString) {
private static String convertFromLegacyResolutionString(String resString) {
if (resString.equalsIgnoreCase("360p")) {
return 360;
return RES_360P;
}
else if (resString.equalsIgnoreCase("480p")) {
return 480;
return RES_480P;
}
else if (resString.equalsIgnoreCase("720p")) {
return 720;
return RES_720P;
}
else if (resString.equalsIgnoreCase("1080p")) {
return 1080;
return RES_1080P;
}
else if (resString.equalsIgnoreCase("1440p")) {
return 1440;
return RES_1440P;
}
else if (resString.equalsIgnoreCase("4K")) {
return 2160;
return RES_4K;
}
else {
// Should be unreachable
return 720;
return RES_720P;
}
}
private static int getWidthFromResolutionString(String resString) {
int height = getHeightFromResolutionString(resString);
if (height == 480) {
// This isn't an exact 16:9 resolution
return 854;
}
else {
return (height * 16) / 9;
}
return Integer.parseInt(resString.split("x")[0]);
}
private static int getHeightFromResolutionString(String resString) {
return Integer.parseInt(resString.split("x")[1]);
}
private static String getResolutionString(int width, int height) {
switch (height) {
case 360:
return "360p";
return RES_360P;
case 480:
return "480p";
return RES_480P;
default:
case 720:
return "720p";
return RES_720P;
case 1080:
return "1080p";
return RES_1080P;
case 1440:
return "1440p";
return RES_1440P;
case 2160:
return "4K";
return RES_4K;
}
}
@@ -317,11 +326,24 @@ public class PreferenceConfiguration {
else {
// Use the new preference location
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
// Convert legacy resolution strings to the new style
if (!resStr.contains("x")) {
resStr = PreferenceConfiguration.convertFromLegacyResolutionString(resStr);
prefs.edit().putString(RESOLUTION_PREF_STRING, resStr).apply();
}
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
}
if (!prefs.contains(SMALL_ICONS_PREF_STRING)) {
// We need to write small icon mode's default to disk for the settings page to display
// the current state of the option properly
prefs.edit().putBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)).apply();
}
// This must happen after the preferences migration to ensure the preferences are populated
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
if (config.bitrate == 0) {
@@ -352,7 +374,6 @@ public class PreferenceConfiguration {
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
@@ -368,6 +389,9 @@ public class PreferenceConfiguration {
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PREF_STRING, DEFAULT_FLIP_FACE_BUTTONS);
config.touchscreenTrackpad = prefs.getBoolean(TOUCHSCREEN_TRACKPAD_PREF_STRING, DEFAULT_TOUCHSCREEN_TRACKPAD);
config.enableLatencyToast = prefs.getBoolean(LATENCY_TOAST_PREF_STRING, DEFAULT_LATENCY_TOAST);
return config;
}

View File

@@ -1,5 +1,6 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaCodecInfo;
@@ -7,6 +8,7 @@ import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
import android.os.Handler;
import android.os.Vibrator;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
@@ -54,9 +56,7 @@ public class StreamSettings extends Activity {
// Check for changes that require a UI reload to take effect
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
if (newPrefs.listMode != previousPrefs.listMode ||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
!newPrefs.language.equals(previousPrefs.language)) {
if (!newPrefs.language.equals(previousPrefs.language)) {
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -141,18 +141,42 @@ public class StreamSettings extends Activity {
// hide on-screen controls category on non touch screen devices
if (!getActivity().getPackageManager().
hasSystemFeature("android.hardware.touchscreen")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_onscreen_controls");
screen.removePreference(category);
{
PreferenceCategory category =
(PreferenceCategory) findPreference("category_onscreen_controls");
screen.removePreference(category);
}
{
PreferenceCategory category =
(PreferenceCategory) findPreference("category_input_settings");
category.removePreference(findPreference("checkbox_touchscreen_trackpad"));
}
}
// Remove PiP mode on devices pre-Oreo
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Remove PiP mode on devices pre-Oreo, where the feature is not available (some low RAM devices),
// and on Fire OS where it violates the Amazon App Store guidelines for some reason.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture") ||
getActivity().getPackageManager().hasSystemFeature("com.amazon.software.fireos")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
(PreferenceCategory) findPreference("category_ui_settings");
category.removePreference(findPreference("checkbox_enable_pip"));
}
// Remove the vibration options if the device can't vibrate
if (!((Vibrator)getActivity().getSystemService(Context.VIBRATOR_SERVICE)).hasVibrator()) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_input_settings");
category.removePreference(findPreference("checkbox_vibrate_fallback"));
// The entire OSC category may have already been removed by the touchscreen check above
category = (PreferenceCategory) findPreference("category_onscreen_controls");
if (category != null) {
category.removePreference(findPreference("checkbox_vibrate_osc"));
}
}
int maxSupportedFps = 0;
// Hide non-supported resolution/FPS combinations
@@ -241,33 +265,33 @@ public class StreamSettings extends Activity {
if (maxSupportedResW != 0) {
if (maxSupportedResW < 3840) {
// 4K is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_4K, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P);
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedResW < 2560) {
// 1440p is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P);
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedResW < 1920) {
// 1080p is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_720P);
resetBitrateToDefault(prefs, null, null);
}
});
@@ -319,7 +343,7 @@ public class StreamSettings extends Activity {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
LimeLog.info("Excluding unlock FPS toggle based on OS");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_unlock_fps"));
}
else {

View File

@@ -3,41 +3,22 @@ package com.limelight.utils;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import com.limelight.HelpActivity;
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) {
// Try to launch the default browser
try {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
// Several Android TV devices will lie and say they do have a browser
// even though the OS just shows an error dialog if we try to use it. We need to
// be a bit more clever on these devices and detect if the browser is a legitimate
// browser or just a fake error message activity.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
isKnownBrowser(context, i)) {
// Several Android TV devices will lie and say they do have a browser even though the OS
// just shows an error dialog if we try to use it. We used to try to be clever and check
// the package name of the resolved intent, but it's not worth it anymore with Android 11's
// package visibility changes. We'll just always use the WebView on Android TV.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
context.startActivity(i);
return;
}

View File

@@ -157,7 +157,7 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
decodeUnit->frameNumber, decodeUnit->receiveTimeMs);
decodeUnit->frameNumber, (jlong)decodeUnit->receiveTimeMs);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
@@ -178,7 +178,7 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
decodeUnit->frameNumber,
decodeUnit->receiveTimeMs);
(jlong)decodeUnit->receiveTimeMs);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);

View File

@@ -49,6 +49,11 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendMouseScroll(JNIEnv *env, jclass c
LiSendScrollEvent(scrollClicks);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_sendMouseHighResScroll(JNIEnv *env, jclass clazz, jshort scrollAmount) {
LiSendHighResScrollEvent(scrollAmount);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_stopConnection(JNIEnv *env, jclass clazz) {
LiStopConnection();

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:duration="100"/>
</set>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:duration="100"/>
</set>

View File

@@ -1,49 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="150dp"
android:layout_height="200dp"
android:padding="10dp">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="175dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="75dp"
android:layout_height="75dp"
android:indeterminate="true">
</ProgressBar>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
</RelativeLayout>
android:layout_marginTop="10dp"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:layout_height="match_parent"
android:layout_below="@id/grid_overlay"
android:layout_margin="5dp"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="18sp" >
android:textSize="18sp">
</TextView>
</RelativeLayout>

View File

@@ -1,49 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="100dp"
android:layout_height="133dp"
android:padding="5dp">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="117dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminate="true">
</ProgressBar>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
android:layout_marginTop="10dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:layout_height="match_parent"
android:layout_below="@id/grid_overlay"
android:layout_margin="5dp"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="14sp" >
android:textSize="14sp">
</TextView>
</RelativeLayout>

View File

@@ -4,7 +4,7 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:columnWidth="150dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"

View File

@@ -4,7 +4,7 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:columnWidth="100dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fastScrollEnabled="true"
android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false"
android:clipToPadding="false"/>

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="75dp"
android:layout_height="75dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:layout_width="33dp"
android:layout_height="33dp"
android:indeterminate="true">
</ProgressBar>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="75dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="14sp" >
</TextView>
</RelativeLayout>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:clipToPadding="false"
android:gravity="center"/>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:padding="10dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/grid_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textIsSelectable="false"
android:textSize="16sp" >
</TextView>
</LinearLayout>

View File

@@ -164,8 +164,6 @@
<string name="category_ui_settings">UI Einstellungen</string>
<string name="title_language_list">Sprache</string>
<string name="summary_language_list">Sprache die Moonlight verwenden soll</string>
<string name="title_checkbox_list_mode">Zeige Listen anstelle von Rastern.</string>
<string name="summary_checkbox_list_mode">Zeige Apps und PCs als Liste anstelle eines Rasters</string>
<string name="title_checkbox_small_icon_mode">Verwende kleine Icons</string>
<string name="summary_checkbox_small_icon_mode">Verwende kleine Icons in der Rasteransicht, um mehr gleichzeitig anzeigen zu können</string>

View File

@@ -111,8 +111,6 @@
<string name="category_ui_settings">Configuración de la interfaz</string>
<string name="title_language_list">Idioma</string>
<string name="summary_language_list">Idioma a usar por Moonlight</string>
<string name="title_checkbox_list_mode">Usar listas en lugar de cuadrículas</string>
<string name="summary_checkbox_list_mode">Mostrar aplicaciones y PCs en listas en lugar de el cuadrículas</string>
<string name="title_checkbox_small_icon_mode">Usar iconos pequeños</string>
<string name="summary_checkbox_small_icon_mode">Usar iconos pequeños en las entradas de cuadrículas para permitir más entradas en la pantalla</string>

View File

@@ -171,8 +171,6 @@
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
<string name="title_language_list">Langue</string>
<string name="summary_language_list">Langue à utiliser pour Moonlight</string>
<string name="title_checkbox_list_mode">Utiliser des listes au lieu des grilles</string>
<string name="summary_checkbox_list_mode">Afficher les applications et les PC en listes au lieu de grilles</string>
<string name="title_checkbox_small_icon_mode">Utiliser des petites icônes</string>
<string name="summary_checkbox_small_icon_mode">Utilisez les petites icônes dans les éléments de la grille pour permettre plus d\'éléments à l\'écran</string>

View File

@@ -139,8 +139,6 @@
<string name="category_ui_settings">Impostazioni dell\'interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Moonlight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza le applicazioni e i PC in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella griglia per avere più oggetti a schermo</string>

View File

@@ -105,8 +105,6 @@
<string name="category_ui_settings">インターフェース</string>
<string name="title_language_list">言語</string>
<string name="summary_language_list">Moonlightで使用する言語</string>
<string name="title_checkbox_list_mode">リストメニュー</string>
<string name="summary_checkbox_list_mode">ゲームをグリッドではなく、リストで表示します</string>
<string name="title_checkbox_small_icon_mode">小さなアイコン</string>
<string name="summary_checkbox_small_icon_mode">グリッドで小さなアイコンを使用します</string>

View File

@@ -120,8 +120,6 @@
<string name="category_ui_settings">UI 설정</string>
<string name="title_language_list">언어</string>
<string name="summary_language_list">Moonlight에서 사용할 언어를 선택합니다.</string>
<string name="title_checkbox_list_mode">그리드(바둑판) 대신 리스트 뷰 사용</string>
<string name="summary_checkbox_list_mode">그리드 대신 리스트로 앱과 PC를 표시합니다.</string>
<string name="title_checkbox_small_icon_mode">작은 아이콘 사용</string>
<string name="summary_checkbox_small_icon_mode">더 많이 표시하기 위해 그리드 표시에서 작은 아이콘을 사용합니다.</string>

View File

@@ -109,8 +109,6 @@
<string name="category_ui_settings">UI Installingen</string>
<string name="title_language_list">Taal</string>
<string name="summary_language_list">Taal te gebruiken in Moonlight</string>
<string name="title_checkbox_list_mode">Gebruik lijsten in plaats van kolommen</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Gebruik kleine iconen</string>
<string name="summary_checkbox_small_icon_mode">Gebruik kleine iconen in kolom onderdelen zodat meer items tegelijk zichtbaar worden.</string>

View File

@@ -169,8 +169,6 @@
<string name="category_ui_settings">Setari UI</string>
<string name="title_language_list">Limba (Language)</string>
<string name="summary_language_list">Limba folosită de către Moonlight</string>
<string name="title_checkbox_list_mode">Folosește liste în loc de grile</string>
<string name="summary_checkbox_list_mode">Aplicațiile și PC-urile vor fi afișate in liste in loc de grile</string>
<string name="title_checkbox_small_icon_mode">Folosește iconițe mici</string>
<string name="summary_checkbox_small_icon_mode">Iconițele folosite în grile vor fi mici pentru a încăpea mai multe odata</string>

View File

@@ -107,8 +107,6 @@
<string name="category_ui_settings">Настройки Интерфейса</string>
<string name="title_language_list">Язык</string>
<string name="summary_language_list">Язык, который будет использоваться в Moonlight</string>
<string name="title_checkbox_list_mode">Использовать списки вместо сеток</string>
<string name="summary_checkbox_list_mode">Выводить приложения и компьютеры списком, вместо использования сетки</string>
<string name="title_checkbox_small_icon_mode">Использовать маленькие иконки</string>
<string name="summary_checkbox_small_icon_mode">Использовать маленькие иконки в сетке для отображения большего числа элементов на экране</string>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="decoder_names">
<item>Автоматичний вибір декодера</item>
<item>Примусове програмне декодування</item>
<item>Примусове апаратне декодування</item>
</string-array>
<string-array name="video_format_names">
<item>Використовувати H.265 тільки якщо безпечно</item>
<item>Завжди використовувати H.265 якщо доступно</item>
<item>Ніколи не використовувати H.265</item>
</string-array>
</resources>

View File

@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Подивитися список ігор</string>
<string name="pcview_menu_pair_pc">Створити пару з ПК</string>
<string name="pcview_menu_unpair_pc">Розірвати пару</string>
<string name="pcview_menu_send_wol">Надіслати Wake-On-LAN запит</string>
<string name="pcview_menu_delete_pc">Видалити ПК</string>
<!-- Pair messages -->
<string name="pairing">Створення пари…</string>
<string name="pair_pc_offline">Комп\'ютер вимкнений або знаходиться не в мережі</string>
<string name="pair_pc_ingame">Комп\'ютер в даний момент знаходиться в грі. Ви повинні закрити гру перед створенням пари.</string>
<string name="pair_pairing_title">Створення пари</string>
<string name="pair_pairing_msg">Будь ласка, введіть цей PIN на ПК:</string>
<string name="pair_incorrect_pin">Неправильний PIN</string>
<string name="pair_fail">Створення пари не вдалося</string>
<!-- WOL messages -->
<string name="wol_pc_online">Комп\'ютер в мережі</string>
<string name="wol_no_mac">Неможливо розбудити ПК бо GFE не відправили MAC адреса</string>
<string name="wol_waking_pc">Пробудження ПК…</string>
<string name="wol_waking_msg">Пробудження ПК може зайняти кілька секунд.
Якщо цього не відбувається, упевніться що Wake-On-LAN налаштований правильно.
</string>
<string name="wol_fail">Помилка при відправці Wake-On-LAN пакетів</string>
<!-- Unpair messages -->
<string name="unpairing">Розрив пари…</string>
<string name="unpair_success">Розрив пари закінчився успішно</string>
<string name="unpair_fail">Розрив пари не вдався</string>
<string name="unpair_error">Пристрій не було спарено</string>
<!-- Errors -->
<string name="error_pc_offline">Комп\'ютер вимкнений або знаходиться не в мережі</string>
<string name="error_manager_not_running">Сервіс ComputerManager не запущено. Будь ласка, зачекайте кілька секунд або запустіть програму.</string>
<string name="error_unknown_host">Неможливо знайти хост</string>
<string name="error_404">GFE повернув помилку HTTP 404. Переконайтеся що Ваш ПК використовує підтримуваний GPU.
Використання програм для віддаленого доступу також може викликати цю помилку. Спробуйте перезавантажити комп\'ютер або перевстановити GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Створення з\'єднання</string>
<string name="conn_establishing_msg">Підключення</string>
<string name="conn_metered">Увага: Відбувається вимір Вашого мережевого з\'єднання!</string>
<string name="conn_client_latency">Середня затримка декодування кадру:</string>
<string name="conn_client_latency_hw">затримка апаратного декодування:</string>
<string name="conn_hardware_latency">Середня затримка апаратного декодування:</string>
<string name="conn_starting">Запуск</string>
<string name="conn_error_title">Помилка з\'єднання</string>
<string name="conn_error_msg">Запуск не вдався</string>
<string name="conn_terminated_title">З\'єднання припинено</string>
<string name="conn_terminated_msg">Підключення було перервано</string>
<!-- General strings -->
<string name="ip_hint">IP-адреса комп\'ютера з GeForce</string>
<string name="searching_pc">Пошук комп\'ютерів із запущеним GameStream…\n\n
Переконайтеся що GameStream увімкнений в налаштуваннях GeForce Experience в розділі SHIELD.</string>
<string name="yes">Так</string>
<string name="no">Ні</string>
<string name="lost_connection">З\'єднання втрачено з ПК</string>
<!-- AppList activity -->
<string name="applist_menu_resume">Відновити сесію</string>
<string name="applist_menu_quit">Вийти з сесії</string>
<string name="applist_menu_quit_and_start">Вийти з поточної гри і запустити</string>
<string name="applist_menu_cancel">Скасування</string>
<string name="applist_menu_tv_channel">Додати на канал</string>
<string name="applist_refresh_title">Список додатків</string>
<string name="applist_refresh_msg">Оновлення додатків…</string>
<string name="applist_refresh_error_title">Помилка</string>
<string name="applist_refresh_error_msg">Помилка при отриманні списку додатків</string>
<string name="applist_quit_app">Вихід з</string>
<string name="applist_quit_success">Вихід відбувся успішно з</string>
<string name="applist_quit_fail">Помилка при виході</string>
<string name="applist_quit_confirmation">Ви впевнені, що хочете вийти з запущеного додатку? Усі незбережені дані будуть втрачені.</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Додавання ПК вручну</string>
<string name="msg_add_pc">З\'єднання з ПК…</string>
<string name="addpc_fail">Неможливо підключитися до вибраного комп\'ютера. Переконайтеся, що необхідні порти дозволені в налаштуваннях брандмауера.</string>
<string name="addpc_success">Комп\'ютер доданий успішно</string>
<string name="addpc_unknown_host">Неможливо знайти ПК за вказаною адресою. Переконайтеся, що Ви не зробили помилок під час його написання.</string>
<string name="addpc_enter_ip">Ви повинні ввести IP адресу</string>
<!-- Preferences -->
<string name="category_basic_settings">Загальні Налаштування</string>
<string name="title_resolution_list">Виберіть розширення і частоту кадрів</string>
<string name="summary_resolution_list">Вибір занадто високого значеня для свого пристрою може викликати гальма або вильоти</string>
<string name="title_seekbar_bitrate">Виберіть бітрейт відео</string>
<string name="summary_seekbar_bitrate">Низький бітрейт зменшить зависання. Збільшення бітрейта поліпшить якість зображення.</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Розтягувати відео на весь екран</string>
<string name="title_checkbox_disable_warnings">Відключити повідомлення з попередженнями</string>
<string name="summary_checkbox_disable_warnings">Вимкнути екранні попередження про з\'єднання під час трансляції</string>
<string name="category_audio_settings">Аудіо Налаштування</string>
<string name="title_audio_config_list">Конфігурація об\'ємного звуку</string>
<string name="summary_audio_config_list">Увімкнути 5.1 або 7.1 об\'ємний звук для систем домашнього кінотеатру</string>
<string name="title_checkbox_multi_controller">Підтримка декількох контролерів</string>
<string name="summary_checkbox_multi_controller">Коли вимкнено, всі контролери визначаються як один</string>
<string name="title_seekbar_deadzone">Регулювати мертву зону аналогового стіку</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Драйвер контролерів Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Увімкнути вбудований USB драйвер для пристроїв без власної підтримки контролерів Xbox</string>
<string name="category_ui_settings">Налаштування інтерфейсу</string>
<string name="title_language_list">Мова</string>
<string name="summary_language_list">Мова, яка буде використовуватися в Moonlight</string>
<string name="title_checkbox_small_icon_mode">Використовувати маленькі іконки</string>
<string name="summary_checkbox_small_icon_mode">Використовувати маленькі іконки в сітці для відображення більшої кількості елементів на екрані</string>
<string name="category_host_settings">Налаштування Хоста</string>
<string name="title_checkbox_enable_sops">Оптимізувати ігрові налаштування</string>
<string name="summary_checkbox_enable_sops">Дозволити GFE змінювати налаштування ігор для оптимальної трансляції</string>
<string name="title_checkbox_host_audio">Програвати звук на ПК</string>
<string name="summary_checkbox_host_audio">Програвати звук на комп\'ютері і поточному пристрої</string>
<string name="category_advanced_settings">Розширені Налаштування</string>
<string name="title_video_format">Змінити налаштування H.265</string>
<string name="summary_video_format">H.265 знижує вимоги до пропускної здатності, але вимагає дуже нового пристрою</string>
<string name="category_on_screen_controls_settings">Налаштування дисплею з кнопками</string>
<string name="title_checkbox_show_onscreen_controls">Показувати екранні кнопки</string>
<string name="summary_checkbox_show_onscreen_controls">Відображати оверлей віртуального контролера на сенсорному екрані</string>
<string name="title_only_l3r3">Показувати тільки L3 і R3</string>
<string name="summary_only_l3r3">Приховувати всі екранні кнопки крім L3 і R3</string>
<string name="scut_deleted_pc">ПК видалений</string>
<string name="scut_not_paired">ПК не сполучений</string>
<string name="help_loading_title">Перегляд Допомоги</string>
<string name="help_loading_msg">Завантаження сторінки допомоги…</string>
<string name="pair_already_in_progress">Сполучення вже в процесі</string>
<string name="help">Допомога</string>
<string name="applist_connect_msg">Підключення до ПК…</string>
<string name="title_decoding_error">Збій відео декодера</string>
<string name="message_decoding_error">Стався збій Moonlight через проблеми з відео декодером даного пристрою. Спробуйте змінити налаштування трансляції якщо збої будуть продовжуватися.</string>
<string name="title_decoding_reset">Відео Налаштування Скинуті</string>
<string name="message_decoding_reset">Відео декодер Вашого пристрою давав збої з вибраними налаштуваннями. Налаштування трансляції були скинуті до значень за замовчуванням.</string>
<string name="error_usb_prohibited">USB доступ заборонений адміністратором пристрою. Перевірте налаштування Knox або MDM.</string>
<string name="addpc_wrong_sitelocal">Адреса вказана невірно. Ви повинні ввести публічну IP-адресу Вашого роутера для передачі через інтернет.</string>
<string name="title_checkbox_enable_pip">Увімкнути перегляд у режимі \"Картинка в картинці\"</string>
<string name="summary_checkbox_enable_pip">Дозволяє переглядати трансляцію (але не керувати нею) під час роботи в інших додатках</string>
<string name="title_checkbox_usb_bind_all">Відхилити підтримку контролерів Android</string>
<string name="summary_checkbox_usb_bind_all">Змушує USB драйвер Moonlight взяти на себе роботу з усіма підтримуваними Xbox геймпадами</string>
<string name="title_checkbox_mouse_emulation">Емуляція миші на геймпаді</string>
<string name="summary_checkbox_mouse_emulation">Довге натиснення кнопки Start перемкне геймпад в режим миші</string>
<string name="title_reset_osc">Скинути схему розташування екранних кнопок</string>
<string name="summary_reset_osc">Повертає всі екранні елементи керування до їх розташуванням за замовчуванням</string>
<string name="dialog_title_reset_osc">Скинути Схему</string>
<string name="dialog_text_reset_osc">Ви впевнені що хочете видалити збережену схему розташування кнопок?</string>
<string name="toast_reset_osc_success">Екранні елементи керування повернуті до положень за замовчуванням</string>
<string name="title_disable_frame_drop">Ніколи не пропускати кадри</string>
<string name="summary_disable_frame_drop">Може зменшити мікрозависання на деяких пристроях, але також збільшити затримку</string>
<string name="title_enable_hdr">Увімкнути HDR (Експериментально)</string>
<string name="summary_enable_hdr">Транслювати в HDR якщо гра і GPU комп\'ютера підтримують це. HDR вимагає відеокарти GTX 1000 серії або більш нової.</string>
<string name="title_checkbox_vibrate_osc">Увімкнути вібрацію</string>
<string name="title_fps_list">Частота кадрів</string>
<string name="applist_menu_details">Деталі</string>
<string name="applist_menu_scut">Створити ярлик</string>
<string name="category_input_settings">Налаштування введення</string>
<string name="title_checkbox_touchscreen_trackpad">Використовувати сенсорний екран як трекпад</string>
<string name="summary_checkbox_touchscreen_trackpad">Якщо увімкнено, сенсорний екран працює як трекпад. Якщо його вимкнено, сенсорний екран безпосередньо керує курсором миші.</string>
<string name="delete_pc_msg">Ви впевнені що хочете видалити цей ПК?</string>
<string name="pcview_menu_details">Деталі</string>
<string name="poor_connection_msg">Слабке з\'єднання з ПК</string>
<string name="title_details">Деталі</string>
<string name="title_enable_perf_overlay">Увімкнути відображення статистики</string>
<string name="title_unlock_fps">Розблокувати всі можливі частоти оновлення</string>
<string name="applist_details_id">ID додатку:</string>
<string name="title_checkbox_vibrate_fallback">Емуляція вібровіддачі</string>
<string name="summary_checkbox_vibrate_osc">Вібрація пристрою для емуляції вібровіддачі при екранному управлінні</string>
<string name="summary_checkbox_vibrate_fallback">Вібрувати пристрій для емуляції вібровіддачі для геймпадов без підтримки вібрації</string>
<string name="summary_checkbox_mouse_nav_buttons">Включення цієї опції може привести до неправильної роботи правої кнопки миші на деяких пристроях</string>
<string name="title_checkbox_flip_face_buttons">Відкідні кнопки</string>
<string name="summary_checkbox_flip_face_buttons">Перемикає лицьові кнопки A/B і X/Y для геймпадів та екранних елементів керування</string>
<string name="scut_pc_not_found">ПК не знайдено</string>
<string name="unable_to_pin_shortcut">Поточний лаунчер не дозволяє створювати закріплені ярлики</string>
<string name="title_checkbox_mouse_nav_buttons">Увімкнути кнопки вперед і назад для миші</string>
<string name="slow_connection_msg">Повільне підключення до ПК\nЗменшити бітрейт</string>
<string name="summary_unlock_fps">Трансляція зі швидкістю 90 або 120 кадрів в секунду може зменшити затримку на пристроях високого класу, але може викликати затримки або збій на пристроях без підтримки цього функціоналу</string>
<string name="summary_enable_perf_overlay">Відображення накладення на екрані з інформацією про продуктивність під час трансляції в режимі реального часу</string>
<string name="perf_overlay_text">Розширення відео: %1$s\nДекодер: %2$s\nРозрахункова частота кадрів ПК-хоста: %3$.2f FPS\nВхідна частота кадрів з мережі: %4$.2f FPS\nЧастота кадрів під час рендерінгу: %5$.2f FPS\nВідкинутих кадрів вашою мережею: %6$.2f%%\nСередній час отримання: %7$.2f ms\nСередній час декодування: %8$.2f ms</string>
<string name="summary_fps_list">Збільшення для більш плавного відео потоку. Зменшіть для кращої продуктивності на більш слабких пристроях.</string>
<string name="scut_invalid_uuid">Зазначений ПК недійсний</string>
<string name="scut_invalid_app_id">Зазначений додаток недійсне</string>
<string name="title_osc_opacity">Змінити непрозорість екранних елементів керування</string>
<string name="summary_osc_opacity">Зробити екранні елементи керування більш/менш прозорими</string>
<string name="dialog_title_osc_opacity">Зміна непрозорості</string>
<string name="suffix_osc_opacity">%</string>
<string name="title_enable_post_stream_toast">Показувати затримку після трансляції</string>
<string name="summary_enable_post_stream_toast">Вивести інформаційне повідомлення про затримку після закінчення потоку</string>
</resources>

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight</string>
<string name="app_label_root" translatable="false">Moonlight Root</string>
<!-- Shortcut strings -->
<string name="scut_deleted_pc"> 电脑已删除 </string>
@@ -137,7 +135,9 @@
<string name="title_audio_config_list"> 环绕声设置 </string>
<string name="summary_audio_config_list"> 为家庭影院系统启用5.1或7.1环绕声 </string>
<string name="category_input_settings">输入设置</string>
<string name="category_input_settings">输入设置</string>
<string name="title_checkbox_touchscreen_trackpad">将触控屏作为触控板使用</string>
<string name="summary_checkbox_touchscreen_trackpad">如果启用,则将触控屏作为触控板使用。 如果禁止,则触控屏直接控制鼠标光标</string>
<string name="title_checkbox_multi_controller"> 自动检测手柄 </string>
<string name="summary_checkbox_multi_controller"> 禁用此项所有手柄将视为一个手柄 </string>
<string name="title_checkbox_vibrate_fallback"> 用设备震动模拟游戏震动效果 </string>
@@ -151,7 +151,9 @@
<string name="title_checkbox_mouse_emulation"> 通过手柄模拟鼠标 </string>
<string name="summary_checkbox_mouse_emulation"> 长按开始键将手柄切换为鼠标模式 </string>
<string name="title_checkbox_mouse_nav_buttons"> 启用前进后退鼠标键 </string>
<string name="summary_checkbox_mouse_nav_buttons"> 在一些支持不佳的设备上启用此项可能会使其右键失效 </string>
<string name="summary_checkbox_mouse_nav_buttons"> 在一些支持不佳的设备上启用此项可能会使其右键失效 </string>
<string name="title_checkbox_flip_face_buttons"> 反转技能键 </string>
<string name="summary_checkbox_flip_face_buttons"> 为手柄和虚拟手柄调转A/B和X/Y技能键 </string>
<string name="category_on_screen_controls_settings"> 屏幕控制按钮设置 </string>
<string name="title_checkbox_show_onscreen_controls"> 显示屏幕控制按钮 </string>
@@ -169,8 +171,6 @@
<string name="category_ui_settings"> 界面设置 </string>
<string name="title_language_list"> 语言 </string>
<string name="summary_language_list"> 选择Moonlight显示的语言 </string>
<string name="title_checkbox_list_mode"> 使用列表代替图标 </string>
<string name="summary_checkbox_list_mode"> 列表显示电脑和游戏 </string>
<string name="title_checkbox_small_icon_mode"> 使用小图标 </string>
<string name="summary_checkbox_small_icon_mode"> 使用小图标以在屏幕上显示更多项目 </string>
@@ -187,24 +187,10 @@
<string name="summary_video_format">H.265能降低视频带宽需求,但需要较新的设备才能支持</string>
<string name="title_enable_hdr"> 启用 HDR (实验) </string>
<string name="summary_enable_hdr"> 当游戏和显卡支持时以HDR模式串流。 HDR需要GTX 1000系列或更高规格显卡。 </string>
<string name="title_enable_perf_overlay"> 启用性能信息 </string>
<string name="summary_enable_perf_overlay"> 在串流中显示实时性能信息 </string>
<string name="title_enable_post_stream_toast">串流完毕显示延迟信息</string>
<string name="summary_enable_post_stream_toast">串流结束后显示延迟信息</string>
<string name="title_osc_opacity">更改屏幕按钮透明度</string>
<string name="dialog_title_osc_opacity">透明度</string>
<string name="suffix_osc_opacity">%</string>

View File

@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight</string>
<string name="app_label_root" translatable="false">Moonlight Root</string>
<!-- Shortcut strings -->
<string name="scut_deleted_pc"> 電腦已刪除 </string>
@@ -137,7 +135,9 @@
<string name="title_audio_config_list"> 環繞聲設置 </string>
<string name="summary_audio_config_list"> 為家庭劇院系統啟用5.1或7.1環繞聲 </string>
<string name="category_input_settings">輸入設置</string>
<string name="category_input_settings"> 輸入設置 </string>
<string name="title_checkbox_touchscreen_trackpad"> 將觸控屏作為觸控板使用 </string>
<string name="summary_checkbox_touchscreen_trackpad"> 如果啟用,則將觸控屏作為觸控板使用。 如果禁止,則觸控屏直接控制滑鼠游標 </string>
<string name="title_checkbox_multi_controller"> 自動檢測手柄 </string>
<string name="summary_checkbox_multi_controller"> 禁用此項所有手柄將視為一個手柄 </string>
<string name="title_checkbox_vibrate_fallback"> 用設備震動類比遊戲震動效果 </string>
@@ -151,7 +151,9 @@
<string name="title_checkbox_mouse_emulation"> 通過手柄類比滑鼠 </string>
<string name="summary_checkbox_mouse_emulation"> 長按開始鍵將手柄切換為滑鼠模式 </string>
<string name="title_checkbox_mouse_nav_buttons"> 啟用前進後退滑鼠鍵 </string>
<string name="summary_checkbox_mouse_nav_buttons"> 在一些支援不佳的設備上啟用此項可能會使其右鍵失效 </string>
<string name="summary_checkbox_mouse_nav_buttons"> 在一些支援不佳的設備上啟用此項可能會使其右鍵失效 </string>
<string name="title_checkbox_flip_face_buttons">反轉技能鍵</string>
<string name="summary_checkbox_flip_face_buttons">為手柄和虛擬手柄調轉A/B和X/Y技能鍵</string>
<string name="category_on_screen_controls_settings"> 螢幕控制按鈕設置 </string>
<string name="title_checkbox_show_onscreen_controls"> 顯示幕幕控制按鈕 </string>
@@ -169,8 +171,6 @@
<string name="category_ui_settings"> 介面設置 </string>
<string name="title_language_list"> 語言 </string>
<string name="summary_language_list"> 選擇Moonlight顯示的語言 </string>
<string name="title_checkbox_list_mode"> 使用清單代替圖示 </string>
<string name="summary_checkbox_list_mode"> 清單顯示電腦和遊戲 </string>
<string name="title_checkbox_small_icon_mode"> 使用小圖示 </string>
<string name="summary_checkbox_small_icon_mode"> 使用小圖示以在螢幕上顯示更多專案 </string>
@@ -187,24 +187,10 @@
<string name="summary_video_format">H.265能降低視頻頻寬需求,但需要較新的設備才能支援</string>
<string name="title_enable_hdr"> 啟用 HDR (實驗) </string>
<string name="summary_enable_hdr"> 當遊戲和顯卡支援時以HDR模式串流。 HDR需要GTX 1000系列或更高規格顯卡。 </string>
<string name="title_enable_perf_overlay"> 啟用性能資訊 </string>
<string name="summary_enable_perf_overlay"> 在串流中顯示即時性能資訊 </string>
<string name="title_enable_post_stream_toast">串流完畢顯示延遲資訊</string>
<string name="summary_enable_post_stream_toast">串流結束後顯示延遲資訊</string>
<string name="title_osc_opacity">更改屏幕按鈕透明度</string>
<string name="dialog_title_osc_opacity">透明度</string>
<string name="suffix_osc_opacity">%</string>

View File

@@ -9,12 +9,12 @@
<item>4K</item>
</string-array>
<string-array name="resolution_values" translatable="false">
<item>360p</item>
<item>480p</item>
<item>720p</item>
<item>1080p</item>
<item>1440p</item>
<item>4K</item>
<item>640x360</item>
<item>854x480</item>
<item>1280x720</item>
<item>1920x1080</item>
<item>2560x1440</item>
<item>3840x2160</item>
</string-array>
<string-array name="fps_names">
@@ -55,6 +55,7 @@
<item>Français</item>
<item>Deutsch</item>
<item>Română</item>
<item>Українська</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
@@ -70,6 +71,7 @@
<item>fr</item>
<item>de</item>
<item>ro</item>
<item>uk</item>
</string-array>
<string-array name="decoder_names">

View File

@@ -61,6 +61,7 @@
<string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
<string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string>
<string name="video_decoder_init_failed">Video decoder failed to initialize. Your device may not support the selected resolution or frame rate.</string>
<string name="no_video_received_error">No video received from host. Check the host PC\'s firewall and port forwarding rules.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
@@ -126,33 +127,31 @@
<string name="title_seekbar_bitrate">Video bitrate</string>
<string name="summary_seekbar_bitrate">Increase for better image quality. Decrease to improve performance on slower connections.</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_unlock_fps">Unlock all possible frame rates</string>
<string name="summary_unlock_fps">Streaming at 90 or 120 FPS may reduce latency on high-end devices but can cause lag or crashes on devices that can\'t support it</string>
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="title_checkbox_enable_pip">Enable Picture-in-Picture observer mode</string>
<string name="summary_checkbox_enable_pip">Allows the stream to be viewed (but not controlled) while multitasking</string>
<string name="category_audio_settings">Audio Settings</string>
<string name="title_audio_config_list">Surround sound configuration</string>
<string name="summary_audio_config_list">Enable 5.1 or 7.1 surround sound for home-theater systems</string>
<string name="category_input_settings">Input Settings</string>
<string name="title_checkbox_touchscreen_trackpad">Use the touchscreen as a trackpad</string>
<string name="summary_checkbox_touchscreen_trackpad">If enabled, the touchscreen acts like a trackpad. If disabled, the touchscreen directly controls the mouse cursor.</string>
<string name="title_checkbox_multi_controller">Automatic gamepad presence detection</string>
<string name="summary_checkbox_multi_controller">Unchecking this option forces a gamepad to always be present</string>
<string name="title_checkbox_vibrate_fallback">Emulate rumble support with vibration</string>
<string name="summary_checkbox_vibrate_fallback">Vibrates your device to emulate rumble if your gamepad does not support it</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One USB gamepad driver</string>
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox controller support</string>
<string name="title_checkbox_usb_bind_all">Override Android controller support</string>
<string name="summary_checkbox_usb_bind_all">Forces Moonlight\'s USB driver to take over all supported Xbox gamepads</string>
<string name="title_checkbox_usb_bind_all">Override native Xbox gamepad support</string>
<string name="summary_checkbox_usb_bind_all">Use Moonlight\'s USB driver for all supported gamepads, even if native Xbox controller support is present</string>
<string name="title_checkbox_mouse_emulation">Mouse emulation via gamepad</string>
<string name="summary_checkbox_mouse_emulation">Long pressing the Start button will switch the gamepad into mouse mode</string>
<string name="title_checkbox_mouse_nav_buttons">Enable back and forward mouse buttons</string>
<string name="summary_checkbox_mouse_nav_buttons">Enabling this option may break right clicking on some buggy devices</string>
<string name="title_checkbox_flip_face_buttons">Flip face buttons</string>
<string name="summary_checkbox_flip_face_buttons">Switches the face buttons A/B and X/Y for gamepads and the on-screen controls</string>
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
@@ -172,12 +171,12 @@
<string name="suffix_osc_opacity">%</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_checkbox_enable_pip">Enable Picture-in-Picture observer mode</string>
<string name="summary_checkbox_enable_pip">Allows the stream to be viewed (but not controlled) while multitasking</string>
<string name="title_language_list">Language</string>
<string name="summary_language_list">Language to use for Moonlight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
<string name="title_checkbox_small_icon_mode">Use small box art</string>
<string name="summary_checkbox_small_icon_mode">Smaller box art in the app grid allows more apps to be visible on screen</string>
<string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string>
@@ -186,13 +185,19 @@
<string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
<string name="category_advanced_settings">Advanced Settings</string>
<string name="title_unlock_fps">Unlock all possible frame rates</string>
<string name="summary_unlock_fps">Streaming at 90 or 120 FPS may reduce latency on high-end devices but can cause lag or instability on devices that can\'t support it</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="title_disable_frame_drop">Never drop frames</string>
<string name="summary_disable_frame_drop">May reduce micro-stuttering on some devices, but can increase latency</string>
<string name="title_video_format">Change H.265 settings</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a newer device</string>
<string name="title_enable_hdr">Enable HDR (Experimental)</string>
<string name="summary_enable_hdr">Stream HDR when the game and PC GPU support it. HDR requires a GTX 1000 series GPU or later.</string>
<string name="title_enable_perf_overlay">Enable performance overlay</string>
<string name="summary_enable_perf_overlay">Display an on-screen overlay with real-time performance information while streaming</string>
<string name="title_enable_perf_overlay">Show performance stats while streaming</string>
<string name="summary_enable_perf_overlay">Display real-time stream performance information while streaming</string>
<string name="title_enable_post_stream_toast">Show latency message after streaming</string>
<string name="summary_enable_post_stream_toast">Display a latency information message after the stream ends</string>
</resources>

View File

@@ -10,7 +10,7 @@
android:summary="@string/summary_resolution_list"
android:entries="@array/resolution_names"
android:entryValues="@array/resolution_values"
android:defaultValue="720p" />
android:defaultValue="1280x720" />
<ListPreference
android:key="list_fps"
android:title="@string/title_fps_list"
@@ -27,20 +27,10 @@
android:summary="@string/summary_seekbar_bitrate"
android:text="@string/suffix_seekbar_bitrate"
android:title="@string/title_seekbar_bitrate" />
<CheckBoxPreference
android:key="checkbox_unlock_fps"
android:title="@string/title_unlock_fps"
android:summary="@string/summary_unlock_fps"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_stretch_video"
android:title="@string/title_checkbox_stretch_video"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_enable_pip"
android:title="@string/title_checkbox_enable_pip"
android:summary="@string/summary_checkbox_enable_pip"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_audio_settings">
<ListPreference
@@ -51,13 +41,19 @@
android:entryValues="@array/audio_config_values"
android:defaultValue="2" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_input_settings">
<PreferenceCategory android:title="@string/category_input_settings"
android:key="category_input_settings">
<!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone"
android:defaultValue="15"
android:max="50"
android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/-->
<CheckBoxPreference
android:key="checkbox_touchscreen_trackpad"
android:title="@string/title_checkbox_touchscreen_trackpad"
android:summary="@string/summary_checkbox_touchscreen_trackpad"
android:defaultValue="true" />
<CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
@@ -89,6 +85,11 @@
android:title="@string/title_checkbox_vibrate_fallback"
android:summary="@string/summary_checkbox_vibrate_fallback"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_flip_face_buttons"
android:title="@string/title_checkbox_flip_face_buttons"
android:summary="@string/summary_checkbox_flip_face_buttons"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
android:key="category_onscreen_controls">
@@ -142,7 +143,13 @@
android:summary="@string/summary_checkbox_host_audio"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_ui_settings">
<PreferenceCategory android:title="@string/category_ui_settings"
android:key="category_ui_settings">
<CheckBoxPreference
android:key="checkbox_enable_pip"
android:title="@string/title_checkbox_enable_pip"
android:summary="@string/summary_checkbox_enable_pip"
android:defaultValue="false" />
<ListPreference
android:key="list_languages"
android:title="@string/title_language_list"
@@ -154,14 +161,14 @@
android:key="checkbox_small_icon_mode"
android:title="@string/title_checkbox_small_icon_mode"
android:summary="@string/summary_checkbox_small_icon_mode" />
<CheckBoxPreference
android:key="checkbox_list_mode"
android:title="@string/title_checkbox_list_mode"
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"
android:key="category_advanced_settings">
<CheckBoxPreference
android:key="checkbox_unlock_fps"
android:title="@string/title_unlock_fps"
android:summary="@string/summary_unlock_fps"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_disable_warnings"
android:title="@string/title_checkbox_disable_warnings"
@@ -189,5 +196,10 @@
android:title="@string/title_enable_perf_overlay"
android:summary="@string/summary_enable_perf_overlay"
android:defaultValue="false"/>
<CheckBoxPreference
android:key="checkbox_enable_post_stream_toast"
android:title="@string/title_enable_post_stream_toast"
android:summary="@string/summary_enable_post_stream_toast"
android:defaultValue="false"/>
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -5,7 +5,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.android.tools.build:gradle:4.0.0'
}
}

View File

@@ -0,0 +1,5 @@
- Direct touchscreen mouse control is now supported! (disable touchscreen trackpad mode in settings)
- Added an option to switch the A/B and X/Y face buttons on gamepads and on-screen controls
- Added a special error message if no video was received instead of just displaying a black screen
- Improved support for Xbox One S controllers using the new 4.8 firmware
- Improved handling of audio capture errors reported by GeForce Experience

View File

@@ -0,0 +1,4 @@
- Fixed stream starting in 720p when configured for 1080p or 4K at 90 or 120 FPS
- Fixed incorrect behavior of right-side modifier keys while streaming
- Fixed the mouse cursor not reaching the bottom and right edges of the host's screen on some devices
- Updated Traditional and Simplified Chinese translations

View File

@@ -0,0 +1,6 @@
- Improved PC and App Grid UI
- Android 11 optimizations
- Added mapping for Nintendo Switch Pro controller
- Post-stream latency message is now disabled by default
- Fixed control stream connection error on some networks
- Reorganized some of the settings options

View File

@@ -0,0 +1,3 @@
- Removed old button emulation feature which prevented LB+Start, RB+Start, and Start+Select from working as expected
- Fixed edges of on-screen analog sticks being clipped
- Updated Simplified and Traditional Chinese translations

View File

@@ -0,0 +1,4 @@
- Added Ukrainian translation
- Fixed SSL error connecting to GFE 3.20.4 on Android 4.1-4.4
- Fixed a random crash when starting a stream in bad network conditions
- Fixed decoder-related crashes on Xiaomi Mi 10 Lite 5G devices

View File

@@ -15,12 +15,12 @@ Streaming performance may vary based on your client device and network setup. HD
* Mouse control via gamepad by long-pressing Start
'''PC Requirements'''
* NVIDIA GeForce GTX/RTX series GPU (''GT-series and AMD GPUs aren't supported by NVIDIA GameStream'')
* NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU
* Windows 7 or later
* NVIDIA GeForce Experience (GFE) 2.2.2 or later
* NVIDIA GeForce Experience or NVIDIA Quadro Experience installed
'''Quick Setup Instructions'''
# Make sure GeForce Experience is open on your PC. Turn on GameStream in the SHIELD settings page.
# Make sure GeForce/Quadro Experience is open on your PC. Turn on GameStream in the SHIELD settings page.
# Tap on the PC in Moonlight and type the PIN on your PC
# Start streaming!

View File

@@ -1,6 +1,6 @@
#Mon Feb 24 12:32:24 PST 2020
#Thu May 28 11:41:09 PDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip