Compare commits

...

58 Commits

Author SHA1 Message Date
Cameron Gutman d1e135db4d Version 6.2 2019-02-06 22:10:29 -08:00
Cameron Gutman 61a17afe69 Fix *, @, #, and + keys on software keyboard 2019-02-06 21:40:28 -08:00
Cameron Gutman 47fd691884 Update to AGP 3.3.1 2019-02-06 21:14:50 -08:00
Cameron Gutman 0d171c6b28 Fix lock icon drawing on top of the loading spinner 2019-02-06 21:14:01 -08:00
Cameron Gutman f0c69d08b8 Add 480p option 2019-02-06 21:09:04 -08:00
Cameron Gutman 629bf5766d Fix a couple crash reports 2019-02-05 22:51:48 -08:00
Cameron Gutman 233bceeece Update common for GFE 3.17 2019-02-05 22:10:11 -08:00
Cameron Gutman 6660ea7d91 Update Xbox driver with Linux xpad.c and init quirks 2019-02-05 21:52:53 -08:00
Cameron Gutman 4864b2ca45 Add lock icon when PC is unpaired 2019-02-05 21:10:09 -08:00
Cameron Gutman 92097b318d Update Gradle and AGP 2019-02-05 20:58:49 -08:00
Cameron Gutman 997898c99d Version 6.1.3 2019-01-04 18:20:28 -08:00
Cameron Gutman 1174e03885 Fix incorrectly persisting host with missing server cert 2019-01-04 18:18:32 -08:00
Cameron Gutman ff0f54d541 Switch to using stun.moonlight-stream.org for STUN 2019-01-04 18:05:28 -08:00
Cameron Gutman 814964a100 Fix exception adding PCs 2019-01-01 23:32:16 -08:00
Cameron Gutman 7e154292a9 Stop suppressing exceptions 2019-01-01 23:31:38 -08:00
Cameron Gutman 0f9cba1053 Fix crash due to a null computer uuid 2019-01-01 22:34:27 -08:00
Cameron Gutman a4e134589d Version 6.1.1 2018-12-27 23:58:30 -08:00
Cameron Gutman cd80a94f28 Fix IllegalStateException caused by making HTTPS request without a pinned cert 2018-12-27 23:55:59 -08:00
Cameron Gutman 57c645a291 Change uuid field to String type due to new format UUIDs that fail to parse on GFE 3.16 2018-12-27 23:48:12 -08:00
Cameron Gutman 0cba200207 Version 6.1 2018-12-24 19:58:51 -08:00
Cameron Gutman 81582d7343 Revert "Hide the mouse cursor during pointer capture to work around DeX bug"
It doesn't actually fix the bug.

This reverts commit 16b845ab84.
2018-12-24 19:58:19 -08:00
Cameron Gutman 04e561fd54 Update common-c with bitrate fix 2018-12-24 19:56:42 -08:00
Cameron Gutman 5efbb5229d Fix up French translation 2018-12-24 19:09:16 -08:00
bubuleur 541e43eb18 Update Translation french Moonlight (#648)
* Update Translation french Moonlight

Hello
Herewith updated French language for your next Moonlight update
If you want you can contact me on igorlachaudarobaseaol.fr to update your application in French before an exit
cordially
Merci pour tous

* Update strings.xml
2018-12-24 19:07:38 -08:00
Cameron Gutman 7e679ff4c6 Fix short window where newly added PC could be incorrectly marked as unpaired 2018-12-23 21:34:20 -08:00
Cameron Gutman 486b4b4c4c Use a shared UID for all Moonlight clients 2018-12-22 21:03:42 -08:00
Cameron Gutman 7d76bf7868 Require cert pinning for HTTPS 2018-12-22 20:13:11 -08:00
Cameron Gutman db49077b9b Add cert pinning during pairing 2018-12-21 21:00:53 -08:00
Cameron Gutman 16b845ab84 Hide the mouse cursor during pointer capture to work around DeX bug 2018-12-19 15:06:46 +05:00
Cameron Gutman 5c175fecf6 Version 6.0.1 2018-12-05 21:29:12 -08:00
Cameron Gutman 773976b265 Update 49 FPS hack for MTK devices running Oreo which remains broken 2018-12-05 20:43:44 -08:00
Cameron Gutman 80070bbdbe Update readme to new URL 2018-12-03 21:42:47 -08:00
Cameron Gutman 4c8d433b6c Always use the new L+ releaseOutputBuffer() to gain drop support on L 2018-12-03 18:15:51 -08:00
Cameron Gutman 404f096d11 Only enable the FPS toggle on Lollipop or later 2018-12-03 18:15:03 -08:00
Cameron Gutman d2ac927cec Version 6.0 r2 2018-12-01 14:24:43 -08:00
Cameron Gutman 5e3d59d3d7 Move FPS unlock into basic settings 2018-12-01 14:23:51 -08:00
Cameron Gutman 9cd2ce1309 Add option to unlock FPS 2018-12-01 14:19:29 -08:00
Cameron Gutman 9ed49730d4 Fix 4K streaming resolution 2018-12-01 13:02:04 -08:00
Cameron Gutman 39ebb48f58 Remove old gamepad settings string 2018-11-30 21:33:22 -08:00
Cameron Gutman 1c29c70fba Version 6.0 2018-11-30 21:28:53 -08:00
Cameron Gutman 6993051529 Retransmit OSC gamepad packets every 100 ms to recover from dropped events in GFE 2018-11-30 21:17:12 -08:00
Cameron Gutman 4930087c4d Remove 63 Hz cap for > 60 FPS streams 2018-11-30 19:49:14 -08:00
Cameron Gutman 795f0a013b Create toggle for back and forward mouse support 2018-11-30 18:37:36 -08:00
Cameron Gutman 213414778e Rename multi-controller option 2018-11-30 18:23:15 -08:00
Cameron Gutman 7eac0ccaf8 Fix controller packet loss when zeroing analog sticks on OSC 2018-11-25 15:02:32 -08:00
Cameron Gutman 6adc9dcb2d Add support for 90/120 FPS streaming and 1440p 2018-11-23 18:41:43 -08:00
Cameron Gutman be620908f9 Update common with 4K check removal 2018-11-22 02:52:53 -08:00
Cameron Gutman e4edfdb043 Add missing apostrophe escape 2018-11-22 02:45:35 -08:00
bubuleur 3b5028d1a4 Update French (#639)
* Update French

* Update strings.xml
2018-11-22 02:43:35 -08:00
Cameron Gutman bc8c45bd59 Version 5.10.3 2018-11-21 21:23:12 -08:00
Cameron Gutman 63eb346a70 Use automatic remote streaming detection 2018-11-21 21:20:11 -08:00
Cameron Gutman 27ad691d23 Version 5.10.2 2018-11-15 22:13:44 -08:00
Cameron Gutman 747e920061 Update common for GFE 3.16 2018-11-15 22:11:32 -08:00
Cameron Gutman 8d09f56a0e Fix race condition causing loss of manual IP address after mDNS discovery 2018-11-13 23:16:25 -08:00
Cameron Gutman 113a0e2c45 Version 5.10.1 2018-10-30 20:26:32 -07:00
Cameron Gutman 977215a098 Fix crash when CMS dies and user returns to app view activity and taps a game 2018-10-30 20:21:11 -07:00
Cameron Gutman a7e65b47f9 Fix race condition on AppView activity startup 2018-10-30 17:52:46 -07:00
Cameron Gutman 7126055ad6 Fix crash on Lenovo Mirage Solo 2018-10-30 17:46:47 -07:00
44 changed files with 867 additions and 391 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Moonlight Android # Moonlight Android
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield. [Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device. We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device, Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
+2 -2
View File
@@ -8,8 +8,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 28 targetSdkVersion 28
versionName "5.10" versionName "6.2"
versionCode = 175 versionCode = 186
} }
flavorDimensions "root" flavorDimensions "root"
+17 -13
View File
@@ -1,8 +1,8 @@
package com.limelight; package com.limelight;
import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
import java.util.List; import java.util.List;
import java.util.UUID;
import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
@@ -28,7 +28,6 @@ import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
@@ -45,6 +44,8 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
import org.xmlpull.v1.XmlPullParserException;
public class AppView extends Activity implements AdapterFragmentCallbacks { public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter; private AppGridAdapter appGridAdapter;
private String uuidString; private String uuidString;
@@ -81,11 +82,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
// Wait for the binder to be ready // Wait for the binder to be ready
localBinder.waitForReady(); localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object // Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString)); computer = localBinder.getComputer(uuidString);
if (computer == null) { if (computer == null) {
finish(); finish();
return; return;
@@ -95,13 +93,18 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
appGridAdapter = new AppGridAdapter(AppView.this, appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode, PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode, PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId()); computer, localBinder.getUniqueId());
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
finish(); finish();
return; return;
} }
// Now make the binder visible. We must do this after appGridAdapter
// is set to prevent us from reaching updateUiWithServerinfo() and
// touching the appGridAdapter prior to initialization.
managerBinder = localBinder;
// Load the app grid with cached data (if possible). // Load the app grid with cached data (if possible).
// This must be done _before_ startComputerUpdates() // This must be done _before_ startComputerUpdates()
// so the initial serverinfo response can update the running // so the initial serverinfo response can update the running
@@ -154,7 +157,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
} }
// Don't care about other computers // Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) { if (!details.uuid.equalsIgnoreCase(uuidString)) {
return; return;
} }
@@ -178,7 +181,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override @Override
public void run() { public void run() {
// Disable shortcuts referencing this PC for now // Disable shortcuts referencing this PC for now
shortcutHelper.disableShortcut(details.uuid.toString(), shortcutHelper.disableShortcut(details.uuid,
getResources().getString(R.string.scut_not_paired)); getResources().getString(R.string.scut_not_paired));
// Display a toast to the user and quit the activity // Display a toast to the user and quit the activity
@@ -214,7 +217,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
blockingLoadSpinner.dismiss(); blockingLoadSpinner.dismiss();
blockingLoadSpinner = null; blockingLoadSpinner = null;
} }
} catch (Exception ignored) {} } catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
} }
}); });
@@ -278,7 +283,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist)); List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist); updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache"); LimeLog.info("Loaded applist from cache");
} catch (Exception e) { } catch (IOException | XmlPullParserException e) {
if (lastRawApplist != null) { if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist); LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace(); e.printStackTrace();
@@ -388,8 +393,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override @Override
public void run() { public void run() {
suspendGridUpdates = true; suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this, ServerHelper.doQuit(AppView.this, computer,
ServerHelper.getCurrentAddressFromComputer(computer),
app.app, managerBinder, new Runnable() { app.app, managerBinder, new Runnable() {
@Override @Override
public void run() { public void run() {
+78 -53
View File
@@ -68,6 +68,11 @@ import android.widget.FrameLayout;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.Toast; 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, public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener, OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
@@ -105,8 +110,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean grabbedInput = true; private boolean grabbedInput = true;
private boolean grabComboDown = false; private boolean grabComboDown = false;
private StreamView streamView; private StreamView streamView;
private boolean gotBackPointerEvent = false;
private boolean syntheticBackDown = false;
private ShortcutHelper shortcutHelper; private ShortcutHelper shortcutHelper;
@@ -134,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_NAME = "AppName";
public static final String EXTRA_APP_ID = "AppId"; public static final String EXTRA_APP_ID = "AppId";
public static final String EXTRA_UNIQUEID = "UniqueId"; 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_UUID = "UUID";
public static final String EXTRA_PC_NAME = "PcName"; public static final String EXTRA_PC_NAME = "PcName";
public static final String EXTRA_APP_HDR = "HDR"; public static final String EXTRA_APP_HDR = "HDR";
public static final String EXTRA_SERVER_CERT = "ServerCert";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -228,10 +231,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME); String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID); int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID); 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 uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME); String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false); 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) { if (appId == StreamConfiguration.INVALID_APP_ID) {
finish(); finish();
@@ -348,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 // Hopefully, we can get rid of this once someone comes up with a better way
// to track the state of the pipeline and time frames. // to track the state of the pipeline and time frames.
int roundedRefreshRate = Math.round(displayRefreshRate); int roundedRefreshRate = Math.round(displayRefreshRate);
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) { if ((!prefConfig.disableFrameDrop || prefConfig.unlockFps) && prefConfig.fps >= roundedRefreshRate) {
if (roundedRefreshRate <= 49) { 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 // Let's avoid clearly bogus refresh rates and fall back to legacy rendering
decoderRenderer.enableLegacyFrameDropRendering(); decoderRenderer.enableLegacyFrameDropRendering();
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate); LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
@@ -372,8 +390,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.setBitrate(prefConfig.bitrate) .setBitrate(prefConfig.bitrate)
.setEnableSops(prefConfig.enableSops) .setEnableSops(prefConfig.enableSops)
.enableLocalAudioPlayback(prefConfig.playHostAudio) .enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292) .setMaxPacketSize(1392)
.setRemote(remote) .setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO)
.setHevcBitratePercentageMultiplier(75) .setHevcBitratePercentageMultiplier(75)
.setHevcSupported(decoderRenderer.isHevcSupported()) .setHevcSupported(decoderRenderer.isHevcSupported())
.setEnableHdr(willStreamHdr) .setEnableHdr(willStreamHdr)
@@ -385,7 +403,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.build(); .build();
// Initialize the connection // 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); controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE); InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
@@ -410,6 +428,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
(FrameLayout)streamView.getParent(), (FrameLayout)streamView.getParent(),
this); this);
virtualController.refreshLayout(); virtualController.refreshLayout();
virtualController.show();
} }
if (prefConfig.usbDriver) { if (prefConfig.usbDriver) {
@@ -501,8 +520,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display.Mode bestMode = display.getMode(); Display.Mode bestMode = display.getMode();
for (Display.Mode candidate : display.getSupportedModes()) { for (Display.Mode candidate : display.getSupportedModes()) {
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() && boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate();
candidate.getRefreshRate() < 63;
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() && boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() && candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
candidate.getPhysicalWidth() <= 4096; candidate.getPhysicalWidth() <= 4096;
@@ -518,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 // Make sure the refresh rate doesn't regress
if (!refreshRateOk) { if (!refreshRateOk) {
continue; continue;
@@ -535,12 +560,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId(); windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
displayRefreshRate = bestMode.getRefreshRate(); 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) { else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
float bestRefreshRate = display.getRefreshRate(); float bestRefreshRate = display.getRefreshRate();
for (float candidate : display.getSupportedRefreshRates()) { for (float candidate : display.getSupportedRefreshRates()) {
if (candidate > bestRefreshRate && candidate < 63) { if (candidate > bestRefreshRate) {
LimeLog.info("Examining refresh rate: "+candidate); 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; bestRefreshRate = candidate;
} }
} }
@@ -682,6 +715,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
if (virtualController != null) {
virtualController.hide();
}
if (conn != null) { if (conn != null) {
int videoFormat = decoderRenderer.getActiveVideoFormat(); int videoFormat = decoderRenderer.getActiveVideoFormat();
@@ -838,16 +875,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Handle a synthetic back button event that some Android OS versions // Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click. This event WILL repeat if // create as a result of a right-click. This event WILL repeat if
// the right mouse button is held down, so we ignore those. // the right mouse button is held down, so we ignore those.
if ((event.getSource() == InputDevice.SOURCE_MOUSE || if (!prefConfig.mouseNavButtons &&
(event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) && event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK) { event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
// It appears this may get turned into a right-click pointer event conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
// if we don't return true to indicate that we handled it on
// some devices. https://github.com/moonlight-stream/moonlight-android/issues/634
if (event.getRepeatCount() == 0 && !gotBackPointerEvent) {
syntheticBackDown = true;
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
return true; return true;
} }
@@ -876,7 +908,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return false; return false;
} }
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState(event)); byte modifiers = getModifierState(event);
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
conn.sendKeyboardInput((short) 0x8010, KeyboardPacket.KEY_DOWN, modifiers);
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, modifiers);
} }
return true; return true;
@@ -896,19 +933,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Handle a synthetic back button event that some Android OS versions // Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click. // create as a result of a right-click.
if ((event.getSource() == InputDevice.SOURCE_MOUSE || if (!prefConfig.mouseNavButtons &&
(event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) && event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK) { event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
// It appears this may get turned into a right-click pointer event conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
// if we don't return true to indicate that we handled it on
// some devices. https://github.com/moonlight-stream/moonlight-android/issues/634
if (!gotBackPointerEvent || syntheticBackDown) {
// We need to raise the button if gotBackPointerEvent is true
// in the case where it transitioned to true after we already
// sent the right click down event.
syntheticBackDown = false;
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
return true; return true;
} }
@@ -935,7 +964,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return false; return false;
} }
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, getModifierState(event)); byte modifiers = getModifierState(event);
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, modifiers);
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
conn.sendKeyboardInput((short) 0x8010, KeyboardPacket.KEY_UP, getModifierState(event));
}
} }
return true; return true;
@@ -1015,11 +1051,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
else { else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT); conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
} }
// Don't use the KEYCODE_BACK hack (which interferes with mice
// with actual back buttons) since we're getting right clicks
// using this callback.
gotBackPointerEvent = true;
} }
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) { if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
@@ -1031,9 +1062,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
} }
// HACK: Disable mouse back button press on Xiaomi due to reported if (prefConfig.mouseNavButtons) {
// issues with right clicks triggering it.
if (!("Xiaomi".equalsIgnoreCase(Build.MANUFACTURER))) {
if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) { if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) { if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1); conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1);
@@ -1041,19 +1070,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
else { else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X1); conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X1);
} }
// Don't use the KEYCODE_BACK hack. That will cause this
// button press to trigger a right-click.
gotBackPointerEvent = true;
} }
}
if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) { if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) { if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2); conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
} }
else { else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2); conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
}
} }
} }
+26 -17
View File
@@ -53,6 +53,8 @@ import android.widget.RelativeLayout;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
import org.xmlpull.v1.XmlPullParserException;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10; import javax.microedition.khronos.opengles.GL10;
@@ -346,7 +348,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
private void doPair(final ComputerDetails computer) { private void doPair(final ComputerDetails computer) {
if (computer.state == ComputerDetails.State.OFFLINE) { if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return; return;
} }
@@ -372,9 +375,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(), managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(), computer.serverCert,
PlatformBinding.getCryptoProvider(PcView.this)); PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { if (httpConn.getPairState() == PairState.PAIRED) {
// Don't display any toast, but open the app list // Don't display any toast, but open the app list
message = null; message = null;
success = true; success = true;
@@ -386,21 +389,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title), Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false); getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr); PairingManager pm = httpConn.getPairingManager();
if (pairState == PairingManager.PairState.PIN_WRONG) {
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
if (pairState == PairState.PIN_WRONG) {
message = getResources().getString(R.string.pair_incorrect_pin); message = getResources().getString(R.string.pair_incorrect_pin);
} }
else if (pairState == PairingManager.PairState.FAILED) { else if (pairState == PairState.FAILED) {
message = getResources().getString(R.string.pair_fail); message = getResources().getString(R.string.pair_fail);
} }
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) { else if (pairState == PairState.ALREADY_IN_PROGRESS) {
message = getResources().getString(R.string.pair_already_in_progress); message = getResources().getString(R.string.pair_already_in_progress);
} }
else if (pairState == PairingManager.PairState.PAIRED) { else if (pairState == PairState.PAIRED) {
// Just navigate to the app view without displaying a toast // Just navigate to the app view without displaying a toast
message = null; message = null;
success = true; success = true;
// Pin this certificate for later HTTPS use
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
// Invalidate reachability information after pairing to force // Invalidate reachability information after pairing to force
// a refresh before reading pair state again // a refresh before reading pair state again
managerBinder.invalidateStateForComputer(computer.uuid); managerBinder.invalidateStateForComputer(computer.uuid);
@@ -414,7 +422,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
message = getResources().getString(R.string.error_unknown_host); message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (XmlPullParserException | IOException e) {
e.printStackTrace(); e.printStackTrace();
message = e.getMessage(); message = e.getMessage();
} }
@@ -478,7 +486,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
private void doUnpair(final ComputerDetails computer) { private void doUnpair(final ComputerDetails computer) {
if (computer.state == ComputerDetails.State.OFFLINE) { if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return; return;
} }
@@ -496,7 +505,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
try { try {
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(), managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(), computer.serverCert,
PlatformBinding.getCryptoProvider(PcView.this)); PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair(); httpConn.unpair();
@@ -514,8 +523,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
message = getResources().getString(R.string.error_unknown_host); message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (XmlPullParserException | IOException e) {
message = e.getMessage(); message = e.getMessage();
e.printStackTrace();
} }
final String toastMessage = message; final String toastMessage = message;
@@ -541,7 +551,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Intent i = new Intent(this, AppView.class); Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name); i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString()); i.putExtra(AppView.UUID_EXTRA, computer.uuid);
startActivity(i); startActivity(i);
} }
@@ -603,8 +613,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
UiHelper.displayQuitConfirmationDialog(this, new Runnable() { UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override @Override
public void run() { public void run() {
ServerHelper.doQuit(PcView.this, ServerHelper.doQuit(PcView.this, computer.details,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0, false), managerBinder, null); new NvApp("app", 0, false), managerBinder, null);
} }
}, null); }, null);
@@ -625,7 +634,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (details.equals(computer.details)) { if (details.equals(computer.details)) {
// Disable or delete shortcuts referencing this PC // Disable or delete shortcuts referencing this PC
shortcutHelper.disableShortcut(details.uuid.toString(), shortcutHelper.disableShortcut(details.uuid,
getResources().getString(R.string.scut_deleted_pc)); getResources().getString(R.string.scut_deleted_pc));
pcGridAdapter.removeComputer(computer); pcGridAdapter.removeComputer(computer);
@@ -656,7 +665,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Add a launcher shortcut for this PC // Add a launcher shortcut for this PC
if (details.pairState == PairState.PAIRED) { if (details.pairState == PairState.PAIRED) {
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false); shortcutHelper.createAppViewShortcut(details.uuid, details, false);
} }
if (existingEntry != null) { if (existingEntry != null) {
@@ -48,7 +48,7 @@ public class ShortcutTrampoline extends Activity {
managerBinder = localBinder; managerBinder = localBinder;
// Get the computer object // Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString)); computer = managerBinder.getComputer(uuidString);
if (computer == null) { if (computer == null) {
Dialog.displayDialog(ShortcutTrampoline.this, Dialog.displayDialog(ShortcutTrampoline.this,
@@ -77,7 +77,7 @@ public class ShortcutTrampoline extends Activity {
@Override @Override
public void notifyComputerUpdated(final ComputerDetails details) { public void notifyComputerUpdated(final ComputerDetails details) {
// Don't care about other computers // Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) { if (!details.uuid.equalsIgnoreCase(uuidString)) {
return; return;
} }
@@ -200,6 +200,14 @@ public class ShortcutTrampoline extends Activity {
protected boolean validateInput() { protected boolean validateInput() {
// Validate UUID // Validate UUID
if (uuidString == null) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_uuid),
true);
return false;
}
try { try {
UUID.fromString(uuidString); UUID.fromString(uuidString);
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
@@ -12,7 +12,6 @@ import java.security.KeyFactory;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider; import java.security.Provider;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
@@ -155,7 +154,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
} catch (Exception e) { } catch (Exception e) {
// Nothing should go wrong here // Nothing should go wrong here
e.printStackTrace(); e.printStackTrace();
return false; throw new RuntimeException(e);
} }
LimeLog.info("Generated a new key pair"); LimeLog.info("Generated a new key pair");
@@ -47,7 +47,21 @@ public class KeyboardTranslator {
public static final int VK_BACK_QUOTE = 192; public static final int VK_BACK_QUOTE = 192;
public static final int VK_QUOTE = 222; public static final int VK_QUOTE = 222;
public static final int VK_PAUSE = 19; public static final int VK_PAUSE = 19;
public static boolean needsShift(int keycode) {
switch (keycode)
{
case KeyEvent.KEYCODE_AT:
case KeyEvent.KEYCODE_POUND:
case KeyEvent.KEYCODE_PLUS:
case KeyEvent.KEYCODE_STAR:
return true;
default:
return false;
}
}
/** /**
* Translates the given keycode and returns the GFE keycode * Translates the given keycode and returns the GFE keycode
* @param keycode the code to be translated * @param keycode the code to be translated
@@ -116,7 +130,8 @@ public class KeyboardTranslator {
case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_ENTER:
translated = 0x0d; translated = 0x0d;
break; break;
case KeyEvent.KEYCODE_PLUS:
case KeyEvent.KEYCODE_EQUALS: case KeyEvent.KEYCODE_EQUALS:
translated = 0xbb; translated = 0xbb;
break; break;
@@ -257,7 +272,19 @@ public class KeyboardTranslator {
case KeyEvent.KEYCODE_NUMPAD_DOT: case KeyEvent.KEYCODE_NUMPAD_DOT:
translated = 0x6E; translated = 0x6E;
break; break;
case KeyEvent.KEYCODE_AT:
translated = 2 + VK_0;
break;
case KeyEvent.KEYCODE_POUND:
translated = 3 + VK_0;
break;
case KeyEvent.KEYCODE_STAR:
translated = 8 + VK_0;
break;
default: default:
System.out.println("No key for "+keycode); System.out.println("No key for "+keycode);
return 0; return 0;
@@ -14,6 +14,7 @@ public class Xbox360Controller extends AbstractXboxController {
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
private static final int[] SUPPORTED_VENDORS = { private static final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster 0x044f, // Thrustmaster
0x045e, // Microsoft 0x045e, // Microsoft
0x046d, // Logitech 0x046d, // Logitech
@@ -23,6 +24,7 @@ public class Xbox360Controller extends AbstractXboxController {
0x07ff, // Mad Catz 0x07ff, // Mad Catz
0x0e6f, // Unknown 0x0e6f, // Unknown
0x0f0d, // Hori 0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon 0x11c9, // Nacon
0x12ab, // Unknown 0x12ab, // Unknown
0x1430, // RedOctane 0x1430, // RedOctane
@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
import com.limelight.nvstream.input.ControllerPacket; import com.limelight.nvstream.input.ControllerPacket;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays;
public class XboxOneController extends AbstractXboxController { public class XboxOneController extends AbstractXboxController {
@@ -23,8 +24,30 @@ public class XboxOneController extends AbstractXboxController {
0x24c6, // PowerA 0x24c6, // PowerA
}; };
// FIXME: odata_serial private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00}; private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
0x00, 0x00, 0x00, (byte)0x80, 0x00};
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
private static final byte[] PDP_INIT2 = {0x06, 0x20, 0x00, 0x02, 0x01, 0x00};
private static final byte[] RUMBLE_INIT1 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
0x1D, 0x1D, (byte)0xFF, 0x00, 0x00};
private static final byte[] RUMBLE_INIT2 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00};
private static InitPacket[] INIT_PKTS = {
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
new InitPacket(0x0000, 0x0000, FW2015_INIT),
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT1),
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT1),
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT2),
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT2),
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT2),
};
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) { public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(device, connection, deviceId, listener); super(device, connection, deviceId, listener);
@@ -111,13 +134,43 @@ public class XboxOneController extends AbstractXboxController {
@Override @Override
protected boolean doInit() { protected boolean doInit() {
// Send the initialization packet byte seqNum = 0;
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
if (res != XB1_INIT_DATA.length) { // Send all applicable init packets
LimeLog.warning("Initialization transfer failed: "+res); for (InitPacket pkt : INIT_PKTS) {
return false; if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
continue;
}
if (pkt.productId != 0 && device.getProductId() != pkt.productId) {
continue;
}
byte[] data = Arrays.copyOf(pkt.data, pkt.data.length);
// Populate sequence number
data[2] = seqNum++;
// Send the initialization packet
int res = connection.bulkTransfer(outEndpt, data, data.length, 3000);
if (res != data.length) {
LimeLog.warning("Initialization transfer failed: "+res);
return false;
}
} }
return true; return true;
} }
private static class InitPacket {
final int vendorId;
final int productId;
final byte[] data;
InitPacket(int vendorId, int productId, byte[] data) {
this.vendorId = vendorId;
this.productId = productId;
this.data = data;
}
}
} }
@@ -338,6 +338,7 @@ public class AnalogStick extends VirtualControllerElement {
} else { } else {
stick_state = STICK_STATE.NO_MOVEMENT; stick_state = STICK_STATE.NO_MOVEMENT;
notifyOnRevoke(); notifyOnRevoke();
// not longer pressed reset analog stick // not longer pressed reset analog stick
notifyOnMovement(0, 0); notifyOnMovement(0, 0);
} }
@@ -17,6 +17,8 @@ import com.limelight.nvstream.NvConnection;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
public class VirtualController { public class VirtualController {
public class ControllerInputContext { public class ControllerInputContext {
@@ -42,6 +44,8 @@ public class VirtualController {
private FrameLayout frame_layout = null; private FrameLayout frame_layout = null;
private RelativeLayout relative_layout = null; private RelativeLayout relative_layout = null;
private Timer retransmitTimer;
ControllerMode currentMode = ControllerMode.Active; ControllerMode currentMode = ControllerMode.Active;
ControllerInputContext inputContext = new ControllerInputContext(); ControllerInputContext inputContext = new ControllerInputContext();
@@ -88,11 +92,24 @@ public class VirtualController {
} }
public void hide() { public void hide() {
retransmitTimer.cancel();
relative_layout.setVisibility(View.INVISIBLE); relative_layout.setVisibility(View.INVISIBLE);
} }
public void show() { public void show() {
relative_layout.setVisibility(View.VISIBLE); relative_layout.setVisibility(View.VISIBLE);
// HACK: GFE sometimes discards gamepad packets when they are received
// very shortly after another. This can be critical if an axis zeroing packet
// is lost and causes an analog stick to get stuck. To avoid this, we send
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
retransmitTimer = new Timer("OSC timer", true);
retransmitTimer.schedule(new TimerTask() {
@Override
public void run() {
sendControllerInputContext();
}
}, 100, 100);
} }
public void removeElements() { public void removeElements() {
@@ -149,32 +166,23 @@ public class VirtualController {
return inputContext; return inputContext;
} }
public void sendControllerInputContext() { void sendControllerInputContext() {
sendControllerInputPacket(); _DBG("INPUT_MAP + " + inputContext.inputMap);
} _DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
private void sendControllerInputPacket() { if (connection != null) {
try { connection.sendControllerInput(
_DBG("INPUT_MAP + " + inputContext.inputMap); inputContext.inputMap,
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger); inputContext.leftTrigger,
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger); inputContext.rightTrigger,
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY); inputContext.leftStickX,
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY); inputContext.leftStickY,
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY); inputContext.rightStickX,
inputContext.rightStickY
if (connection != null) { );
connection.sendControllerInput(
inputContext.inputMap,
inputContext.leftTrigger,
inputContext.rightTrigger,
inputContext.leftStickX,
inputContext.leftStickY,
inputContext.rightStickX,
inputContext.rightStickY
);
}
} catch (Exception e) {
e.printStackTrace();
} }
} }
} }
@@ -166,58 +166,54 @@ public abstract class VirtualControllerElement extends View {
} }
protected void showConfigurationDialog() { protected void showConfigurationDialog() {
try { AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
alertBuilder.setTitle("Configuration"); alertBuilder.setTitle("Configuration");
CharSequence functions[] = new CharSequence[]{ CharSequence functions[] = new CharSequence[]{
"Move", "Move",
"Resize", "Resize",
/*election /*election
"Set n "Set n
Disable color sormal color", Disable color sormal color",
"Set pressed color", "Set pressed color",
*/
"Cancel"
};
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0: { // move
actionEnableMove();
break;
}
case 1: { // resize
actionEnableResize();
break;
}
/*
case 2: { // set default color
actionShowNormalColorChooser();
break;
}
case 3: { // set pressed color
actionShowPressedColorChooser();
break;
}
*/ */
"Cancel" default: { // cancel
}; actionCancel();
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0: { // move
actionEnableMove();
break;
}
case 1: { // resize
actionEnableResize();
break;
}
/*
case 2: { // set default color
actionShowNormalColorChooser();
break; break;
} }
case 3: { // set pressed color
actionShowPressedColorChooser();
break;
}
*/
default: { // cancel
actionCancel();
break;
}
}
} }
}); }
AlertDialog alert = alertBuilder.create(); });
// show menu AlertDialog alert = alertBuilder.create();
alert.show(); // show menu
} catch (Exception e) { alert.show();
e.printStackTrace();
}
} }
@Override @Override
@@ -405,10 +405,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
// Render the last buffer // Render the last buffer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !legacyFrameDropRendering) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Use a PTS that will cause this frame to never be dropped if frame dropping if (legacyFrameDropRendering) {
// is disabled // Use a PTS that will cause this frame to be dropped if another comes in within
videoDecoder.releaseOutputBuffer(lastIndex, 0); // 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 { else {
videoDecoder.releaseOutputBuffer(lastIndex, true); videoDecoder.releaseOutputBuffer(lastIndex, true);
@@ -160,8 +160,8 @@ public class MediaCodecHelper {
// We see a bunch of crashes on MediaTek Android TVs running // We see a bunch of crashes on MediaTek Android TVs running
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for // at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
// these devices and hope they fix it in Oreo. // these devices and hope they fix it in Pie.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
blacklisted49FpsDecoderPrefixes.add("omx.mtk"); blacklisted49FpsDecoderPrefixes.add("omx.mtk");
} }
} }
@@ -1,11 +1,14 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.UUID;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import android.content.ContentValues; import android.content.ContentValues;
@@ -23,6 +26,7 @@ public class ComputerDatabaseManager {
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress"; private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress"; private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress"; private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
private SQLiteDatabase computerDb; private SQLiteDatabase computerDb;
@@ -43,13 +47,21 @@ public class ComputerDatabaseManager {
} }
private void initializeDb(Context c) { private void initializeDb(Context c) {
// Add cert column to the table if not present
try {
computerDb.execSQL(String.format((Locale)null,
"ALTER TABLE %s ADD COLUMN %s TEXT",
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
} catch (SQLiteException e) {}
// Create tables if they aren't already there // Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, 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)", "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
COMPUTER_TABLE_NAME, COMPUTER_TABLE_NAME,
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME, LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
MAC_ADDRESS_COLUMN_NAME)); MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
// Move all computers from the old DB (if any) to the new one // Move all computers from the old DB (if any) to the new one
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c); List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
@@ -64,32 +76,47 @@ public class ComputerDatabaseManager {
public boolean updateComputer(ComputerDetails details) { public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString()); values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
values.put(COMPUTER_NAME_COLUMN_NAME, details.name); values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress); values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress); values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress); values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress); values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
try {
if (details.serverCert != null) {
values.put(SERVER_CERT_COLUMN_NAME, details.serverCert.getEncoded());
}
else {
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
}
} catch (CertificateEncodingException e) {
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
e.printStackTrace();
}
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
private ComputerDetails getComputerFromCursor(Cursor c) { private ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
String uuidStr = c.getString(0); details.uuid = c.getString(0);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
details.name = c.getString(1); details.name = c.getString(1);
details.localAddress = c.getString(2); details.localAddress = c.getString(2);
details.remoteAddress = c.getString(3); details.remoteAddress = c.getString(3);
details.manualAddress = c.getString(4); details.manualAddress = c.getString(4);
details.macAddress = c.getString(5); details.macAddress = c.getString(5);
try {
byte[] derCertData = c.getBlob(6);
if (derCertData != null) {
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(derCertData));
}
} catch (CertificateException e) {
e.printStackTrace();
}
// This signifies we don't have dynamic state (like pair state) // This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN; details.state = ComputerDetails.State.UNKNOWN;
@@ -115,8 +142,8 @@ public class ComputerDatabaseManager {
return computerList; return computerList;
} }
public ComputerDetails getComputerByUUID(UUID uuid) { public ComputerDetails getComputerByUUID(String uuid) {
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid.toString() }, null, null, null); Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
if (!c.moveToFirst()) { if (!c.moveToFirst()) {
// No matching computer // No matching computer
c.close(); c.close();
@@ -8,7 +8,6 @@ import java.net.Socket;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog; import com.limelight.LimeLog;
@@ -18,6 +17,7 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper; import com.limelight.utils.CacheHelper;
@@ -105,17 +105,27 @@ public class ComputerManagerService extends Service {
// If it's online, update our persistent state // If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) { if (details.state == ComputerDetails.State.ONLINE) {
if (!newPc) { ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
// Check if it's in the database because it could have been
// removed after this was issued // Check if it's in the database because it could have been
if (dbManager.getComputerByUUID(details.uuid) == null) { // removed after this was issued
// It's gone if (!newPc && existingComputer == null) {
releaseLocalDatabaseReference(); // It's gone
return false; releaseLocalDatabaseReference();
} return false;
} }
dbManager.updateComputer(details); // If we already have an entry for this computer in the DB, we must
// combine the existing data with this new data (which may be partially available
// due to detecting the PC via mDNS) without the saved external address. If we
// write to the DB without doing this first, we can overwrite our existing data.
if (existingComputer != null) {
existingComputer.update(details);
dbManager.updateComputer(existingComputer);
}
else {
dbManager.updateComputer(details);
}
} }
// Don't call the listener if this is a failed lookup of a new PC // Don't call the listener if this is a failed lookup of a new PC
@@ -231,7 +241,7 @@ public class ComputerManagerService extends Service {
return idManager.getUniqueId(); return idManager.getUniqueId();
} }
public ComputerDetails getComputer(UUID uuid) { public ComputerDetails getComputer(String uuid) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) { if (uuid.equals(tuple.computer.uuid)) {
@@ -243,7 +253,7 @@ public class ComputerManagerService extends Service {
return null; return null;
} }
public void invalidateStateForComputer(UUID uuid) { public void invalidateStateForComputer(String uuid) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) { if (uuid.equals(tuple.computer.uuid)) {
@@ -304,7 +314,7 @@ public class ComputerManagerService extends Service {
}; };
} }
private void addTuple(ComputerDetails details, boolean manuallyAdded) { private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer // Check if this is the same computer
@@ -350,12 +360,28 @@ public class ComputerManagerService extends Service {
// Since we're on the same network, we can use STUN to find // Since we're on the same network, we can use STUN to find
// our WAN address, which is also very likely the WAN address // our WAN address, which is also very likely the WAN address
// of the PC. We can use this later to connect remotely. // of the PC. We can use this later to connect remotely.
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns(); fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
} }
// Block while we try to fill the details // Block while we try to fill the details
try { try {
runPoll(fakeDetails, true, 0); // We cannot use runPoll() here because it will attempt to persist the state of the machine
// in the database, which would be bad because we don't have our pinned cert loaded yet.
if (pollComputer(fakeDetails)) {
// See if we have record of this PC to pull its pinned cert
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (tuple.computer.uuid.equals(fakeDetails.uuid)) {
fakeDetails.serverCert = tuple.computer.serverCert;
break;
}
}
}
// Poll again, possibly with the pinned cert, to get accurate pairing information.
// This will insert the host into the database too.
runPoll(fakeDetails, true, 0);
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
return false; return false;
} }
@@ -365,7 +391,7 @@ public class ComputerManagerService extends Service {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid); LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine // Start a polling thread for this machine
addTuple(fakeDetails, manuallyAdded); addTuple(fakeDetails);
return true; return true;
} }
else { else {
@@ -425,14 +451,18 @@ public class ComputerManagerService extends Service {
} }
try { try {
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails(); ComputerDetails newDetails = http.getComputerDetails();
// Check if this is the PC we expected // Check if this is the PC we expected
if (details.uuid != null && newDetails.uuid != null && if (newDetails.uuid == null) {
!details.uuid.equals(newDetails.uuid)) { LimeLog.severe("Polling returned no UUID!");
return null;
}
// details.uuid can be null on initial PC add
else if (details.uuid != null && !details.uuid.equals(newDetails.uuid)) {
// We got the wrong PC! // We got the wrong PC!
LimeLog.info("Polling returned the wrong PC!"); LimeLog.info("Polling returned the wrong PC!");
return null; return null;
@@ -442,7 +472,8 @@ public class ComputerManagerService extends Service {
newDetails.activeAddress = address; newDetails.activeAddress = address;
return newDetails; return newDetails;
} catch (Exception e) { } catch (XmlPullParserException | IOException e) {
e.printStackTrace();
return null; return null;
} }
} }
@@ -537,7 +568,7 @@ public class ComputerManagerService extends Service {
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC // 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 +")"); LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")");
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress); String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
LimeLog.info("Fast poll for "+details.name+" returned active address: "+details.activeAddress); 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 no connection could be established to either IP address, there's nothing we can do
if (candidateAddress == null) { if (candidateAddress == null) {
@@ -593,7 +624,7 @@ public class ComputerManagerService extends Service {
for (ComputerDetails computer : dbManager.getAllComputers()) { for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add tuples for each computer // Add tuples for each computer
addTuple(computer, true); addTuple(computer);
} }
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
@@ -671,8 +702,9 @@ public class ComputerManagerService extends Service {
public void run() { public void run() {
int emptyAppListResponses = 0; int emptyAppListResponses = 0;
do { do {
// Can't poll if it's not online // Can't poll if it's not online or paired
if (computer.state != ComputerDetails.State.ONLINE) { if (computer.state != ComputerDetails.State.ONLINE ||
computer.pairState != PairingManager.PairState.PAIRED) {
if (listener != null) { if (listener != null) {
listener.notifyComputerUpdated(computer); listener.notifyComputerUpdated(computer);
} }
@@ -688,7 +720,7 @@ public class ComputerManagerService extends Service {
try { try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(), NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
String appList; String appList;
if (tuple != null) { if (tuple != null) {
@@ -712,12 +744,12 @@ public class ComputerManagerService extends Service {
// in a row, we'll go ahead and believe it. // in a row, we'll go ahead and believe it.
emptyAppListResponses++; emptyAppListResponses++;
} }
if (appList != null && !appList.isEmpty() && if (!appList.isEmpty() &&
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) { (!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
// Open the cache file // Open the cache file
OutputStream cacheOut = null; OutputStream cacheOut = null;
try { try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString()); cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
CacheHelper.writeStringToOutputStream(cacheOut, appList); CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
@@ -744,7 +776,7 @@ public class ComputerManagerService extends Service {
listener.notifyComputerUpdated(computer); listener.notifyComputerUpdated(computer);
} }
} }
else if (appList == null || appList.isEmpty()) { else if (appList.isEmpty()) {
LimeLog.warning("Null app list received from "+computer.uuid); LimeLog.warning("Null app list received from "+computer.uuid);
} }
} catch (IOException e) { } catch (IOException e) {
@@ -12,7 +12,6 @@ import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID;
public class LegacyDatabaseReader { public class LegacyDatabaseReader {
private static final String COMPUTER_DB_NAME = "computers.db"; private static final String COMPUTER_DB_NAME = "computers.db";
@@ -24,14 +23,7 @@ public class LegacyDatabaseReader {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
details.name = c.getString(0); details.name = c.getString(0);
details.uuid = c.getString(1);
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 // An earlier schema defined addresses as byte blobs. We'll
// gracefully migrate those to strings so we can store DNS names // gracefully migrate those to strings so we can store DNS names
@@ -9,6 +9,7 @@ import android.widget.TextView;
import com.limelight.PcView; import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.PairingManager;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@@ -77,6 +78,14 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
overlayView.setAlpha(0.4f); overlayView.setAlpha(0.4f);
return true; return true;
} }
// We must check if the status is exactly online and unpaired
// to avoid colliding with the loading spinner when status is unknown
else if (obj.details.state == ComputerDetails.State.ONLINE &&
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
overlayView.setImageResource(R.drawable.ic_lock);
overlayView.setAlpha(1.0f);
return true;
}
return false; return false;
} }
} }
@@ -39,7 +39,7 @@ public class DiskAssetLoader {
} }
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) { public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
} }
// https://developer.android.com/topic/performance/graphics/load-bitmap.html // https://developer.android.com/topic/performance/graphics/load-bitmap.html
@@ -65,7 +65,7 @@ public class DiskAssetLoader {
} }
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) { public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
// Don't bother with anything if it doesn't exist // Don't bother with anything if it doesn't exist
if (!file.exists()) { if (!file.exists()) {
@@ -137,7 +137,7 @@ public class DiskAssetLoader {
OutputStream out = null; OutputStream out = null;
boolean success = false; boolean success = false;
try { try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE); CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
success = true; success = true;
} catch (IOException e) { } catch (IOException e) {
@@ -151,7 +151,7 @@ public class DiskAssetLoader {
if (!success) { if (!success) {
LimeLog.warning("Unable to populate cache with tuple: "+tuple); LimeLog.warning("Unable to populate cache with tuple: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
} }
} }
} }
@@ -16,7 +16,7 @@ public class MemoryAssetLoader {
}; };
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) { private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId(); return tuple.computer.uuid+"-"+tuple.app.getAppId();
} }
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) { public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
@@ -22,7 +22,8 @@ public class NetworkAssetLoader {
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) { public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
InputStream in = null; InputStream in = null;
try { 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); in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {} } catch (IOException ignored) {}
@@ -91,14 +91,20 @@ public class AddComputerManually extends Activity {
} }
private void doAddPc(String host) { private void doAddPc(String host) {
String msg;
boolean wrongSiteLocal = false; boolean wrongSiteLocal = false;
boolean success; boolean success;
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc), SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false); getResources().getString(R.string.msg_add_pc), false);
success = managerBinder.addComputerBlocking(host, true); try {
success = managerBinder.addComputerBlocking(host, true);
} catch (IllegalArgumentException e) {
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
e.printStackTrace();
success = false;
}
if (!success){ if (!success){
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host); wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
} }
@@ -3,7 +3,6 @@ package com.limelight.preferences;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.preference.DialogPreference; import android.preference.DialogPreference;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -7,7 +7,11 @@ import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
public class PreferenceConfiguration { public class PreferenceConfiguration {
static final String RES_FPS_PREF_STRING = "list_resolution_fps"; private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
static final String RESOLUTION_PREF_STRING = "list_resolution";
static final String FPS_PREF_STRING = "list_fps";
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps"; static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate"; private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video"; private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
@@ -29,18 +33,11 @@ public class PreferenceConfiguration {
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip"; 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 BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation"; 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; static final String DEFAULT_RESOLUTION = "720p";
private static final int BITRATE_DEFAULT_360_60 = 2000; static final String DEFAULT_FPS = "60";
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;
private static final boolean DEFAULT_STRETCH = false; private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true; private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false; private static final boolean DEFAULT_DISABLE_TOASTS = false;
@@ -54,12 +51,13 @@ public class PreferenceConfiguration {
private static final String DEFAULT_VIDEO_FORMAT = "auto"; private static final String DEFAULT_VIDEO_FORMAT = "auto";
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false; private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
private static final boolean ONLY_L3_R3_DEFAULT = false; private static final boolean 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_DISABLE_FRAME_DROP = false;
private static final boolean DEFAULT_ENABLE_HDR = false; private static final boolean DEFAULT_ENABLE_HDR = false;
private static final boolean DEFAULT_ENABLE_PIP = false; private static final boolean DEFAULT_ENABLE_PIP = false;
private static final boolean DEFAULT_BIND_ALL_USB = false; private static final boolean DEFAULT_BIND_ALL_USB = false;
private static final boolean DEFAULT_MOUSE_EMULATION = true; 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 FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0; public static final int AUTOSELECT_H265 = 0;
@@ -79,35 +77,95 @@ public class PreferenceConfiguration {
public boolean enablePip; public boolean enablePip;
public boolean bindAllUsb; public boolean bindAllUsb;
public boolean mouseEmulation; public boolean mouseEmulation;
public boolean mouseNavButtons;
public boolean unlockFps;
public static int getDefaultBitrate(String resFpsString) { private static int getHeightFromResolutionString(String resString) {
if (resFpsString.equals("360p30")) { if (resString.equalsIgnoreCase("360p")) {
return BITRATE_DEFAULT_360_30; return 360;
} }
else if (resFpsString.equals("360p60")) { else if (resString.equalsIgnoreCase("480p")) {
return BITRATE_DEFAULT_360_60; return 480;
} }
else if (resFpsString.equals("720p30")) { else if (resString.equalsIgnoreCase("720p")) {
return BITRATE_DEFAULT_720_30; return 720;
} }
else if (resFpsString.equals("720p60")) { else if (resString.equalsIgnoreCase("1080p")) {
return BITRATE_DEFAULT_720_60; return 1080;
} }
else if (resFpsString.equals("1080p30")) { else if (resString.equalsIgnoreCase("1440p")) {
return BITRATE_DEFAULT_1080_30; return 1440;
} }
else if (resFpsString.equals("1080p60")) { else if (resString.equalsIgnoreCase("4K")) {
return BITRATE_DEFAULT_1080_60; return 2160;
}
else if (resFpsString.equals("4K30")) {
return BITRATE_DEFAULT_4K_30;
}
else if (resFpsString.equals("4K60")) {
return BITRATE_DEFAULT_4K_60;
} }
else { else {
// Should never get here // Should be unreachable
return DEFAULT_BITRATE; return 720;
}
}
private static int getWidthFromResolutionString(String resString) {
int height = getHeightFromResolutionString(resString);
if (height == 480) {
// This isn't an exact 16:9 resolution
return 854;
}
else {
return (height * 16) / 9;
}
}
private static String getResolutionString(int width, int height) {
switch (height) {
case 360:
return "360p";
case 480:
return "480p";
default:
case 720:
return "720p";
case 1080:
return "1080p";
case 1440:
return "1440p";
case 2160:
return "4K";
}
}
public static int getDefaultBitrate(String resString, String fpsString) {
int width = getWidthFromResolutionString(resString);
int height = getHeightFromResolutionString(resString);
int fps = Integer.parseInt(fpsString);
// This table prefers 16:10 resolutions because they are
// only slightly more pixels than the 16:9 equivalents, so
// we don't want to bump those 16:10 resolutions up to the
// next 16:9 slot.
//
// This logic is shamelessly stolen from Moonlight Qt:
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
if (width * height <= 640 * 360) {
return (int)(1000 * (fps / 30.0));
}
else if (width * height <= 854 * 480) {
return (int)(1500 * (fps / 30.0));
}
// This covers 1280x720 and 1280x800 too
else if (width * height <= 1366 * 768) {
return (int)(5000 * (fps / 30.0));
}
else if (width * height <= 1920 * 1200) {
return (int)(10000 * (fps / 30.0));
}
else if (width * height <= 2560 * 1600) {
return (int)(20000 * (fps / 30.0));
}
else /* if (width * height <= 3840 * 2160) */ {
return (int)(40000 * (fps / 30.0));
} }
} }
@@ -133,7 +191,9 @@ public class PreferenceConfiguration {
public static int getDefaultBitrate(Context context) { public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS)); return getDefaultBitrate(
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
} }
private static int getVideoFormatValue(Context context) { private static int getVideoFormatValue(Context context) {
@@ -161,9 +221,12 @@ public class PreferenceConfiguration {
prefs.edit() prefs.edit()
.remove(BITRATE_PREF_STRING) .remove(BITRATE_PREF_STRING)
.remove(BITRATE_PREF_OLD_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(VIDEO_FORMAT_PREF_STRING)
.remove(ENABLE_HDR_PREF_STRING) .remove(ENABLE_HDR_PREF_STRING)
.remove(UNLOCK_FPS_STRING)
.apply(); .apply();
} }
@@ -171,59 +234,76 @@ public class PreferenceConfiguration {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration(); 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); config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
if (config.bitrate == 0) { if (config.bitrate == 0) {
config.bitrate = getDefaultBitrate(context); config.bitrate = getDefaultBitrate(context);
} }
String 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.videoFormat = getVideoFormatValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE); config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
@@ -247,6 +327,8 @@ public class PreferenceConfiguration {
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP); config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB); config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION); 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; return config;
} }
@@ -6,6 +6,7 @@ import android.media.MediaCodecInfo;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
import android.os.Handler;
import android.preference.ListPreference; import android.preference.ListPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceCategory; import android.preference.PreferenceCategory;
@@ -24,6 +25,12 @@ import com.limelight.utils.UiHelper;
public class StreamSettings extends Activity { public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs; private PreferenceConfiguration previousPrefs;
void reloadSettings() {
getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment()
).commit();
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -33,9 +40,7 @@ public class StreamSettings extends Activity {
UiHelper.setLocale(this); UiHelper.setLocale(this);
setContentView(R.layout.activity_stream_settings); setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace( reloadSettings();
R.id.stream_settings, new SettingsFragment()
).commit();
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
} }
@@ -58,12 +63,20 @@ public class StreamSettings extends Activity {
public static class SettingsFragment extends PreferenceFragment { public static class SettingsFragment extends PreferenceFragment {
private static void removeResolution(ListPreference pref, String prefix) { 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; int matchingCount = 0;
ListPreference pref = (ListPreference) findPreference(preferenceKey);
// Count the number of matching entries we'll be removing // Count the number of matching entries we'll be removing
for (CharSequence seq : pref.getEntryValues()) { for (CharSequence seq : pref.getEntryValues()) {
if (seq.toString().startsWith(prefix)) { if (seq.toString().equalsIgnoreCase(value)) {
matchingCount++; matchingCount++;
} }
} }
@@ -73,8 +86,8 @@ public class StreamSettings extends Activity {
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount]; CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
int outIndex = 0; int outIndex = 0;
for (int i = 0; i < pref.getEntryValues().length; i++) { for (int i = 0; i < pref.getEntryValues().length; i++) {
if (pref.getEntryValues()[i].toString().startsWith(prefix)) { if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
// Skip matching prefixes // Skip matching values
continue; continue;
} }
@@ -83,15 +96,34 @@ public class StreamSettings extends Activity {
outIndex++; outIndex++;
} }
if (pref.getValue().equalsIgnoreCase(value)) {
onMatched.run();
}
// Update the preference with the new list // Update the preference with the new list
pref.setEntries(entries); pref.setEntries(entries);
pref.setEntryValues(entryValues); pref.setEntryValues(entryValues);
} }
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
if (res == null) {
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
}
if (fps == null) {
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
}
prefs.edit()
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
PreferenceConfiguration.getDefaultBitrate(res, fps))
.apply();
}
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
PreferenceScreen screen = getPreferenceScreen(); PreferenceScreen screen = getPreferenceScreen();
@@ -110,6 +142,8 @@ public class StreamSettings extends Activity {
category.removePreference(findPreference("checkbox_enable_pip")); category.removePreference(findPreference("checkbox_enable_pip"));
} }
int maxSupportedFps = 0;
// Hide non-supported resolution/FPS combinations // Hide non-supported resolution/FPS combinations
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display display = getActivity().getWindowManager().getDefaultDisplay(); Display display = getActivity().getWindowManager().getDefaultDisplay();
@@ -134,9 +168,16 @@ public class StreamSettings extends Activity {
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) { if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
maxSupportedResW = 3840; maxSupportedResW = 3840;
} }
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
maxSupportedResW = 2560;
}
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) { else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
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 // 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); LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
if (maxSupportedResW != 0) { if (maxSupportedResW != 0) {
if (maxSupportedResW < 3840) { if (maxSupportedResW < 3840) {
// 4K is unsupported // 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) { if (maxSupportedResW < 1920) {
// 1080p is unsupported // 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 // 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 // Remove HDR preference for devices below Nougat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
LimeLog.info("Excluding HDR toggle based on OS"); 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 // We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false; boolean foundHdr10 = false;
for (int hdrType : hdrCaps.getSupportedHdrTypes()) { if (hdrCaps != null) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) { // getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
foundHdr10 = true; 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 // Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted // so the bitrate can be auto-adjusted
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING); findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override @Override
public boolean onPreferenceChange(Preference preference, Object newValue) { public boolean onPreferenceChange(Preference preference, Object newValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity()); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue; String valueStr = (String) newValue;
// Write the new bitrate value // Write the new bitrate value
prefs.edit() resetBitrateToDefault(prefs, valueStr, null);
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
PreferenceConfiguration.getDefaultBitrate(valueStr)) // Allow the original preference change to take place
.apply(); return true;
}
});
findPreference(PreferenceConfiguration.FPS_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// Write the new bitrate value
resetBitrateToDefault(prefs, null, valueStr);
// Allow the original preference change to take place // Allow the original preference change to take place
return true; return true;
@@ -13,8 +13,12 @@ import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import org.xmlpull.v1.XmlPullParserException;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.security.cert.CertificateEncodingException;
public class ServerHelper { public class ServerHelper {
public static String getCurrentAddressFromComputer(ComputerDetails computer) { public static String getCurrentAddressFromComputer(ComputerDetails computer) {
@@ -29,19 +33,30 @@ public class ServerHelper {
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId()); intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported()); intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, getCurrentAddressFromComputer(computer).equals(computer.remoteAddress)); intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid);
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
intent.putExtra(Game.EXTRA_PC_NAME, computer.name); intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
try {
if (computer.serverCert != null) {
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
}
} catch (CertificateEncodingException e) {
e.printStackTrace();
}
return intent; return intent;
} }
public static void doStart(Activity parent, NvApp app, ComputerDetails computer, public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) { ComputerManagerService.ComputerManagerBinder managerBinder) {
if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
parent.startActivity(createStartIntent(parent, app, computer, managerBinder)); parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
} }
public static void doQuit(final Activity parent, public static void doQuit(final Activity parent,
final String address, final ComputerDetails computer,
final NvApp app, final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder, final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) { final Runnable onComplete) {
@@ -52,8 +67,8 @@ public class ServerHelper {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
try { try {
httpConn = new NvHTTP(address, httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent)); managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) { if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName(); message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else { } else {
@@ -72,8 +87,9 @@ public class ServerHelper {
message = parent.getResources().getString(R.string.error_unknown_host); message = parent.getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = parent.getResources().getString(R.string.error_404); message = parent.getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (IOException | XmlPullParserException e) {
message = e.getMessage(); message = e.getMessage();
e.printStackTrace();
} finally { } finally {
if (onComplete != null) { if (onComplete != null) {
onComplete.run(); onComplete.run();
@@ -125,7 +125,7 @@ public class ShortcutHelper {
} }
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) { public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd); createAppViewShortcut(id, details.name, details.uuid, forceAdd);
} }
@TargetApi(Build.VERSION_CODES.O) @TargetApi(Build.VERSION_CODES.O)
@@ -158,7 +158,7 @@ public class ShortcutHelper {
} }
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) { public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid.toString(), app.getAppName(), Integer.valueOf(app.getAppId()).toString()); return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid, app.getAppName(), Integer.valueOf(app.getAppId()).toString());
} }
public void disableShortcut(String id, CharSequence reason) { public void disableShortcut(String id, CharSequence reason) {
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</vector>
-1
View File
@@ -97,7 +97,6 @@
<string name="title_checkbox_51_surround">Activar sonido 5.1 surround</string> <string name="title_checkbox_51_surround">Activar sonido 5.1 surround</string>
<string name="summary_checkbox_51_surround">Desmarcar si experimentas problemas de audio. Requiere GFE 2.7 o superior.</string> <string name="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="title_checkbox_multi_controller">Soporte para múltiples mandos</string>
<string name="summary_checkbox_multi_controller">Si no está marcado, todos los mandos aparecen como uno solo</string> <string name="summary_checkbox_multi_controller">Si no está marcado, todos los mandos aparecen como uno solo</string>
<string name="title_seekbar_deadzone">Ajustar zona muerta del stick analógico</string> <string name="title_seekbar_deadzone">Ajustar zona muerta del stick analógico</string>
+11 -2
View File
@@ -80,6 +80,7 @@
<string name="lost_connection">Perte de connexion avec le PC</string> <string name="lost_connection">Perte de connexion avec le PC</string>
<string name="title_details">Détails</string> <string name="title_details">Détails</string>
<string name="help">Aide</string> <string name="help">Aide</string>
<string name="delete_pc_msg">Êtes-vous sûr de vouloir supprimer ce PC?</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="applist_connect_msg">Connexion au PC…</string> <string name="applist_connect_msg">Connexion au PC…</string>
@@ -110,11 +111,15 @@
<!-- Preferences --> <!-- Preferences -->
<string name="category_basic_settings">Paramètres de base</string> <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="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="title_seekbar_bitrate">Sélectionnez le bitrate vidéo à obtenir</string>
<string name="summary_seekbar_bitrate">Bitrate inférieur pour réduire la saccade. Augmentez le bitrate pour augmenter la qualité de l\'image.</string> <string name="summary_seekbar_bitrate">Bitrate inférieur pour réduire la saccade. Augmentez le bitrate pour augmenter la qualité de l\'image.</string>
<string name="suffix_seekbar_bitrate">Kbps</string> <string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_unlock_fps">Débloquer tous les taux d\'images possibles</string>
<string name="summary_unlock_fps">La diffusion en continu à 90 ou 120 FPS peut réduire la latence sur les périphériques haut de gamme, mais peut provoquer des retards ou des blocages sur les périphériques qui ne peuvent \pas le prendre en charge</string>
<string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string> <string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string>
<string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string> <string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string>
<string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string> <string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string>
@@ -125,7 +130,7 @@
<string name="title_checkbox_51_surround">Activer son surround 5.1</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="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="title_checkbox_multi_controller">Prise en charge de plusieurs contrôleurs</string>
<string name="summary_checkbox_multi_controller">Lorsqu\'elle n\'est pas cochée, tous les contrôleurs sont regroupés</string> <string name="summary_checkbox_multi_controller">Lorsqu\'elle n\'est pas cochée, tous les contrôleurs sont regroupés</string>
<string name="title_seekbar_deadzone">Régler la zone morte du stick analogique</string> <string name="title_seekbar_deadzone">Régler la zone morte du stick analogique</string>
@@ -136,6 +141,8 @@
<string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string> <string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string>
<string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string> <string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string>
<string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string> <string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string>
<string name="title_checkbox_mouse_nav_buttons">Activer les boutons de souris arrière et avant</string>
<string name="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="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string> <string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
@@ -163,6 +170,8 @@
<string name="summary_checkbox_host_audio">Lire l\'audio de l\'ordinateur et de ce périphérique</string> <string name="summary_checkbox_host_audio">Lire l\'audio de l\'ordinateur et de ce périphérique</string>
<string name="category_advanced_settings">Réglages avancés</string> <string name="category_advanced_settings">Réglages avancés</string>
<string name="title_disable_frame_drop">Désactiver la suppression d\'image</string>
<string name="summary_disable_frame_drop">Peut réduire les micro-saccades sur certains appareils, mais peut augmenter la latence</string>
<string name="title_video_format">Modifier les paramètres H.265</string> <string name="title_video_format">Modifier les paramètres H.265</string>
<string name="summary_video_format">H.265 réduit les besoins en bande passante vidéo mais nécessite un périphérique très récent</string> <string name="summary_video_format">H.265 réduit les besoins en bande passante vidéo mais nécessite un périphérique très récent</string>
<string name="title_enable_hdr">Activer le HDR (expérimental)</string> <string name="title_enable_hdr">Activer le HDR (expérimental)</string>
-1
View File
@@ -116,7 +116,6 @@
<string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string> <string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string>
<string name="summary_checkbox_51_surround">Se riscontri problemi, disabilitalo. Richiede GFE 2.7 o versioni sucessive.</string> <string name="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="title_checkbox_multi_controller">Supporto a più controller</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string> <string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Regola i punti morti degli stick analogici</string> <string name="title_seekbar_deadzone">Regola i punti morti degli stick analogici</string>
-1
View File
@@ -93,7 +93,6 @@
<string name="title_checkbox_51_surround">5.1chサラウンド</string> <string name="title_checkbox_51_surround">5.1chサラウンド</string>
<string name="summary_checkbox_51_surround">音声に問題が生じる場合はチェックを外してください。バージョン2.7以降のGFEが必要です</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="title_checkbox_multi_controller">複数のゲームコントローラ</string>
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string> <string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
<string name="title_seekbar_deadzone">アナログゲームコントローラのデッドゾーン</string> <string name="title_seekbar_deadzone">アナログゲームコントローラのデッドゾーン</string>
-1
View File
@@ -108,7 +108,6 @@
<string name="title_checkbox_51_surround">5.1 서라운드 사운드 활성화</string> <string name="title_checkbox_51_surround">5.1 서라운드 사운드 활성화</string>
<string name="summary_checkbox_51_surround">오디오 문제가 발생한다면 체크를 해제하세요. GFE 2.7이나 그 이상 버전이 필요합니다.</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="title_checkbox_multi_controller">다중 컨트롤러 지원</string>
<string name="summary_checkbox_multi_controller">이 옵션을 선택하지 않으면 모든 컨트롤러가 하나로 표시됩니다</string> <string name="summary_checkbox_multi_controller">이 옵션을 선택하지 않으면 모든 컨트롤러가 하나로 표시됩니다</string>
<string name="title_seekbar_deadzone">아날로그 스틱 데드존 설정</string> <string name="title_seekbar_deadzone">아날로그 스틱 데드존 설정</string>
-1
View File
@@ -97,7 +97,6 @@
<string name="title_checkbox_51_surround">Gebruik 5.1 surround sound</string> <string name="title_checkbox_51_surround">Gebruik 5.1 surround sound</string>
<string name="summary_checkbox_51_surround">Gebruik dit niet als er problemen zijn met de audio. Vereist GFE 2.7 of hoger.</string> <string name="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="title_checkbox_multi_controller">Multi-gamepad support</string>
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string> <string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
<string name="title_seekbar_deadzone">Pas analoge dodezone aan.</string> <string name="title_seekbar_deadzone">Pas analoge dodezone aan.</string>
-1
View File
@@ -98,7 +98,6 @@
<string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string> <string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string>
<string name="summary_checkbox_51_surround">Отключите, если появляются аудио проблемы. Требуется GFE 2.7 или выше.</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="title_checkbox_multi_controller">Поддержка нескольких контроллеров</string>
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один</string> <string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один</string>
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика</string> <string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика</string>
@@ -108,7 +108,6 @@
<string name="title_checkbox_51_surround"> 启用 5.1 环绕音效 </string> <string name="title_checkbox_51_surround"> 启用 5.1 环绕音效 </string>
<string name="summary_checkbox_51_surround"> 如果你的声音听起来有问题请禁用。\n\n需要GeForce Experience 2.7 或更高版本 </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="title_checkbox_multi_controller"> 启用多手柄支持 </string>
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄将会认作一个手柄 </string> <string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄将会认作一个手柄 </string>
<string name="title_seekbar_deadzone"> 调整摇杆死区 </string> <string name="title_seekbar_deadzone"> 调整摇杆死区 </string>
@@ -108,7 +108,6 @@
<string name="title_checkbox_51_surround"> 啟用 5.1 環繞音效 </string> <string name="title_checkbox_51_surround"> 啟用 5.1 環繞音效 </string>
<string name="summary_checkbox_51_surround"> 如果你的聲音聽起來有問題請禁用。\n\n需要GeForce Experience 2.7 或更高版本 </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="title_checkbox_multi_controller"> 啟用多手柄支持 </string>
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄將會認作一個手柄 </string> <string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄將會認作一個手柄 </string>
<string name="title_seekbar_deadzone"> 調整搖桿死區 </string> <string name="title_seekbar_deadzone"> 調整搖桿死區 </string>
+25 -16
View File
@@ -1,24 +1,33 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string-array name="resolution_names"> <string-array name="resolution_names">
<item>360p 30 FPS</item> <item>360p</item>
<item>360p 60 FPS</item> <item>480p</item>
<item>720p 30 FPS</item> <item>720p</item>
<item>720p 60 FPS</item> <item>1080p</item>
<item>1080p 30 FPS</item> <item>1440p</item>
<item>1080p 60 FPS</item> <item>4K</item>
<item>4K 30 FPS</item>
<item>4K 60 FPS</item>
</string-array> </string-array>
<string-array name="resolution_values" translatable="false"> <string-array name="resolution_values" translatable="false">
<item>360p30</item> <item>360p</item>
<item>360p60</item> <item>480p</item>
<item>720p30</item> <item>720p</item>
<item>720p60</item> <item>1080p</item>
<item>1080p30</item> <item>1440p</item>
<item>1080p60</item> <item>4K</item>
<item>4K30</item> </string-array>
<item>4K60</item>
<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>
<string-array name="language_names" translatable="false"> <string-array name="language_names" translatable="false">
+13 -7
View File
@@ -114,11 +114,15 @@
<!-- Preferences --> <!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string> <string name="category_basic_settings">Basic Settings</string>
<string name="title_resolution_list">Select resolution and FPS target</string> <string name="title_resolution_list">Video resolution</string>
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</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_seekbar_bitrate">Select target video bitrate</string> <string name="title_fps_list">Video frame rate</string>
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</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="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_stretch_video">Stretch video to full-screen</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string> <string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string> <string name="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="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="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="category_input_settings">Input Settings</string>
<string name="title_checkbox_multi_controller">Multiple controller support</string> <string name="title_checkbox_multi_controller">Automatic gamepad presence detection</string>
<string name="summary_checkbox_multi_controller">Uncheck for games with controller detection issues</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="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string> <string name="title_checkbox_xb1_driver">Xbox 360/One 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="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="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="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="category_on_screen_controls_settings">On-screen Controls Settings</string>
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string> <string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
+20 -3
View File
@@ -5,12 +5,19 @@
<PreferenceCategory android:title="@string/category_basic_settings" <PreferenceCategory android:title="@string/category_basic_settings"
android:key="category_basic_settings"> android:key="category_basic_settings">
<ListPreference <ListPreference
android:key="list_resolution_fps" android:key="list_resolution"
android:title="@string/title_resolution_list" android:title="@string/title_resolution_list"
android:summary="@string/summary_resolution_list" android:summary="@string/summary_resolution_list"
android:entries="@array/resolution_names" android:entries="@array/resolution_names"
android:entryValues="@array/resolution_values" 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 <com.limelight.preferences.SeekBarPreference
android:key="seekbar_bitrate_kbps" android:key="seekbar_bitrate_kbps"
android:dialogMessage="@string/summary_seekbar_bitrate" android:dialogMessage="@string/summary_seekbar_bitrate"
@@ -20,6 +27,11 @@
android:summary="@string/summary_seekbar_bitrate" android:summary="@string/summary_seekbar_bitrate"
android:text="@string/suffix_seekbar_bitrate" android:text="@string/suffix_seekbar_bitrate"
android:title="@string/title_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 <CheckBoxPreference
android:key="checkbox_stretch_video" android:key="checkbox_stretch_video"
android:title="@string/title_checkbox_stretch_video" android:title="@string/title_checkbox_stretch_video"
@@ -37,7 +49,7 @@
android:summary="@string/summary_checkbox_51_surround" android:summary="@string/summary_checkbox_51_surround"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/category_gamepad_settings"> <PreferenceCategory android:title="@string/category_input_settings">
<!--com.limelight.preferences.SeekBarPreference <!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone" android:key="seekbar_deadzone"
android:defaultValue="15" android:defaultValue="15"
@@ -49,6 +61,11 @@
android:title="@string/title_checkbox_multi_controller" android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller" android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" /> 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 <CheckBoxPreference
android:key="checkbox_usb_driver" android:key="checkbox_usb_driver"
android:title="@string/title_checkbox_xb1_driver" android:title="@string/title_checkbox_xb1_driver"
+1 -1
View File
@@ -5,7 +5,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.2.1' classpath 'com.android.tools.build:gradle:3.3.1'
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
#Tue May 08 18:56:31 PDT 2018 #Tue Feb 05 20:54:22 PST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip