Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cba200207 | |||
| 81582d7343 | |||
| 04e561fd54 | |||
| 5efbb5229d | |||
| 541e43eb18 | |||
| 7e679ff4c6 | |||
| 486b4b4c4c | |||
| 7d76bf7868 | |||
| db49077b9b | |||
| 16b845ab84 | |||
| 5c175fecf6 | |||
| 773976b265 | |||
| 80070bbdbe | |||
| 4c8d433b6c | |||
| 404f096d11 | |||
| d2ac927cec | |||
| 5e3d59d3d7 | |||
| 9cd2ce1309 | |||
| 9ed49730d4 | |||
| 39ebb48f58 | |||
| 1c29c70fba | |||
| 6993051529 | |||
| 4930087c4d | |||
| 795f0a013b | |||
| 213414778e | |||
| 7eac0ccaf8 | |||
| 6adc9dcb2d | |||
| be620908f9 | |||
| e4edfdb043 | |||
| 3b5028d1a4 | |||
| bc8c45bd59 | |||
| 63eb346a70 | |||
| 27ad691d23 | |||
| 747e920061 | |||
| 8d09f56a0e | |||
| 113a0e2c45 | |||
| 977215a098 | |||
| a7e65b47f9 | |||
| 7126055ad6 | |||
| 3de9765eaa | |||
| d4072eb295 | |||
| cac2bdbb81 | |||
| 66f0aee3f8 | |||
| b690dc5474 | |||
| c2fbe6ad91 | |||
| cf07c02398 | |||
| 42dc928ad5 | |||
| 11597f0aa7 | |||
| cdcd4d48f2 | |||
| a9af4e54a9 | |||
| 7eac609219 | |||
| fa761debc4 | |||
| 62e175f069 | |||
| d7d8c40565 | |||
| 64de13ab50 | |||
| 2f02939638 | |||
| 1d7c8697e9 | |||
| 7dea322bbd | |||
| 349ecb16ab | |||
| a3867735c1 | |||
| 5b087e9f70 | |||
| eed18223eb | |||
| 30d4d2a918 | |||
| 30f666c70e | |||
| 209fead0e8 | |||
| 5c6889bf6d | |||
| 7d24900756 | |||
| 79a75b9d19 | |||
| 29b64992bd | |||
| c9b14540f2 | |||
| 546843a26c | |||
| d03d260535 | |||
| 6946e3c7a2 |
@@ -0,0 +1,8 @@
|
||||
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||
|
||||
daysUntilClose: 7
|
||||
responseRequiredLabel: 'need more info'
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there was no response to a
|
||||
request for more information from the issue opener. Please leave a comment or
|
||||
open a new issue if you have additional information related to this issue.
|
||||
@@ -0,0 +1,14 @@
|
||||
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||
|
||||
daysUntilStale: 90
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- accepted
|
||||
- bug
|
||||
- enhancement
|
||||
- meta
|
||||
staleLabel: stale
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
closeComment: false
|
||||
@@ -1,13 +1,11 @@
|
||||
# Moonlight
|
||||
# Moonlight Android
|
||||
|
||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||
|
||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||
whether in your own home or over the internet.
|
||||
|
||||
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
||||
|
||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||
|
||||
## Features
|
||||
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
buildToolsVersion '28.0.1'
|
||||
buildToolsVersion '28.0.3'
|
||||
compileSdkVersion 28
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
|
||||
versionName "5.9"
|
||||
versionCode = 168
|
||||
versionName "6.1"
|
||||
versionCode = 182
|
||||
}
|
||||
|
||||
flavorDimensions "root"
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
@@ -80,11 +81,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Wait for the binder to be ready
|
||||
localBinder.waitForReady();
|
||||
|
||||
// Now make the binder visible
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Get the computer object
|
||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||
computer = localBinder.getComputer(UUID.fromString(uuidString));
|
||||
if (computer == null) {
|
||||
finish();
|
||||
return;
|
||||
@@ -94,13 +92,18 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||
computer, managerBinder.getUniqueId());
|
||||
computer, localBinder.getUniqueId());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Now make the binder visible. We must do this after appGridAdapter
|
||||
// is set to prevent us from reaching updateUiWithServerinfo() and
|
||||
// touching the appGridAdapter prior to initialization.
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Load the app grid with cached data (if possible).
|
||||
// This must be done _before_ startComputerUpdates()
|
||||
// so the initial serverinfo response can update the running
|
||||
@@ -257,10 +260,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||
|
||||
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
||||
TextView label = findViewById(R.id.appListText);
|
||||
setTitle(labelText);
|
||||
label.setText(labelText);
|
||||
setTitle(computerName);
|
||||
label.setText(computerName);
|
||||
|
||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
||||
@@ -344,7 +346,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
||||
// Only add an option to create shortcut if box art is loaded
|
||||
// and when we're in grid-mode (not list-mode).
|
||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
||||
if (appImageView != null) {
|
||||
// We have a grid ImageView, so we must be in grid-mode
|
||||
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
||||
if (drawable != null && drawable.getBitmap() != null) {
|
||||
// We have a bitmap loaded too
|
||||
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,8 +390,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void run() {
|
||||
suspendGridUpdates = true;
|
||||
ServerHelper.doQuit(AppView.this,
|
||||
ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
ServerHelper.doQuit(AppView.this, computer,
|
||||
app.app, managerBinder, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
@@ -68,6 +68,11 @@ import android.widget.FrameLayout;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
|
||||
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||
@@ -132,10 +137,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
public static final String EXTRA_APP_NAME = "AppName";
|
||||
public static final String EXTRA_APP_ID = "AppId";
|
||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||
public static final String EXTRA_PC_UUID = "UUID";
|
||||
public static final String EXTRA_PC_NAME = "PcName";
|
||||
public static final String EXTRA_APP_HDR = "HDR";
|
||||
public static final String EXTRA_SERVER_CERT = "ServerCert";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -226,10 +231,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
||||
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
|
||||
byte[] derCertData = Game.this.getIntent().getByteArrayExtra(EXTRA_SERVER_CERT);
|
||||
|
||||
X509Certificate serverCert = null;
|
||||
try {
|
||||
if (derCertData != null) {
|
||||
serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||
finish();
|
||||
@@ -346,8 +361,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Hopefully, we can get rid of this once someone comes up with a better way
|
||||
// to track the state of the pipeline and time frames.
|
||||
int roundedRefreshRate = Math.round(displayRefreshRate);
|
||||
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) {
|
||||
if (roundedRefreshRate <= 49) {
|
||||
if ((!prefConfig.disableFrameDrop || prefConfig.unlockFps) && prefConfig.fps >= roundedRefreshRate) {
|
||||
if (prefConfig.unlockFps) {
|
||||
// Use frame drops when rendering above the screen frame rate
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Using drop mode for FPS > Hz");
|
||||
}
|
||||
else if (roundedRefreshRate <= 49) {
|
||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
|
||||
@@ -370,8 +390,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
.setBitrate(prefConfig.bitrate)
|
||||
.setEnableSops(prefConfig.enableSops)
|
||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292)
|
||||
.setRemote(remote)
|
||||
.setMaxPacketSize(1392)
|
||||
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO)
|
||||
.setHevcBitratePercentageMultiplier(75)
|
||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||
.setEnableHdr(willStreamHdr)
|
||||
@@ -383,7 +403,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
.build();
|
||||
|
||||
// Initialize the connection
|
||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this));
|
||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert);
|
||||
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
||||
|
||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||
@@ -408,6 +428,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
(FrameLayout)streamView.getParent(),
|
||||
this);
|
||||
virtualController.refreshLayout();
|
||||
virtualController.show();
|
||||
}
|
||||
|
||||
if (prefConfig.usbDriver) {
|
||||
@@ -499,8 +520,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display.Mode bestMode = display.getMode();
|
||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
|
||||
candidate.getRefreshRate() < 63;
|
||||
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate();
|
||||
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
||||
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
||||
candidate.getPhysicalWidth() <= 4096;
|
||||
@@ -516,6 +536,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
||||
if (prefConfig.fps <= 60) {
|
||||
if (candidate.getRefreshRate() >= 63) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the refresh rate doesn't regress
|
||||
if (!refreshRateOk) {
|
||||
continue;
|
||||
@@ -533,12 +560,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||
displayRefreshRate = bestMode.getRefreshRate();
|
||||
}
|
||||
// On L, we can at least tell the OS that we want 60 Hz
|
||||
// On L, we can at least tell the OS that we want a refresh rate
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
float bestRefreshRate = display.getRefreshRate();
|
||||
for (float candidate : display.getSupportedRefreshRates()) {
|
||||
if (candidate > bestRefreshRate && candidate < 63) {
|
||||
if (candidate > bestRefreshRate) {
|
||||
LimeLog.info("Examining refresh rate: "+candidate);
|
||||
|
||||
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
||||
if (prefConfig.fps <= 60) {
|
||||
if (candidate >= 63) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bestRefreshRate = candidate;
|
||||
}
|
||||
}
|
||||
@@ -680,6 +715,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
SpinnerDialog.closeDialogs(this);
|
||||
Dialog.closeDialogs();
|
||||
|
||||
if (virtualController != null) {
|
||||
virtualController.hide();
|
||||
}
|
||||
|
||||
if (conn != null) {
|
||||
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
||||
|
||||
@@ -836,10 +875,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Handle a synthetic back button event that some Android OS versions
|
||||
// create as a result of a right-click. This event WILL repeat if
|
||||
// the right mouse button is held down, so we ignore those.
|
||||
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
if (!prefConfig.mouseNavButtons &&
|
||||
(event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK &&
|
||||
event.getRepeatCount() == 0) {
|
||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||
return true;
|
||||
}
|
||||
@@ -889,7 +928,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// Handle a synthetic back button event that some Android OS versions
|
||||
// create as a result of a right-click.
|
||||
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
if (!prefConfig.mouseNavButtons &&
|
||||
(event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||
@@ -1010,6 +1050,26 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
if (prefConfig.mouseNavButtons) {
|
||||
if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1);
|
||||
}
|
||||
else {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X1);
|
||||
}
|
||||
}
|
||||
|
||||
if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
|
||||
}
|
||||
else {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get relative axis values if we can
|
||||
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
||||
// Send the deltas straight from the motion event
|
||||
@@ -1360,6 +1420,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
case EvdevListener.BUTTON_RIGHT:
|
||||
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
|
||||
break;
|
||||
case EvdevListener.BUTTON_X1:
|
||||
buttonIndex = MouseButtonPacket.BUTTON_X1;
|
||||
break;
|
||||
case EvdevListener.BUTTON_X2:
|
||||
buttonIndex = MouseButtonPacket.BUTTON_X2;
|
||||
break;
|
||||
default:
|
||||
LimeLog.warning("Unhandled button: "+buttonId);
|
||||
return;
|
||||
|
||||
@@ -313,8 +313,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||
|
||||
// Inflate the context menu
|
||||
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
|
||||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
||||
computer.details.state == ComputerDetails.State.UNKNOWN) {
|
||||
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||
}
|
||||
@@ -346,7 +346,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void doPair(final ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -372,9 +373,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
PlatformBinding.getDeviceName(),
|
||||
computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
if (httpConn.getPairState() == PairState.PAIRED) {
|
||||
// Don't display any toast, but open the app list
|
||||
message = null;
|
||||
success = true;
|
||||
@@ -386,21 +387,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||
|
||||
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||
PairingManager pm = httpConn.getPairingManager();
|
||||
|
||||
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairState.PIN_WRONG) {
|
||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.FAILED) {
|
||||
else if (pairState == PairState.FAILED) {
|
||||
message = getResources().getString(R.string.pair_fail);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) {
|
||||
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
|
||||
message = getResources().getString(R.string.pair_already_in_progress);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||
else if (pairState == PairState.PAIRED) {
|
||||
// Just navigate to the app view without displaying a toast
|
||||
message = null;
|
||||
success = true;
|
||||
|
||||
// Pin this certificate for later HTTPS use
|
||||
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
|
||||
|
||||
// Invalidate reachability information after pairing to force
|
||||
// a refresh before reading pair state again
|
||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||
@@ -478,7 +484,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void doUnpair(final ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -496,7 +503,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
try {
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
PlatformBinding.getDeviceName(),
|
||||
computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
httpConn.unpair();
|
||||
@@ -530,7 +537,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void doAppList(ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -567,12 +574,17 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
LimeLog.info("Ignoring delete PC request from monkey");
|
||||
return true;
|
||||
}
|
||||
if (managerBinder == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
managerBinder.removeComputer(computer.details.name);
|
||||
removeComputer(computer.details);
|
||||
UiHelper.displayDeletePcConfirmationDialog(this, computer.details, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (managerBinder == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
managerBinder.removeComputer(computer.details.name);
|
||||
removeComputer(computer.details);
|
||||
}
|
||||
}, null);
|
||||
return true;
|
||||
|
||||
case APP_LIST_ID:
|
||||
@@ -598,8 +610,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ServerHelper.doQuit(PcView.this,
|
||||
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
||||
ServerHelper.doQuit(PcView.this, computer.details,
|
||||
new NvApp("app", 0, false), managerBinder, null);
|
||||
}
|
||||
}, null);
|
||||
@@ -685,8 +696,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||
long id) {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
||||
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.details.state == ComputerDetails.State.UNKNOWN ||
|
||||
computer.details.state == ComputerDetails.State.OFFLINE) {
|
||||
// Open the context menu if a PC is offline or refreshing
|
||||
openContextMenu(arg1);
|
||||
} else if (computer.details.pairState != PairState.PAIRED) {
|
||||
|
||||
@@ -112,11 +112,15 @@ public class ShortcutTrampoline extends Activity {
|
||||
// Now start the activities
|
||||
startActivities(intentStack.toArray(new Intent[]{}));
|
||||
} else {
|
||||
// Create the start intent immediately, so we can safely unbind the managerBinder
|
||||
// below before we return.
|
||||
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
||||
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder);
|
||||
|
||||
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
||||
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder));
|
||||
intentStack.add(startIntent);
|
||||
|
||||
// Close this activity
|
||||
finish();
|
||||
|
||||
@@ -4,6 +4,8 @@ public interface EvdevListener {
|
||||
int BUTTON_LEFT = 1;
|
||||
int BUTTON_MIDDLE = 2;
|
||||
int BUTTON_RIGHT = 3;
|
||||
int BUTTON_X1 = 4;
|
||||
int BUTTON_X2 = 5;
|
||||
|
||||
void mouseMove(int deltaX, int deltaY);
|
||||
void mouseButtonEvent(int buttonId, boolean down);
|
||||
|
||||
@@ -338,6 +338,7 @@ public class AnalogStick extends VirtualControllerElement {
|
||||
} else {
|
||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||
notifyOnRevoke();
|
||||
|
||||
// not longer pressed reset analog stick
|
||||
notifyOnMovement(0, 0);
|
||||
}
|
||||
|
||||
+18
-6
@@ -17,6 +17,8 @@ import com.limelight.nvstream.NvConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class VirtualController {
|
||||
public class ControllerInputContext {
|
||||
@@ -42,6 +44,8 @@ public class VirtualController {
|
||||
private FrameLayout frame_layout = null;
|
||||
private RelativeLayout relative_layout = null;
|
||||
|
||||
private Timer retransmitTimer;
|
||||
|
||||
ControllerMode currentMode = ControllerMode.Active;
|
||||
ControllerInputContext inputContext = new ControllerInputContext();
|
||||
|
||||
@@ -88,11 +92,24 @@ public class VirtualController {
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
retransmitTimer.cancel();
|
||||
relative_layout.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
relative_layout.setVisibility(View.VISIBLE);
|
||||
|
||||
// HACK: GFE sometimes discards gamepad packets when they are received
|
||||
// very shortly after another. This can be critical if an axis zeroing packet
|
||||
// is lost and causes an analog stick to get stuck. To avoid this, we send
|
||||
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
|
||||
retransmitTimer = new Timer("OSC timer", true);
|
||||
retransmitTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendControllerInputContext();
|
||||
}
|
||||
}, 100, 100);
|
||||
}
|
||||
|
||||
public void removeElements() {
|
||||
@@ -149,18 +166,13 @@ public class VirtualController {
|
||||
return inputContext;
|
||||
}
|
||||
|
||||
public void sendControllerInputContext() {
|
||||
sendControllerInputPacket();
|
||||
}
|
||||
|
||||
private void sendControllerInputPacket() {
|
||||
void sendControllerInputContext() {
|
||||
try {
|
||||
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
||||
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
||||
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
||||
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||
|
||||
if (connection != null) {
|
||||
connection.sendControllerInput(
|
||||
|
||||
@@ -405,10 +405,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
// Render the last buffer
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !legacyFrameDropRendering) {
|
||||
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
||||
// is disabled
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (legacyFrameDropRendering) {
|
||||
// Use a PTS that will cause this frame to be dropped if another comes in within
|
||||
// the same V-sync period
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime());
|
||||
}
|
||||
else {
|
||||
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
||||
// is disabled
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
|
||||
@@ -160,8 +160,8 @@ public class MediaCodecHelper {
|
||||
|
||||
// We see a bunch of crashes on MediaTek Android TVs running
|
||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
||||
// these devices and hope they fix it in Oreo.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
// these devices and hope they fix it in Pie.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -17,15 +21,15 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
public class ComputerDatabaseManager {
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
private static final String COMPUTER_DB_NAME = "computers2.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||
private static final String MAC_COLUMN_NAME = "Mac";
|
||||
|
||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
|
||||
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
|
||||
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
|
||||
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||
|
||||
private SQLiteDatabase computerDb;
|
||||
|
||||
@@ -38,20 +42,35 @@ public class ComputerDatabaseManager {
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||
}
|
||||
initializeDb();
|
||||
initializeDb(c);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
computerDb.close();
|
||||
}
|
||||
|
||||
private void initializeDb() {
|
||||
private void initializeDb(Context c) {
|
||||
// Add cert column to the table if not present
|
||||
try {
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"ALTER TABLE %s ADD COLUMN %s TEXT",
|
||||
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
|
||||
} catch (SQLiteException e) {}
|
||||
|
||||
|
||||
// Create tables if they aren't already there
|
||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
|
||||
COMPUTER_TABLE_NAME,
|
||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
|
||||
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
||||
|
||||
// Move all computers from the old DB (if any) to the new one
|
||||
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
||||
for (ComputerDetails computer : oldComputers) {
|
||||
updateComputer(computer);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteComputer(String name) {
|
||||
@@ -60,20 +79,30 @@ public class ComputerDatabaseManager {
|
||||
|
||||
public boolean updateComputer(ComputerDetails details) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
|
||||
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
|
||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
|
||||
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
||||
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
||||
try {
|
||||
if (details.serverCert != null) {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, details.serverCert.getEncoded());
|
||||
}
|
||||
else {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
String uuidStr = c.getString(0);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -81,43 +110,25 @@ public class ComputerDatabaseManager {
|
||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||
}
|
||||
|
||||
// An earlier schema defined addresses as byte blobs. We'll
|
||||
// gracefully migrate those to strings so we can store DNS names
|
||||
// too. To disambiguate, we'll need to prefix them with a string
|
||||
// greater than the allowable IP address length.
|
||||
try {
|
||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy local address for "+details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(2);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
||||
}
|
||||
else {
|
||||
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
||||
}
|
||||
}
|
||||
details.name = c.getString(1);
|
||||
details.localAddress = c.getString(2);
|
||||
details.remoteAddress = c.getString(3);
|
||||
details.manualAddress = c.getString(4);
|
||||
details.macAddress = c.getString(5);
|
||||
|
||||
try {
|
||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy remote address for "+details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(3);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
||||
}
|
||||
else {
|
||||
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
||||
}
|
||||
}
|
||||
byte[] derCertData = c.getBlob(6);
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
if (derCertData != null) {
|
||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
|
||||
return details;
|
||||
}
|
||||
@@ -128,13 +139,11 @@ public class ComputerDatabaseManager {
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
// If a field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
||||
details.macAddress == null) {
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
@@ -143,8 +152,8 @@ public class ComputerDatabaseManager {
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerByName(String name) {
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
||||
public ComputerDetails getComputerByUUID(UUID uuid) {
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid.toString() }, null, null, null);
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
c.close();
|
||||
@@ -154,9 +163,8 @@ public class ComputerDatabaseManager {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
c.close();
|
||||
|
||||
// If a field is corrupt or missing, delete the database entry
|
||||
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
||||
details.macAddress == null) {
|
||||
// If a critical field is corrupt or missing, delete the database entry
|
||||
if (details.uuid == null) {
|
||||
deleteComputer(details.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ package com.limelight.computers;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -15,6 +14,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.discovery.DiscoveryService;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
@@ -95,7 +95,6 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
details.state = ComputerDetails.State.OFFLINE;
|
||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
releaseLocalDatabaseReference();
|
||||
@@ -106,17 +105,27 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
// If it's online, update our persistent state
|
||||
if (details.state == ComputerDetails.State.ONLINE) {
|
||||
if (!newPc) {
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (dbManager.getComputerByName(details.name) == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
|
||||
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (!newPc && existingComputer == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
|
||||
dbManager.updateComputer(details);
|
||||
// If we already have an entry for this computer in the DB, we must
|
||||
// combine the existing data with this new data (which may be partially available
|
||||
// due to detecting the PC via mDNS) without the saved external address. If we
|
||||
// write to the DB without doing this first, we can overwrite our existing data.
|
||||
if (existingComputer != null) {
|
||||
existingComputer.update(details);
|
||||
dbManager.updateComputer(existingComputer);
|
||||
}
|
||||
else {
|
||||
dbManager.updateComputer(details);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't call the listener if this is a failed lookup of a new PC
|
||||
@@ -156,7 +165,7 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
};
|
||||
t.setName("Polling thread for " + tuple.computer.localAddress);
|
||||
t.setName("Polling thread for " + tuple.computer.name);
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -177,7 +186,6 @@ public class ComputerManagerService extends Service {
|
||||
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
||||
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
}
|
||||
|
||||
// Report this computer initially
|
||||
@@ -253,7 +261,6 @@ public class ComputerManagerService extends Service {
|
||||
// from wiping this change out
|
||||
synchronized (tuple.networkLock) {
|
||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,22 +314,13 @@ public class ComputerManagerService extends Service {
|
||||
};
|
||||
}
|
||||
|
||||
private void addTuple(ComputerDetails details, boolean manuallyAdded) {
|
||||
private void addTuple(ComputerDetails details) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// Check if this is the same computer
|
||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||
if (manuallyAdded) {
|
||||
// Update details anyway in case this machine has been re-added by IP
|
||||
// after not being reachable by our existing information
|
||||
tuple.computer.localAddress = details.localAddress;
|
||||
tuple.computer.remoteAddress = details.remoteAddress;
|
||||
}
|
||||
else {
|
||||
// This indicates that mDNS discovered this address, so we
|
||||
// should only apply the local address.
|
||||
tuple.computer.localAddress = details.localAddress;
|
||||
}
|
||||
// Update the saved computer with potentially new details
|
||||
tuple.computer.update(details);
|
||||
|
||||
// Start a polling thread if polling is active
|
||||
if (pollingActive && tuple.thread == null) {
|
||||
@@ -350,12 +348,40 @@ public class ComputerManagerService extends Service {
|
||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localAddress = addr;
|
||||
fakeDetails.remoteAddress = addr;
|
||||
|
||||
if (manuallyAdded) {
|
||||
// Add PC UI
|
||||
fakeDetails.manualAddress = addr;
|
||||
}
|
||||
else {
|
||||
// mDNS
|
||||
fakeDetails.localAddress = addr;
|
||||
|
||||
// Since we're on the same network, we can use STUN to find
|
||||
// our WAN address, which is also very likely the WAN address
|
||||
// of the PC. We can use this later to connect remotely.
|
||||
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns();
|
||||
}
|
||||
|
||||
// Block while we try to fill the details
|
||||
try {
|
||||
runPoll(fakeDetails, true, 0);
|
||||
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
||||
// See if we have record of this PC to pull its pinned cert
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (tuple.computer.uuid.equals(fakeDetails.uuid)) {
|
||||
fakeDetails.serverCert = tuple.computer.serverCert;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fakeDetails.serverCert != null) {
|
||||
// Poll again with the pinned cert to get accurate pairing information
|
||||
runPoll(fakeDetails, true, 0);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -365,7 +391,7 @@ public class ComputerManagerService extends Service {
|
||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||
|
||||
// Start a polling thread for this machine
|
||||
addTuple(fakeDetails, manuallyAdded);
|
||||
addTuple(fakeDetails);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
@@ -425,8 +451,8 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
||||
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
ComputerDetails newDetails = http.getComputerDetails();
|
||||
|
||||
@@ -438,6 +464,9 @@ public class ComputerManagerService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the new active address
|
||||
newDetails.activeAddress = address;
|
||||
|
||||
return newDetails;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
@@ -447,6 +476,11 @@ public class ComputerManagerService extends Service {
|
||||
// Just try to establish a TCP connection to speculatively detect a running
|
||||
// GFE server
|
||||
private boolean fastPollIp(String address) {
|
||||
if (address == null) {
|
||||
// Don't bother if our address is null
|
||||
return false;
|
||||
}
|
||||
|
||||
Socket s = new Socket();
|
||||
try {
|
||||
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||
@@ -475,12 +509,14 @@ public class ComputerManagerService extends Service {
|
||||
t.start();
|
||||
}
|
||||
|
||||
private ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) throws InterruptedException {
|
||||
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException {
|
||||
final boolean[] remoteInfo = new boolean[2];
|
||||
final boolean[] localInfo = new boolean[2];
|
||||
final boolean[] manualInfo = new boolean[2];
|
||||
|
||||
startFastPollThread(localAddress, localInfo);
|
||||
startFastPollThread(remoteAddress, remoteInfo);
|
||||
startFastPollThread(manualAddress, manualInfo);
|
||||
|
||||
// Check local first
|
||||
synchronized (localInfo) {
|
||||
@@ -489,174 +525,78 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
if (localInfo[1]) {
|
||||
return ComputerDetails.Reachability.LOCAL;
|
||||
return localAddress;
|
||||
}
|
||||
}
|
||||
|
||||
// Now remote
|
||||
// Now manual
|
||||
synchronized (manualInfo) {
|
||||
while (!manualInfo[0]) {
|
||||
manualInfo.wait(500);
|
||||
}
|
||||
|
||||
if (manualInfo[1]) {
|
||||
return manualAddress;
|
||||
}
|
||||
}
|
||||
|
||||
// And finally, remote
|
||||
synchronized (remoteInfo) {
|
||||
while (!remoteInfo[0]) {
|
||||
remoteInfo.wait(500);
|
||||
}
|
||||
|
||||
if (remoteInfo[1]) {
|
||||
return ComputerDetails.Reachability.REMOTE;
|
||||
return remoteAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
|
||||
private static boolean isAddressLikelyLocal(String str) {
|
||||
try {
|
||||
// This will tend to be wrong for IPv6 but falling back to
|
||||
// remote will be fine in that case. For IPv4, it should be
|
||||
// pretty accurate due to NAT prevalence.
|
||||
InetAddress addr = InetAddress.getByName(str);
|
||||
return addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
||||
ComputerDetails polledDetails;
|
||||
ComputerDetails.Reachability reachability;
|
||||
|
||||
if (details.localAddress.equals(details.remoteAddress)) {
|
||||
reachability = isAddressLikelyLocal(details.localAddress) ?
|
||||
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else {
|
||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +")");
|
||||
reachability = fastPollPc(details.localAddress, details.remoteAddress);
|
||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||
|
||||
// If no connection could be established to either IP address, there's nothing we can do
|
||||
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||
|
||||
if (localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localAddress);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details, details.remoteAddress);
|
||||
}
|
||||
|
||||
String reachableAddr = null;
|
||||
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
|
||||
// Failed, so let's try the fallback
|
||||
if (!localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localAddress);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details, details.remoteAddress);
|
||||
}
|
||||
|
||||
if (polledDetails != null) {
|
||||
// The fallback poll worked
|
||||
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
|
||||
}
|
||||
}
|
||||
else if (polledDetails != null) {
|
||||
reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
|
||||
}
|
||||
|
||||
if (reachableAddr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If both addresses are the same, guess whether we're local based on
|
||||
// IP address heuristics.
|
||||
if (reachableAddr.equals(polledDetails.localAddress) &&
|
||||
reachableAddr.equals(polledDetails.remoteAddress)) {
|
||||
polledDetails.reachability = isAddressLikelyLocal(reachableAddr) ?
|
||||
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else if (polledDetails.remoteAddress.equals(reachableAddr)) {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else if (polledDetails.localAddress.equals(reachableAddr)) {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
||||
}
|
||||
else {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
}
|
||||
|
||||
return new ReachabilityTuple(polledDetails, reachableAddr);
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||
ReachabilityTuple initialReachTuple = pollForReachability(details);
|
||||
if (initialReachTuple == null) {
|
||||
ComputerDetails polledDetails;
|
||||
|
||||
// Do a TCP-level connection to the HTTP server to see if it's listening.
|
||||
// Do not write this address to details.activeAddress because:
|
||||
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
|
||||
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
|
||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")");
|
||||
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
|
||||
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
|
||||
|
||||
// If no connection could be established to either IP address, there's nothing we can do
|
||||
if (candidateAddress == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
// Neither IP address reported in the serverinfo response was the one we used.
|
||||
// Poll again to see if we can contact this machine on either of its reported addresses.
|
||||
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
||||
if (confirmationReachTuple == null) {
|
||||
// Neither of those seem to work, so we'll hold onto the address that did work
|
||||
initialReachTuple.computer.localAddress = initialReachTuple.reachableAddress;
|
||||
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
||||
}
|
||||
else {
|
||||
// We got it on one of the returned addresses; replace the original reach tuple
|
||||
// with the new one
|
||||
initialReachTuple = confirmationReachTuple;
|
||||
// Try using the active address from fast-poll
|
||||
polledDetails = tryPollIp(details, candidateAddress);
|
||||
if (polledDetails == null) {
|
||||
// If that failed, try all unique addresses except what we've
|
||||
// already tried
|
||||
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||
uniqueAddresses.add(details.localAddress);
|
||||
uniqueAddresses.add(details.remoteAddress);
|
||||
uniqueAddresses.add(details.manualAddress);
|
||||
for (String addr : uniqueAddresses) {
|
||||
if (addr == null || addr.equals(candidateAddress)) {
|
||||
continue;
|
||||
}
|
||||
polledDetails = tryPollIp(details, addr);
|
||||
if (polledDetails != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save some details about the old state of the PC that we may wish
|
||||
// to restore later.
|
||||
String savedMacAddress = details.macAddress;
|
||||
String savedLocalAddress = details.localAddress;
|
||||
String savedRemoteAddress = details.remoteAddress;
|
||||
|
||||
// If we got here, it's reachable
|
||||
details.update(initialReachTuple.computer);
|
||||
|
||||
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
||||
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
||||
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
||||
details.macAddress = savedMacAddress;
|
||||
if (polledDetails != null) {
|
||||
details.update(polledDetails);
|
||||
return true;
|
||||
}
|
||||
|
||||
// We never want to lose IP addresses by polling server info. If we get a poll back
|
||||
// where localAddress == remoteAddress but savedLocalAddress != savedRemoteAddress,
|
||||
// then we've lost an address in the polling and we should restore the one that's missing.
|
||||
if (details.localAddress.equals(details.remoteAddress) &&
|
||||
!savedLocalAddress.equals(savedRemoteAddress)) {
|
||||
if (details.localAddress.equals(savedLocalAddress)) {
|
||||
// Local addresses are identical, so put the old remote address back
|
||||
details.remoteAddress = savedRemoteAddress;
|
||||
}
|
||||
else if (details.remoteAddress.equals(savedRemoteAddress)) {
|
||||
// Remote addresses are identical, so put the old local address back
|
||||
details.localAddress = savedLocalAddress;
|
||||
}
|
||||
else {
|
||||
// Neither IP address match. Let's restore the remote address to be safe.
|
||||
details.remoteAddress = savedRemoteAddress;
|
||||
}
|
||||
|
||||
// Now update the reachability so the correct address is used
|
||||
if (details.localAddress.equals(initialReachTuple.reachableAddress)) {
|
||||
details.reachability = ComputerDetails.Reachability.LOCAL;
|
||||
}
|
||||
else {
|
||||
details.reachability = ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -679,7 +619,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||
// Add tuples for each computer
|
||||
addTuple(computer, true);
|
||||
addTuple(computer);
|
||||
}
|
||||
|
||||
releaseLocalDatabaseReference();
|
||||
@@ -774,7 +714,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
String appList;
|
||||
if (tuple != null) {
|
||||
@@ -841,7 +781,7 @@ public class ComputerManagerService extends Service {
|
||||
} while (waitPollingDelay());
|
||||
}
|
||||
};
|
||||
thread.setName("App list polling thread for " + computer.localAddress);
|
||||
thread.setName("App list polling thread for " + computer.name);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class LegacyDatabaseReader {
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
|
||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
||||
|
||||
private static ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for " + details.name);
|
||||
}
|
||||
|
||||
// An earlier schema defined addresses as byte blobs. We'll
|
||||
// gracefully migrate those to strings so we can store DNS names
|
||||
// too. To disambiguate, we'll need to prefix them with a string
|
||||
// greater than the allowable IP address length.
|
||||
try {
|
||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy local address for " + details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(2);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
||||
} else {
|
||||
LimeLog.severe("DB: Corrupted local address for " + details.name);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy remote address for " + details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(3);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
||||
} else {
|
||||
LimeLog.severe("DB: Corrupted remote address for " + details.name);
|
||||
}
|
||||
}
|
||||
|
||||
// On older versions of Moonlight, this is typically where manual addresses got stored,
|
||||
// so let's initialize it just to be safe.
|
||||
details.manualAddress = details.remoteAddress;
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
private static List<ComputerDetails> getAllComputers(SQLiteDatabase db) {
|
||||
Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
||||
SQLiteDatabase computerDb = null;
|
||||
try {
|
||||
// Open the existing database
|
||||
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||
return getAllComputers(computerDb);
|
||||
} catch (SQLiteException e) {
|
||||
return new LinkedList<ComputerDetails>();
|
||||
} finally {
|
||||
// Close and delete the old DB
|
||||
if (computerDb != null) {
|
||||
computerDb.close();
|
||||
}
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
imgView.setAlpha(0.4f);
|
||||
}
|
||||
|
||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
|
||||
prgView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -22,7 +22,8 @@ public class NetworkAssetLoader {
|
||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
InputStream in = null;
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
|
||||
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
|
||||
in = http.getBoxArt(tuple.app);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
public class PreferenceConfiguration {
|
||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
|
||||
|
||||
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
||||
static final String FPS_PREF_STRING = "list_fps";
|
||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
||||
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||
@@ -29,18 +33,11 @@ public class PreferenceConfiguration {
|
||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
||||
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
||||
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
|
||||
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
||||
|
||||
private static final int BITRATE_DEFAULT_360_30 = 1000;
|
||||
private static final int BITRATE_DEFAULT_360_60 = 2000;
|
||||
private static final int BITRATE_DEFAULT_720_30 = 5000;
|
||||
private static final int BITRATE_DEFAULT_720_60 = 10000;
|
||||
private static final int BITRATE_DEFAULT_1080_30 = 10000;
|
||||
private static final int BITRATE_DEFAULT_1080_60 = 20000;
|
||||
private static final int BITRATE_DEFAULT_4K_30 = 40000;
|
||||
private static final int BITRATE_DEFAULT_4K_60 = 80000;
|
||||
|
||||
private static final String DEFAULT_RES_FPS = "720p60";
|
||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||
static final String DEFAULT_RESOLUTION = "720p";
|
||||
static final String DEFAULT_FPS = "60";
|
||||
private static final boolean DEFAULT_STRETCH = false;
|
||||
private static final boolean DEFAULT_SOPS = true;
|
||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||
@@ -54,12 +51,13 @@ public class PreferenceConfiguration {
|
||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
||||
private static final boolean DEFAULT_BATTERY_SAVER = false;
|
||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
||||
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
||||
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
||||
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
||||
|
||||
public static final int FORCE_H265_ON = -1;
|
||||
public static final int AUTOSELECT_H265 = 0;
|
||||
@@ -79,35 +77,80 @@ public class PreferenceConfiguration {
|
||||
public boolean enablePip;
|
||||
public boolean bindAllUsb;
|
||||
public boolean mouseEmulation;
|
||||
public boolean mouseNavButtons;
|
||||
public boolean unlockFps;
|
||||
|
||||
public static int getDefaultBitrate(String resFpsString) {
|
||||
if (resFpsString.equals("360p30")) {
|
||||
return BITRATE_DEFAULT_360_30;
|
||||
private static int getHeightFromResolutionString(String resString) {
|
||||
if (resString.equalsIgnoreCase("360p")) {
|
||||
return 360;
|
||||
}
|
||||
else if (resFpsString.equals("360p60")) {
|
||||
return BITRATE_DEFAULT_360_60;
|
||||
else if (resString.equalsIgnoreCase("720p")) {
|
||||
return 720;
|
||||
}
|
||||
else if (resFpsString.equals("720p30")) {
|
||||
return BITRATE_DEFAULT_720_30;
|
||||
else if (resString.equalsIgnoreCase("1080p")) {
|
||||
return 1080;
|
||||
}
|
||||
else if (resFpsString.equals("720p60")) {
|
||||
return BITRATE_DEFAULT_720_60;
|
||||
else if (resString.equalsIgnoreCase("1440p")) {
|
||||
return 1440;
|
||||
}
|
||||
else if (resFpsString.equals("1080p30")) {
|
||||
return BITRATE_DEFAULT_1080_30;
|
||||
}
|
||||
else if (resFpsString.equals("1080p60")) {
|
||||
return BITRATE_DEFAULT_1080_60;
|
||||
}
|
||||
else if (resFpsString.equals("4K30")) {
|
||||
return BITRATE_DEFAULT_4K_30;
|
||||
}
|
||||
else if (resFpsString.equals("4K60")) {
|
||||
return BITRATE_DEFAULT_4K_60;
|
||||
else if (resString.equalsIgnoreCase("4K")) {
|
||||
return 2160;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return DEFAULT_BITRATE;
|
||||
// Should be unreachable
|
||||
return 720;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getWidthFromResolutionString(String resString) {
|
||||
return (getHeightFromResolutionString(resString) * 16) / 9;
|
||||
}
|
||||
|
||||
private static String getResolutionString(int width, int height) {
|
||||
switch (height) {
|
||||
case 360:
|
||||
return "360p";
|
||||
default:
|
||||
case 720:
|
||||
return "720p";
|
||||
case 1080:
|
||||
return "1080p";
|
||||
case 1440:
|
||||
return "1440p";
|
||||
case 2160:
|
||||
return "4K";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(String resString, String fpsString) {
|
||||
int width = getWidthFromResolutionString(resString);
|
||||
int height = getHeightFromResolutionString(resString);
|
||||
int fps = Integer.parseInt(fpsString);
|
||||
|
||||
// This table prefers 16:10 resolutions because they are
|
||||
// only slightly more pixels than the 16:9 equivalents, so
|
||||
// we don't want to bump those 16:10 resolutions up to the
|
||||
// next 16:9 slot.
|
||||
//
|
||||
// This logic is shamelessly stolen from Moonlight Qt:
|
||||
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
|
||||
|
||||
if (width * height <= 640 * 360) {
|
||||
return (int)(1000 * (fps / 30.0));
|
||||
}
|
||||
// This covers 1280x720 and 1280x800 too
|
||||
else if (width * height <= 1366 * 768) {
|
||||
return (int)(5000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 1920 * 1200) {
|
||||
return (int)(10000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 2560 * 1600) {
|
||||
return (int)(20000 * (fps / 30.0));
|
||||
}
|
||||
else /* if (width * height <= 3840 * 2160) */ {
|
||||
return (int)(40000 * (fps / 30.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +176,9 @@ public class PreferenceConfiguration {
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
||||
return getDefaultBitrate(
|
||||
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
|
||||
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||
}
|
||||
|
||||
private static int getVideoFormatValue(Context context) {
|
||||
@@ -161,9 +206,12 @@ public class PreferenceConfiguration {
|
||||
prefs.edit()
|
||||
.remove(BITRATE_PREF_STRING)
|
||||
.remove(BITRATE_PREF_OLD_STRING)
|
||||
.remove(RES_FPS_PREF_STRING)
|
||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||
.remove(RESOLUTION_PREF_STRING)
|
||||
.remove(FPS_PREF_STRING)
|
||||
.remove(VIDEO_FORMAT_PREF_STRING)
|
||||
.remove(ENABLE_HDR_PREF_STRING)
|
||||
.remove(UNLOCK_FPS_STRING)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -171,59 +219,76 @@ public class PreferenceConfiguration {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||
|
||||
// Migrate legacy preferences to the new locations
|
||||
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
||||
if (str != null) {
|
||||
if (str.equals("360p30")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("360p60")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("4K30")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("4K60")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||
.putString(RESOLUTION_PREF_STRING, getResolutionString(config.width, config.height))
|
||||
.putString(FPS_PREF_STRING, ""+config.fps)
|
||||
.apply();
|
||||
}
|
||||
else {
|
||||
// Use the new preference location
|
||||
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
||||
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
||||
}
|
||||
|
||||
// This must happen after the preferences migration to ensure the preferences are populated
|
||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
||||
if (config.bitrate == 0) {
|
||||
config.bitrate = getDefaultBitrate(context);
|
||||
}
|
||||
|
||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||
if (str.equals("360p30")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("360p60")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("4K30")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("4K60")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
|
||||
config.videoFormat = getVideoFormatValue(context);
|
||||
|
||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||
@@ -247,6 +312,8 @@ public class PreferenceConfiguration {
|
||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
||||
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
||||
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
||||
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.media.MediaCodecInfo;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
@@ -24,6 +25,12 @@ import com.limelight.utils.UiHelper;
|
||||
public class StreamSettings extends Activity {
|
||||
private PreferenceConfiguration previousPrefs;
|
||||
|
||||
void reloadSettings() {
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -33,9 +40,7 @@ public class StreamSettings extends Activity {
|
||||
UiHelper.setLocale(this);
|
||||
|
||||
setContentView(R.layout.activity_stream_settings);
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
reloadSettings();
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
}
|
||||
@@ -58,12 +63,20 @@ public class StreamSettings extends Activity {
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment {
|
||||
|
||||
private static void removeResolution(ListPreference pref, String prefix) {
|
||||
private void setValue(String preferenceKey, String value) {
|
||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||
|
||||
pref.setValue(value);
|
||||
}
|
||||
|
||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||
int matchingCount = 0;
|
||||
|
||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||
|
||||
// Count the number of matching entries we'll be removing
|
||||
for (CharSequence seq : pref.getEntryValues()) {
|
||||
if (seq.toString().startsWith(prefix)) {
|
||||
if (seq.toString().equalsIgnoreCase(value)) {
|
||||
matchingCount++;
|
||||
}
|
||||
}
|
||||
@@ -73,8 +86,8 @@ public class StreamSettings extends Activity {
|
||||
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
||||
int outIndex = 0;
|
||||
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
||||
if (pref.getEntryValues()[i].toString().startsWith(prefix)) {
|
||||
// Skip matching prefixes
|
||||
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
||||
// Skip matching values
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -83,15 +96,34 @@ public class StreamSettings extends Activity {
|
||||
outIndex++;
|
||||
}
|
||||
|
||||
if (pref.getValue().equalsIgnoreCase(value)) {
|
||||
onMatched.run();
|
||||
}
|
||||
|
||||
// Update the preference with the new list
|
||||
pref.setEntries(entries);
|
||||
pref.setEntryValues(entryValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
||||
if (res == null) {
|
||||
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||
}
|
||||
if (fps == null) {
|
||||
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(res, fps))
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
PreferenceScreen screen = getPreferenceScreen();
|
||||
|
||||
@@ -110,6 +142,8 @@ public class StreamSettings extends Activity {
|
||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
||||
}
|
||||
|
||||
int maxSupportedFps = 0;
|
||||
|
||||
// Hide non-supported resolution/FPS combinations
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||
@@ -134,9 +168,16 @@ public class StreamSettings extends Activity {
|
||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||
maxSupportedResW = 3840;
|
||||
}
|
||||
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
|
||||
maxSupportedResW = 2560;
|
||||
}
|
||||
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
||||
maxSupportedResW = 1920;
|
||||
}
|
||||
|
||||
if (candidate.getRefreshRate() > maxSupportedFps) {
|
||||
maxSupportedFps = (int)candidate.getRefreshRate();
|
||||
}
|
||||
}
|
||||
|
||||
// This must be called to do runtime initialization before calling functions that evaluate
|
||||
@@ -186,20 +227,102 @@ public class StreamSettings extends Activity {
|
||||
|
||||
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
||||
|
||||
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
|
||||
if (maxSupportedResW != 0) {
|
||||
if (maxSupportedResW < 3840) {
|
||||
// 4K is unsupported
|
||||
removeResolution(resPref, "4K");
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedResW < 2560) {
|
||||
// 1440p is unsupported
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedResW < 1920) {
|
||||
// 1080p is unsupported
|
||||
removeResolution(resPref, "1080p");
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Never remove 720p
|
||||
}
|
||||
}
|
||||
|
||||
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
||||
// We give some extra room in case the FPS is rounded down
|
||||
if (maxSupportedFps < 118) {
|
||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "120", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "90");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedFps < 88) {
|
||||
// 1080p is unsupported
|
||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "90", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "60");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Never remove 30 FPS or 60 FPS
|
||||
}
|
||||
|
||||
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
|
||||
// that the unlock FPS option relies on to not massively increase latency.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_basic_settings");
|
||||
category.removePreference(findPreference("checkbox_unlock_fps"));
|
||||
}
|
||||
else {
|
||||
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
// HACK: We need to let the preference change succeed before reinitializing to ensure
|
||||
// it's reflected in the new layout.
|
||||
final Handler h = new Handler();
|
||||
h.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Ensure the activity is still open when this timeout expires
|
||||
StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity();
|
||||
if (settingsActivity != null) {
|
||||
settingsActivity.reloadSettings();
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove HDR preference for devices below Nougat
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
LimeLog.info("Excluding HDR toggle based on OS");
|
||||
@@ -213,9 +336,12 @@ public class StreamSettings extends Activity {
|
||||
|
||||
// We must now ensure our display is compatible with HDR10
|
||||
boolean foundHdr10 = false;
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
foundHdr10 = true;
|
||||
if (hdrCaps != null) {
|
||||
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
foundHdr10 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,18 +355,27 @@ public class StreamSettings extends Activity {
|
||||
|
||||
// Add a listener to the FPS and resolution preference
|
||||
// so the bitrate can be auto-adjusted
|
||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
||||
.apply();
|
||||
resetBitrateToDefault(prefs, valueStr, null);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
findPreference(PreferenceConfiguration.FPS_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
resetBitrateToDefault(prefs, null, valueStr);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
|
||||
@@ -62,9 +62,8 @@ public class StreamView extends SurfaceView {
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
// This callbacks allows us to override dumb IME behavior like when
|
||||
// Samsung's default keyboard consumes Shift+Space. We'll process
|
||||
// the input event directly if any modifier keys are down.
|
||||
if (inputCallbacks != null && event.getModifiers() != 0) {
|
||||
// Samsung's default keyboard consumes Shift+Space.
|
||||
if (inputCallbacks != null) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (inputCallbacks.handleKeyDown(event)) {
|
||||
return true;
|
||||
|
||||
@@ -15,11 +15,11 @@ import com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
|
||||
public class ServerHelper {
|
||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||
computer.localAddress : computer.remoteAddress;
|
||||
return computer.activeAddress;
|
||||
}
|
||||
|
||||
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||
@@ -30,20 +30,30 @@ public class ServerHelper {
|
||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
|
||||
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||
try {
|
||||
if (computer.serverCert != null) {
|
||||
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||
}
|
||||
|
||||
public static void doQuit(final Activity parent,
|
||||
final String address,
|
||||
final ComputerDetails computer,
|
||||
final NvApp app,
|
||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||
final Runnable onComplete) {
|
||||
@@ -54,8 +64,8 @@ public class ServerHelper {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(address,
|
||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
|
||||
if (httpConn.quitApp()) {
|
||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -135,4 +136,32 @@ public class UiHelper {
|
||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void displayDeletePcConfirmationDialog(Activity parent, ComputerDetails computer, final Runnable onYes, final Runnable onNo) {
|
||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which){
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
if (onYes != null) {
|
||||
onYes.run();
|
||||
}
|
||||
break;
|
||||
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
if (onNo != null) {
|
||||
onNo.run();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||
builder.setMessage(parent.getResources().getString(R.string.delete_pc_msg))
|
||||
.setTitle(computer.name)
|
||||
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
<string name="lost_connection">Conexión perdida</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Aplicaciones en</string>
|
||||
<string name="applist_menu_resume">Reanudar sesión</string>
|
||||
<string name="applist_menu_quit">Cerrar sesión</string>
|
||||
<string name="applist_menu_quit_and_start">Cerrar juego actual e iniciar</string>
|
||||
@@ -98,7 +97,6 @@
|
||||
<string name="title_checkbox_51_surround">Activar sonido 5.1 surround</string>
|
||||
<string name="summary_checkbox_51_surround">Desmarcar si experimentas problemas de audio. Requiere GFE 2.7 o superior.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Configuración de mando</string>
|
||||
<string name="title_checkbox_multi_controller">Soporte para múltiples mandos</string>
|
||||
<string name="summary_checkbox_multi_controller">Si no está marcado, todos los mandos aparecen como uno solo</string>
|
||||
<string name="title_seekbar_deadzone">Ajustar zona muerta del stick analógico</string>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<!-- Shortcut strings -->
|
||||
<string name="scut_deleted_pc">PC supprimé</string>
|
||||
<string name="scut_not_paired">PC non appairé</string>
|
||||
<string name="scut_pc_not_found">PC non trouvé</string>
|
||||
<string name="scut_invalid_uuid">Le PC fourni n\'est pas valide</string>
|
||||
<string name="scut_invalid_app_id">L\'application fournie n\'est pas valide</string>
|
||||
|
||||
<!-- Help strings -->
|
||||
<string name="help_loading_title">Visionneuse d\'aide</string>
|
||||
@@ -14,6 +17,7 @@
|
||||
<string name="pcview_menu_unpair_pc">Désappairer</string>
|
||||
<string name="pcview_menu_send_wol">Envoyer la requête Wake-On-LAN</string>
|
||||
<string name="pcview_menu_delete_pc">Supprimer PC</string>
|
||||
<string name="pcview_menu_details">Voir les détails</string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing">Appariement…</string>
|
||||
@@ -47,6 +51,12 @@
|
||||
<string name="error_404">GFE renvoi une erreur HTTP 404. Assurez-vous que votre PC exécute un GPU pris en charge.
|
||||
L\'utilisation d\'un logiciel de bureau à distance peut également provoquer cette erreur. Essayez de redémarrer votre machine ou de réinstaller GFE.
|
||||
</string>
|
||||
<string name="title_decoding_error">Le décodeur vidéo s\'est écrasé</string>
|
||||
<string name="message_decoding_error">Moonlight s\'est écrasé en raison d\'une incompatibilité avec le décodeur vidéo de cet appareil. Assurez-vous que GeForce Experience soit mis à jour vers la dernière version sur votre PC. Essayez de régler les paramètres de diffusion si les plantages continuent.</string>
|
||||
<string name="title_decoding_reset">Paramètres vidéo réinitialiser</string>
|
||||
<string name="message_decoding_reset">Le décodeur vidéo de votre appareil continue de planter avec les paramètres de diffusion sélectionnés. Vos paramètres de diffusion ont été réinitialisés par défaut.</string>
|
||||
<string name="error_usb_prohibited">L\'accès USB est interdit par votre appareil. Vérifiez vos paramètres Knox ou MDM.</string>
|
||||
<string name="unable_to_pin_shortcut">Votre lanceur actuel ne permet pas de créer des raccourcis épinglés.</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">Établissement de la connexion</string>
|
||||
@@ -63,20 +73,23 @@
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">Adresse IP de GeForce PC</string>
|
||||
<string name="searching_pc">Recherche de PC avec GameStream en cours...\n\n
|
||||
<string name="searching_pc">Recherche de PC avec GameStream en cours…\n\n
|
||||
Assurez-vous que GameStream est activé dans les paramètres GeForce Experience SHIELD.</string>
|
||||
<string name="yes">Oui</string>
|
||||
<string name="no">Non</string>
|
||||
<string name="lost_connection">Perte de connexion avec le PC</string>
|
||||
<string name="title_details">Détails</string>
|
||||
<string name="help">Aide</string>
|
||||
<string name="delete_pc_msg">Êtes-vous sûr de vouloir supprimer ce PC?</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Applications sur</string>
|
||||
<string name="applist_connect_msg">Connexion au PC…</string>
|
||||
<string name="applist_menu_resume">Reprise de la session</string>
|
||||
<string name="applist_menu_quit">Quitter la session</string>
|
||||
<string name="applist_menu_quit_and_start">Quitter le jeu actuel et démarrer</string>
|
||||
<string name="applist_menu_cancel">Annuler</string>
|
||||
<string name="applist_menu_details">Voir les détails</string>
|
||||
<string name="applist_menu_scut">Créer un raccourci</string>
|
||||
<string name="applist_refresh_title">Liste des applications</string>
|
||||
<string name="applist_refresh_msg">Actualisation des applications…</string>
|
||||
<string name="applist_refresh_error_title">Erreur</string>
|
||||
@@ -85,6 +98,7 @@
|
||||
<string name="applist_quit_success">Fermeture avec succès</string>
|
||||
<string name="applist_quit_fail">Échec de la fermeture</string>
|
||||
<string name="applist_quit_confirmation">Voulez-vous vraiment quitter l\'application en cours d\'exécution? Toutes les données non enregistrées seront perdues.</string>
|
||||
<string name="applist_details_id">ID app:</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc">Ajouter un PC manuellement</string>
|
||||
@@ -93,35 +107,53 @@
|
||||
<string name="addpc_success">Ajouté avec succès de l\'ordinateur</string>
|
||||
<string name="addpc_unknown_host">Impossible de résoudre l\'adresse du PC. Assurez-vous que vous n\'avez pas fait une faute de frappe dans l\'adresse.</string>
|
||||
<string name="addpc_enter_ip">Vous devez entrer une adresse IP</string>
|
||||
<string name="addpc_wrong_sitelocal">Cette adresse ne semble pas correcte. Vous devez utiliser l\'adresse IP publique de votre routeur pour la diffusion en continu sur Internet..</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Paramètres de base</string>
|
||||
<string name="title_resolution_list">Sélectionner la résolution et les FPS à atteindre</string>
|
||||
<string name="title_resolution_list">Résolution vidéo</string>
|
||||
<string name="summary_resolution_list">Le réglage de valeurs trop élevées pour votre appareil peut provoquer un retard ou un plantage</string>
|
||||
<string name="title_fps_list">Fréquence d\'images vidéo</string>
|
||||
<string name="summary_fps_list">Augmenter pour un flux vidéo plus lisse. Diminution pour de meilleures performances sur les périphériques bas de gamme.</string>
|
||||
<string name="title_seekbar_bitrate">Sélectionnez le bitrate vidéo à obtenir</string>
|
||||
<string name="summary_seekbar_bitrate">Bitrate inférieur pour réduire la saccade. Augmentez le bitrate pour augmenter la qualité de l\'image.</string>
|
||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
||||
<string name="title_unlock_fps">Débloquer tous les taux d\'images possibles</string>
|
||||
<string name="summary_unlock_fps">La diffusion en continu à 90 ou 120 FPS peut réduire la latence sur les périphériques haut de gamme, mais peut provoquer des retards ou des blocages sur les périphériques qui ne peuvent \pas le prendre en charge</string>
|
||||
<string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string>
|
||||
<string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string>
|
||||
<string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string>
|
||||
<string name="title_checkbox_enable_pip">Activer le mode observateur dans l\'image</string>
|
||||
<string name="summary_checkbox_enable_pip">Permet de visualiser le flux (sans le contrôleur) tout en multitâche</string>
|
||||
|
||||
<string name="category_audio_settings">Paramètres audio</string>
|
||||
<string name="title_checkbox_51_surround">Activer son surround 5.1</string>
|
||||
<string name="summary_checkbox_51_surround">Décochez si vous rencontrez des problèmes audio. Nécessite GFE 2.7 ou supérieur.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Paramètres du gamepad</string>
|
||||
<string name="category_input_settings">Paramètres d\'entrée</string>
|
||||
<string name="title_checkbox_multi_controller">Prise en charge de plusieurs contrôleurs</string>
|
||||
<string name="summary_checkbox_multi_controller">Lorsqu\'elle n\'est pas cochée, tous les contrôleurs sont regroupés</string>
|
||||
<string name="title_seekbar_deadzone">Régler la zone morte du stick analogique</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Pilote de contrôleur Xbox 360/One</string>
|
||||
<string name="summary_checkbox_xb1_driver">Active un pilote USB intégré pour les périphériques sans prise en charge du contrôleur Xbox natif.</string>
|
||||
<string name="title_checkbox_usb_bind_all">Ignorer le support du contrôleur Android</string>
|
||||
<string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string>
|
||||
<string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string>
|
||||
<string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Activer les boutons de souris arrière et avant</string>
|
||||
<string name="summary_checkbox_mouse_nav_buttons">L\'activation de cette option peut entraîner un clic droit sur certains périphériques.</string>
|
||||
|
||||
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
|
||||
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
|
||||
<string name="title_only_l3r3">Montre seulement L3 et R3</string>
|
||||
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
|
||||
<string name="title_reset_osc">Effacer la disposition des commandes à l\'écran sauvegardée</string>
|
||||
<string name="summary_reset_osc">Rétablit la taille et la position par défaut de tous les contrôles à l\'écran</string>
|
||||
<string name="dialog_title_reset_osc">Réinitialiser la mise en page</string>
|
||||
<string name="dialog_text_reset_osc">Êtes-vous sûr de vouloir supprimer la disposition des commandes à l\'écran que vous avez sauvegardée?</string>
|
||||
<string name="toast_reset_osc_success">Les contrôles à l\'écran sont réinitialisés</string>
|
||||
|
||||
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
|
||||
<string name="title_language_list">Langue</string>
|
||||
@@ -138,6 +170,11 @@
|
||||
<string name="summary_checkbox_host_audio">Lire l\'audio de l\'ordinateur et de ce périphérique</string>
|
||||
|
||||
<string name="category_advanced_settings">Réglages avancés</string>
|
||||
<string name="title_disable_frame_drop">Désactiver la suppression d\'image</string>
|
||||
<string name="summary_disable_frame_drop">Peut réduire les micro-saccades sur certains appareils, mais peut augmenter la latence</string>
|
||||
<string name="title_video_format">Modifier les paramètres H.265</string>
|
||||
<string name="summary_video_format">H.265 réduit les exigences de bande passante vidéo, mais requiert un périphérique très récent.</string>
|
||||
<string name="summary_video_format">H.265 réduit les besoins en bande passante vidéo mais nécessite un périphérique très récent</string>
|
||||
<string name="title_enable_hdr">Activer le HDR (expérimental)</string>
|
||||
<string name="summary_enable_hdr">Diffuser du HDR lorsque le jeu et le processeur graphique du PC le prennent en charge. HDR nécessite un GPU série GTX 1000 ou une version ultérieure.</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
<string name="help">Assistenza</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Applicazioni su</string>
|
||||
<string name="applist_connect_msg">Connessione al PC in corso…</string>
|
||||
<string name="applist_menu_resume">Riprendi sessione</string>
|
||||
<string name="applist_menu_quit">Chiudi sessione</string>
|
||||
@@ -117,7 +116,6 @@
|
||||
<string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string>
|
||||
<string name="summary_checkbox_51_surround">Se riscontri problemi, disabilitalo. Richiede GFE 2.7 o versioni sucessive.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Impostazioni controller</string>
|
||||
<string name="title_checkbox_multi_controller">Supporto a più controller</string>
|
||||
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string>
|
||||
<string name="title_seekbar_deadzone">Regola i punti morti degli stick analogici</string>
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
<string name="lost_connection">コンピュータとの接続が失われました</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">ゲーム</string>
|
||||
<string name="applist_menu_resume">セッションを続ける</string>
|
||||
<string name="applist_menu_quit">セッションを終了する</string>
|
||||
<string name="applist_menu_quit_and_start">現在のゲームを終了して新しいゲームを始める</string>
|
||||
@@ -94,7 +93,6 @@
|
||||
<string name="title_checkbox_51_surround">5.1chサラウンド</string>
|
||||
<string name="summary_checkbox_51_surround">音声に問題が生じる場合はチェックを外してください。バージョン2.7以降のGFEが必要です</string>
|
||||
|
||||
<string name="category_gamepad_settings">ゲームコントローラ</string>
|
||||
<string name="title_checkbox_multi_controller">複数のゲームコントローラ</string>
|
||||
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
|
||||
<string name="title_seekbar_deadzone">アナログゲームコントローラのデッドゾーン</string>
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
<string name="help">도움말</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">앱 사용 가능</string>
|
||||
<string name="applist_connect_msg">PC에 연결중…</string>
|
||||
<string name="applist_menu_resume">세션 계속</string>
|
||||
<string name="applist_menu_quit">세션 종료</string>
|
||||
@@ -109,7 +108,6 @@
|
||||
<string name="title_checkbox_51_surround">5.1 서라운드 사운드 활성화</string>
|
||||
<string name="summary_checkbox_51_surround">오디오 문제가 발생한다면 체크를 해제하세요. GFE 2.7이나 그 이상 버전이 필요합니다.</string>
|
||||
|
||||
<string name="category_gamepad_settings">게임패드 설정</string>
|
||||
<string name="title_checkbox_multi_controller">다중 컨트롤러 지원</string>
|
||||
<string name="summary_checkbox_multi_controller">이 옵션을 선택하지 않으면 모든 컨트롤러가 하나로 표시됩니다</string>
|
||||
<string name="title_seekbar_deadzone">아날로그 스틱 데드존 설정</string>
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
<string name="lost_connection">Verbinding met PC verloren</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Apps op</string>
|
||||
<string name="applist_menu_resume">Hervat Sessie</string>
|
||||
<string name="applist_menu_quit">Stop Sessie</string>
|
||||
<string name="applist_menu_quit_and_start">Stop Huidige Spel en Start</string>
|
||||
@@ -98,7 +97,6 @@
|
||||
<string name="title_checkbox_51_surround">Gebruik 5.1 surround sound</string>
|
||||
<string name="summary_checkbox_51_surround">Gebruik dit niet als er problemen zijn met de audio. Vereist GFE 2.7 of hoger.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Gamepad Instellingen</string>
|
||||
<string name="title_checkbox_multi_controller">Multi-gamepad support</string>
|
||||
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
|
||||
<string name="title_seekbar_deadzone">Pas analoge dodezone aan.</string>
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
<string name="lost_connection">Потеряно соединение с PC</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Приложения на</string>
|
||||
<string name="applist_menu_resume">Возобновить сессию</string>
|
||||
<string name="applist_menu_quit">Выйти из сессии</string>
|
||||
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
|
||||
@@ -99,7 +98,6 @@
|
||||
<string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string>
|
||||
<string name="summary_checkbox_51_surround">Отключите, если появляются аудио проблемы. Требуется GFE 2.7 или выше.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Настройки Геймпада</string>
|
||||
<string name="title_checkbox_multi_controller">Поддержка нескольких контроллеров</string>
|
||||
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один</string>
|
||||
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика</string>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
-->
|
||||
<style name="AppBaseTheme" parent="android:Theme.Material">
|
||||
<!-- API 21 theme customizations can go here. -->
|
||||
<item name="android:statusBarColor">#212121</item>
|
||||
<item name="android:navigationBarColor">#212121</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
<string name="help">帮助</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Apps on</string>
|
||||
<string name="applist_connect_msg"> 连接到电脑中…… </string>
|
||||
<string name="applist_menu_resume"> 恢复串流 </string>
|
||||
<string name="applist_menu_quit"> 退出串流 </string>
|
||||
@@ -109,7 +108,6 @@
|
||||
<string name="title_checkbox_51_surround"> 启用 5.1 环绕音效 </string>
|
||||
<string name="summary_checkbox_51_surround"> 如果你的声音听起来有问题请禁用。\n\n需要GeForce Experience 2.7 或更高版本 </string>
|
||||
|
||||
<string name="category_gamepad_settings"> 手柄设置 </string>
|
||||
<string name="title_checkbox_multi_controller"> 启用多手柄支持 </string>
|
||||
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄将会认作一个手柄 </string>
|
||||
<string name="title_seekbar_deadzone"> 调整摇杆死区 </string>
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
<string name="help">幫助</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Apps on</string>
|
||||
<string name="applist_connect_msg"> 連接到電腦中…… </string>
|
||||
<string name="applist_menu_resume"> 恢復串流 </string>
|
||||
<string name="applist_menu_quit"> 退出串流 </string>
|
||||
@@ -109,7 +108,6 @@
|
||||
<string name="title_checkbox_51_surround"> 啟用 5.1 環繞音效 </string>
|
||||
<string name="summary_checkbox_51_surround"> 如果你的聲音聽起來有問題請禁用。\n\n需要GeForce Experience 2.7 或更高版本 </string>
|
||||
|
||||
<string name="category_gamepad_settings"> 手柄設置 </string>
|
||||
<string name="title_checkbox_multi_controller"> 啟用多手柄支持 </string>
|
||||
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄將會認作一個手柄 </string>
|
||||
<string name="title_seekbar_deadzone"> 調整搖桿死區 </string>
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="resolution_names">
|
||||
<item>360p 30 FPS</item>
|
||||
<item>360p 60 FPS</item>
|
||||
<item>720p 30 FPS</item>
|
||||
<item>720p 60 FPS</item>
|
||||
<item>1080p 30 FPS</item>
|
||||
<item>1080p 60 FPS</item>
|
||||
<item>4K 30 FPS</item>
|
||||
<item>4K 60 FPS</item>
|
||||
<item>360p</item>
|
||||
<item>720p</item>
|
||||
<item>1080p</item>
|
||||
<item>1440p</item>
|
||||
<item>4K</item>
|
||||
</string-array>
|
||||
<string-array name="resolution_values" translatable="false">
|
||||
<item>360p30</item>
|
||||
<item>360p60</item>
|
||||
<item>720p30</item>
|
||||
<item>720p60</item>
|
||||
<item>1080p30</item>
|
||||
<item>1080p60</item>
|
||||
<item>4K30</item>
|
||||
<item>4K60</item>
|
||||
<item>360p</item>
|
||||
<item>720p</item>
|
||||
<item>1080p</item>
|
||||
<item>1440p</item>
|
||||
<item>4K</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="fps_names">
|
||||
<item>30 FPS</item>
|
||||
<item>60 FPS</item>
|
||||
<item>90 FPS</item>
|
||||
<item>120 FPS</item>
|
||||
</string-array>
|
||||
<string-array name="fps_values" translatable="false">
|
||||
<item>30</item>
|
||||
<item>60</item>
|
||||
<item>90</item>
|
||||
<item>120</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="language_names" translatable="false">
|
||||
|
||||
@@ -83,9 +83,9 @@
|
||||
<string name="lost_connection">Lost connection to PC</string>
|
||||
<string name="title_details">Details</string>
|
||||
<string name="help">Help</string>
|
||||
<string name="delete_pc_msg">Are you sure you want to delete this PC?</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Apps on</string>
|
||||
<string name="applist_connect_msg">Connecting to PC…</string>
|
||||
<string name="applist_menu_resume">Resume Session</string>
|
||||
<string name="applist_menu_quit">Quit Session</string>
|
||||
@@ -114,11 +114,15 @@
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Basic Settings</string>
|
||||
<string name="title_resolution_list">Select resolution and FPS target</string>
|
||||
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</string>
|
||||
<string name="title_seekbar_bitrate">Select target video bitrate</string>
|
||||
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
|
||||
<string name="title_resolution_list">Video resolution</string>
|
||||
<string name="summary_resolution_list">Increase to improve image clarity. Decrease for better performance on lower end devices and slower networks.</string>
|
||||
<string name="title_fps_list">Video frame rate</string>
|
||||
<string name="summary_fps_list">Increase for a smoother video stream. Decrease for better performance on lower end devices.</string>
|
||||
<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>
|
||||
@@ -129,9 +133,9 @@
|
||||
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
|
||||
<string name="summary_checkbox_51_surround">Uncheck if you experience audio issues. Requires GFE 2.7 or higher.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Gamepad Settings</string>
|
||||
<string name="title_checkbox_multi_controller">Multiple controller support</string>
|
||||
<string name="summary_checkbox_multi_controller">Uncheck for games with controller detection issues</string>
|
||||
<string name="category_input_settings">Input Settings</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_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>
|
||||
@@ -140,6 +144,8 @@
|
||||
<string name="summary_checkbox_usb_bind_all">Forces Moonlight\'s USB driver to take over all supported Xbox gamepads</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="category_on_screen_controls_settings">On-screen Controls Settings</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
|
||||
|
||||
@@ -5,12 +5,19 @@
|
||||
<PreferenceCategory android:title="@string/category_basic_settings"
|
||||
android:key="category_basic_settings">
|
||||
<ListPreference
|
||||
android:key="list_resolution_fps"
|
||||
android:key="list_resolution"
|
||||
android:title="@string/title_resolution_list"
|
||||
android:summary="@string/summary_resolution_list"
|
||||
android:entries="@array/resolution_names"
|
||||
android:entryValues="@array/resolution_values"
|
||||
android:defaultValue="720p60" />
|
||||
android:defaultValue="720p" />
|
||||
<ListPreference
|
||||
android:key="list_fps"
|
||||
android:title="@string/title_fps_list"
|
||||
android:summary="@string/summary_fps_list"
|
||||
android:entries="@array/fps_names"
|
||||
android:entryValues="@array/fps_values"
|
||||
android:defaultValue="60" />
|
||||
<com.limelight.preferences.SeekBarPreference
|
||||
android:key="seekbar_bitrate_kbps"
|
||||
android:dialogMessage="@string/summary_seekbar_bitrate"
|
||||
@@ -20,6 +27,11 @@
|
||||
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"
|
||||
@@ -37,7 +49,7 @@
|
||||
android:summary="@string/summary_checkbox_51_surround"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_gamepad_settings">
|
||||
<PreferenceCategory android:title="@string/category_input_settings">
|
||||
<!--com.limelight.preferences.SeekBarPreference
|
||||
android:key="seekbar_deadzone"
|
||||
android:defaultValue="15"
|
||||
@@ -49,6 +61,11 @@
|
||||
android:title="@string/title_checkbox_multi_controller"
|
||||
android:summary="@string/summary_checkbox_multi_controller"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_mouse_nav_buttons"
|
||||
android:title="@string/title_checkbox_mouse_nav_buttons"
|
||||
android:summary="@string/summary_checkbox_mouse_nav_buttons"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_usb_driver"
|
||||
android:title="@string/title_checkbox_xb1_driver"
|
||||
|
||||
@@ -55,7 +55,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Launch evdev_reader directly via SU
|
||||
try {
|
||||
su = Runtime.getRuntime().exec("su -c "+evdevReaderCmd);
|
||||
su = new ProcessBuilder("su", "-c", evdevReaderCmd).start();
|
||||
} catch (IOException e) {
|
||||
reportDeviceNotRooted();
|
||||
e.printStackTrace();
|
||||
@@ -151,7 +151,15 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
||||
break;
|
||||
|
||||
case EvdevEvent.BTN_SIDE:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_X1,
|
||||
event.value != 0);
|
||||
break;
|
||||
|
||||
case EvdevEvent.BTN_EXTRA:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_X2,
|
||||
event.value != 0);
|
||||
break;
|
||||
|
||||
case EvdevEvent.BTN_FORWARD:
|
||||
case EvdevEvent.BTN_BACK:
|
||||
case EvdevEvent.BTN_TASK:
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.0-beta04'
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule moonlight-common updated: 416105b12f...ee6fd70642
Reference in New Issue
Block a user