Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99b53f9a6a | |||
| 8da563b280 | |||
| d5b950e5cf | |||
| c46b9acf6b | |||
| d8e322bac9 | |||
| 44871626cf | |||
| f661522b5d | |||
| a454b0ab78 | |||
| 75bf84d0d9 | |||
| c248994ed4 | |||
| a7a34ec629 | |||
| 8d469c5d0a | |||
| e6979d50b5 | |||
| 6e25b135a3 | |||
| 04e093a2c2 | |||
| 813f2edd95 | |||
| 337d753a33 | |||
| 1137c74f76 | |||
| 0c1451f757 | |||
| 5ab9ea48fd | |||
| ffcb623040 | |||
| bfe6929642 | |||
| 50d45011a8 | |||
| 2f7087d6d3 | |||
| 92b71588d0 | |||
| 4f3d018764 | |||
| a22e33eeb9 | |||
| 6a939e7495 | |||
| f8ba7cf190 | |||
| d1e135db4d | |||
| 61a17afe69 | |||
| 47fd691884 | |||
| 0d171c6b28 | |||
| f0c69d08b8 | |||
| 629bf5766d | |||
| 233bceeece | |||
| 6660ea7d91 | |||
| 4864b2ca45 | |||
| 92097b318d | |||
| 997898c99d | |||
| 1174e03885 | |||
| ff0f54d541 | |||
| 814964a100 | |||
| 7e154292a9 | |||
| 0f9cba1053 | |||
| a4e134589d | |||
| cd80a94f28 | |||
| 57c645a291 | |||
| 0cba200207 | |||
| 81582d7343 | |||
| 04e561fd54 | |||
| 5efbb5229d | |||
| 541e43eb18 | |||
| 7e679ff4c6 | |||
| 486b4b4c4c | |||
| 7d76bf7868 | |||
| db49077b9b | |||
| 16b845ab84 | |||
| 5c175fecf6 | |||
| 773976b265 | |||
| 80070bbdbe | |||
| 4c8d433b6c | |||
| 404f096d11 | |||
| d2ac927cec | |||
| 5e3d59d3d7 | |||
| 9cd2ce1309 | |||
| 9ed49730d4 | |||
| 39ebb48f58 | |||
| 1c29c70fba | |||
| 6993051529 | |||
| 4930087c4d | |||
| 795f0a013b | |||
| 213414778e | |||
| 7eac0ccaf8 | |||
| 6adc9dcb2d | |||
| be620908f9 | |||
| e4edfdb043 | |||
| 3b5028d1a4 | |||
| bc8c45bd59 | |||
| 63eb346a70 | |||
| 27ad691d23 | |||
| 747e920061 | |||
| 8d09f56a0e | |||
| 113a0e2c45 | |||
| 977215a098 | |||
| a7e65b47f9 | |||
| 7126055ad6 |
@@ -1,6 +1,6 @@
|
||||
# Moonlight Android
|
||||
|
||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||
|
||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
|
||||
versionName "5.10"
|
||||
versionCode = 175
|
||||
versionName "7.3"
|
||||
versionCode = 193
|
||||
}
|
||||
|
||||
flavorDimensions "root"
|
||||
|
||||
Vendored
+1
@@ -25,3 +25,4 @@
|
||||
|
||||
# jMDNS
|
||||
-dontwarn javax.jmdns.impl.DNSCache
|
||||
-dontwarn org.slf4j.**
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
@@ -28,7 +28,6 @@ import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
@@ -45,6 +44,8 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private AppGridAdapter appGridAdapter;
|
||||
private String uuidString;
|
||||
@@ -81,11 +82,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Wait for the binder to be ready
|
||||
localBinder.waitForReady();
|
||||
|
||||
// Now make the binder visible
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Get the computer object
|
||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||
computer = localBinder.getComputer(uuidString);
|
||||
if (computer == null) {
|
||||
finish();
|
||||
return;
|
||||
@@ -95,13 +93,18 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||
computer, managerBinder.getUniqueId());
|
||||
computer, localBinder.getUniqueId());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Now make the binder visible. We must do this after appGridAdapter
|
||||
// is set to prevent us from reaching updateUiWithServerinfo() and
|
||||
// touching the appGridAdapter prior to initialization.
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Load the app grid with cached data (if possible).
|
||||
// This must be done _before_ startComputerUpdates()
|
||||
// so the initial serverinfo response can update the running
|
||||
@@ -154,7 +157,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
// Don't care about other computers
|
||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,7 +181,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void run() {
|
||||
// Disable shortcuts referencing this PC for now
|
||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
||||
shortcutHelper.disableShortcut(details.uuid,
|
||||
getResources().getString(R.string.scut_not_paired));
|
||||
|
||||
// Display a toast to the user and quit the activity
|
||||
@@ -214,7 +217,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
blockingLoadSpinner.dismiss();
|
||||
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));
|
||||
updateUiWithAppList(applist);
|
||||
LimeLog.info("Loaded applist from cache");
|
||||
} catch (Exception e) {
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
if (lastRawApplist != null) {
|
||||
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||
e.printStackTrace();
|
||||
@@ -388,8 +393,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void run() {
|
||||
suspendGridUpdates = true;
|
||||
ServerHelper.doQuit(AppView.this,
|
||||
ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
ServerHelper.doQuit(AppView.this, computer,
|
||||
app.app, managerBinder, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
@@ -66,8 +66,15 @@ import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||
@@ -105,8 +112,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private boolean grabbedInput = true;
|
||||
private boolean grabComboDown = false;
|
||||
private StreamView streamView;
|
||||
private boolean gotBackPointerEvent = false;
|
||||
private boolean syntheticBackDown = false;
|
||||
private TextView notificationOverlayView;
|
||||
|
||||
private ShortcutHelper shortcutHelper;
|
||||
|
||||
@@ -134,10 +140,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
public static final String EXTRA_APP_NAME = "AppName";
|
||||
public static final String EXTRA_APP_ID = "AppId";
|
||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||
public static final String EXTRA_PC_UUID = "UUID";
|
||||
public static final String EXTRA_PC_NAME = "PcName";
|
||||
public static final String EXTRA_APP_HDR = "HDR";
|
||||
public static final String EXTRA_SERVER_CERT = "ServerCert";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -198,6 +204,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
streamView.setOnTouchListener(this);
|
||||
streamView.setInputCallbacks(this);
|
||||
|
||||
notificationOverlayView = findViewById(R.id.notificationOverlay);
|
||||
|
||||
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -228,10 +236,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
||||
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
|
||||
byte[] derCertData = Game.this.getIntent().getByteArrayExtra(EXTRA_SERVER_CERT);
|
||||
|
||||
X509Certificate serverCert = null;
|
||||
try {
|
||||
if (derCertData != null) {
|
||||
serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||
finish();
|
||||
@@ -348,8 +366,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Hopefully, we can get rid of this once someone comes up with a better way
|
||||
// to track the state of the pipeline and time frames.
|
||||
int roundedRefreshRate = Math.round(displayRefreshRate);
|
||||
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) {
|
||||
if (roundedRefreshRate <= 49) {
|
||||
if ((!prefConfig.disableFrameDrop || prefConfig.unlockFps) && prefConfig.fps >= roundedRefreshRate) {
|
||||
if (prefConfig.unlockFps) {
|
||||
// Use frame drops when rendering above the screen frame rate
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Using drop mode for FPS > Hz");
|
||||
}
|
||||
else if (roundedRefreshRate <= 49) {
|
||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
|
||||
@@ -372,8 +395,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
.setBitrate(prefConfig.bitrate)
|
||||
.setEnableSops(prefConfig.enableSops)
|
||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292)
|
||||
.setRemote(remote)
|
||||
.setMaxPacketSize(1392)
|
||||
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO)
|
||||
.setHevcBitratePercentageMultiplier(75)
|
||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||
.setEnableHdr(willStreamHdr)
|
||||
@@ -385,7 +408,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
.build();
|
||||
|
||||
// Initialize the connection
|
||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this));
|
||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert);
|
||||
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
||||
|
||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||
@@ -406,10 +429,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
if (prefConfig.onscreenController) {
|
||||
// create virtual onscreen controller
|
||||
virtualController = new VirtualController(conn,
|
||||
virtualController = new VirtualController(controllerHandler,
|
||||
(FrameLayout)streamView.getParent(),
|
||||
this);
|
||||
virtualController.refreshLayout();
|
||||
virtualController.show();
|
||||
}
|
||||
|
||||
if (prefConfig.usbDriver) {
|
||||
@@ -501,8 +525,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display.Mode bestMode = display.getMode();
|
||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
|
||||
candidate.getRefreshRate() < 63;
|
||||
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate();
|
||||
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
||||
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
||||
candidate.getPhysicalWidth() <= 4096;
|
||||
@@ -518,6 +541,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
||||
if (prefConfig.fps <= 60) {
|
||||
if (candidate.getRefreshRate() >= 63) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the refresh rate doesn't regress
|
||||
if (!refreshRateOk) {
|
||||
continue;
|
||||
@@ -535,12 +565,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||
displayRefreshRate = bestMode.getRefreshRate();
|
||||
}
|
||||
// On L, we can at least tell the OS that we want 60 Hz
|
||||
// On L, we can at least tell the OS that we want a refresh rate
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
float bestRefreshRate = display.getRefreshRate();
|
||||
for (float candidate : display.getSupportedRefreshRates()) {
|
||||
if (candidate > bestRefreshRate && candidate < 63) {
|
||||
if (candidate > bestRefreshRate) {
|
||||
LimeLog.info("Examining refresh rate: "+candidate);
|
||||
|
||||
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
||||
if (prefConfig.fps <= 60) {
|
||||
if (candidate >= 63) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bestRefreshRate = candidate;
|
||||
}
|
||||
}
|
||||
@@ -682,6 +720,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
SpinnerDialog.closeDialogs(this);
|
||||
Dialog.closeDialogs();
|
||||
|
||||
if (virtualController != null) {
|
||||
virtualController.hide();
|
||||
}
|
||||
|
||||
if (conn != null) {
|
||||
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
||||
|
||||
@@ -838,16 +880,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Handle a synthetic back button event that some Android OS versions
|
||||
// create as a result of a right-click. This event WILL repeat if
|
||||
// the right mouse button is held down, so we ignore those.
|
||||
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
if (!prefConfig.mouseNavButtons &&
|
||||
(event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
// It appears this may get turned into a right-click pointer event
|
||||
// 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);
|
||||
}
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -876,7 +913,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
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;
|
||||
@@ -896,19 +938,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// Handle a synthetic back button event that some Android OS versions
|
||||
// create as a result of a right-click.
|
||||
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
if (!prefConfig.mouseNavButtons &&
|
||||
(event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
// It appears this may get turned into a right-click pointer event
|
||||
// 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);
|
||||
}
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -935,7 +969,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
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;
|
||||
@@ -1015,11 +1056,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
else {
|
||||
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) {
|
||||
@@ -1031,9 +1067,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: Disable mouse back button press on Xiaomi due to reported
|
||||
// issues with right clicks triggering it.
|
||||
if (!("Xiaomi".equalsIgnoreCase(Build.MANUFACTURER))) {
|
||||
if (prefConfig.mouseNavButtons) {
|
||||
if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1);
|
||||
@@ -1041,19 +1075,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
else {
|
||||
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 ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
|
||||
}
|
||||
else {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
|
||||
if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
|
||||
}
|
||||
else {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1242,6 +1272,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (connecting || connected) {
|
||||
connecting = connected = false;
|
||||
|
||||
controllerHandler.stop();
|
||||
|
||||
// Stop may take a few hundred ms to do some network I/O to tell
|
||||
// the server we're going away and clean up. Let it run in a separate
|
||||
// thread to keep things smooth for the UI. Inside moonlight-common,
|
||||
@@ -1297,8 +1329,41 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
LimeLog.severe("Connection terminated: " + errorCode);
|
||||
stopConnection();
|
||||
|
||||
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
|
||||
getResources().getString(R.string.conn_terminated_msg), true);
|
||||
// Display the error dialog if it was an unexpected termination.
|
||||
// Otherwise, just finish the activity immediately.
|
||||
if (errorCode != 0) {
|
||||
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
|
||||
getResources().getString(R.string.conn_terminated_msg), true);
|
||||
}
|
||||
else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionStatusUpdate(final int connectionStatus) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (prefConfig.disableWarnings) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionStatus == MoonBridge.CONN_STATUS_POOR) {
|
||||
if (prefConfig.bitrate > 5000) {
|
||||
notificationOverlayView.setText(getResources().getString(R.string.slow_connection_msg));
|
||||
}
|
||||
else {
|
||||
notificationOverlayView.setText(getResources().getString(R.string.poor_connection_msg));
|
||||
}
|
||||
|
||||
notificationOverlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else if (connectionStatus == MoonBridge.CONN_STATUS_OKAY) {
|
||||
notificationOverlayView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1352,6 +1417,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
|
||||
LimeLog.info(String.format((Locale)null, "Rumble on gamepad %d: %04x %04x", controllerNumber, lowFreqMotor, highFreqMotor));
|
||||
|
||||
controllerHandler.handleRumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
if (!surfaceCreated) {
|
||||
|
||||
@@ -53,6 +53,8 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
@@ -346,7 +348,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@@ -372,9 +375,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
PlatformBinding.getDeviceName(),
|
||||
computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
if (httpConn.getPairState() == PairState.PAIRED) {
|
||||
// Don't display any toast, but open the app list
|
||||
message = null;
|
||||
success = true;
|
||||
@@ -386,21 +389,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||
|
||||
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||
PairingManager pm = httpConn.getPairingManager();
|
||||
|
||||
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairState.PIN_WRONG) {
|
||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.FAILED) {
|
||||
else if (pairState == PairState.FAILED) {
|
||||
message = getResources().getString(R.string.pair_fail);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) {
|
||||
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
|
||||
message = getResources().getString(R.string.pair_already_in_progress);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||
else if (pairState == PairState.PAIRED) {
|
||||
// Just navigate to the app view without displaying a toast
|
||||
message = null;
|
||||
success = true;
|
||||
|
||||
// Pin this certificate for later HTTPS use
|
||||
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
|
||||
|
||||
// Invalidate reachability information after pairing to force
|
||||
// a refresh before reading pair state again
|
||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||
@@ -414,7 +422,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
message = getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
message = e.getMessage();
|
||||
}
|
||||
@@ -478,7 +486,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@@ -496,7 +505,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
try {
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
PlatformBinding.getDeviceName(),
|
||||
computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
httpConn.unpair();
|
||||
@@ -514,8 +523,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
message = getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
message = e.getMessage();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
final String toastMessage = message;
|
||||
@@ -541,7 +551,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
Intent i = new Intent(this, AppView.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||
startActivity(i);
|
||||
}
|
||||
|
||||
@@ -603,8 +613,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ServerHelper.doQuit(PcView.this,
|
||||
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
||||
ServerHelper.doQuit(PcView.this, computer.details,
|
||||
new NvApp("app", 0, false), managerBinder, null);
|
||||
}
|
||||
}, null);
|
||||
@@ -625,7 +634,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
if (details.equals(computer.details)) {
|
||||
// Disable or delete shortcuts referencing this PC
|
||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
||||
shortcutHelper.disableShortcut(details.uuid,
|
||||
getResources().getString(R.string.scut_deleted_pc));
|
||||
|
||||
pcGridAdapter.removeComputer(computer);
|
||||
@@ -656,7 +665,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
// Add a launcher shortcut for this PC
|
||||
if (details.pairState == PairState.PAIRED) {
|
||||
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false);
|
||||
shortcutHelper.createAppViewShortcut(details.uuid, details, false);
|
||||
}
|
||||
|
||||
if (existingEntry != null) {
|
||||
|
||||
@@ -48,7 +48,7 @@ public class ShortcutTrampoline extends Activity {
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Get the computer object
|
||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||
computer = managerBinder.getComputer(uuidString);
|
||||
|
||||
if (computer == null) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
@@ -77,7 +77,7 @@ public class ShortcutTrampoline extends Activity {
|
||||
@Override
|
||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||
// Don't care about other computers
|
||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -200,6 +200,14 @@ public class ShortcutTrampoline extends Activity {
|
||||
|
||||
protected boolean validateInput() {
|
||||
// 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 {
|
||||
UUID.fromString(uuidString);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
|
||||
@@ -168,8 +168,17 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playDecodedAudio(byte[] audioData) {
|
||||
track.write(audioData, 0, audioData.length);
|
||||
public void playDecodedAudio(short[] audioData) {
|
||||
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
||||
if (MoonBridge.getPendingAudioFrames() < 8) {
|
||||
// This will block until the write is completed. That can cause a backlog
|
||||
// of pending audio data, so we do the above check to be able to bound
|
||||
// latency at 40 ms in that situation.
|
||||
track.write(audioData, 0, audioData.length);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Too many pending audio frames: " + MoonBridge.getPendingAudioFrames());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
@@ -155,7 +154,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
} catch (Exception e) {
|
||||
// Nothing should go wrong here
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
LimeLog.info("Generated a new key pair");
|
||||
|
||||
@@ -4,8 +4,11 @@ import android.content.Context;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.media.AudioAttributes;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.SparseArray;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
@@ -14,6 +17,7 @@ import android.view.MotionEvent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.input.driver.AbstractController;
|
||||
import com.limelight.binding.input.driver.UsbDriverListener;
|
||||
import com.limelight.binding.input.driver.UsbDriverService;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
@@ -51,6 +55,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
private final double stickDeadzone;
|
||||
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
||||
private final GameGestures gestures;
|
||||
private final Vibrator deviceVibrator;
|
||||
private boolean hasGameController;
|
||||
|
||||
private final PreferenceConfiguration prefConfig;
|
||||
@@ -61,10 +66,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
this.conn = conn;
|
||||
this.gestures = gestures;
|
||||
this.prefConfig = prefConfig;
|
||||
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
|
||||
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
||||
// HACK: For now we're hardcoding a 7% deadzone. Some deadzone
|
||||
// is required for controller batching support to work.
|
||||
int deadzonePercentage = 10;
|
||||
int deadzonePercentage = 7;
|
||||
|
||||
int[] ids = InputDevice.getDeviceIds();
|
||||
for (int id : ids) {
|
||||
@@ -151,6 +157,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
onInputDeviceAdded(deviceId);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
||||
|
||||
if (deviceContext.vibrator != null) {
|
||||
deviceContext.vibrator.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
deviceVibrator.cancel();
|
||||
}
|
||||
|
||||
private static boolean hasJoystickAxes(InputDevice device) {
|
||||
return (device.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
|
||||
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_X) != null &&
|
||||
@@ -206,6 +224,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
}
|
||||
|
||||
if (PreferenceConfiguration.readPreferences(context).onscreenController) {
|
||||
LimeLog.info("Counting OSC gamepad");
|
||||
mask |= 1;
|
||||
}
|
||||
|
||||
LimeLog.info("Enumerated "+count+" gamepads");
|
||||
return mask;
|
||||
}
|
||||
@@ -298,10 +321,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.assignedControllerNumber = true;
|
||||
}
|
||||
|
||||
private UsbDeviceContext createUsbDeviceContextForDevice(int deviceId) {
|
||||
private UsbDeviceContext createUsbDeviceContextForDevice(AbstractController device) {
|
||||
UsbDeviceContext context = new UsbDeviceContext();
|
||||
|
||||
context.id = deviceId;
|
||||
context.id = device.getControllerId();
|
||||
context.device = device;
|
||||
|
||||
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||
@@ -393,6 +417,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.name = devName;
|
||||
context.id = dev.getId();
|
||||
|
||||
if (dev.getVibrator().hasVibrator()) {
|
||||
context.vibrator = dev.getVibrator();
|
||||
}
|
||||
|
||||
context.leftStickXAxis = MotionEvent.AXIS_X;
|
||||
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
|
||||
@@ -614,7 +642,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
|
||||
private short getActiveControllerMask() {
|
||||
if (prefConfig.multiController) {
|
||||
return (short)(currentControllers | initialControllers);
|
||||
return (short)(currentControllers | initialControllers | (prefConfig.onscreenController ? 1 : 0));
|
||||
}
|
||||
else {
|
||||
// Only Player 1 is active with multi-controller disabled
|
||||
@@ -1043,6 +1071,94 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
}
|
||||
|
||||
private void rumbleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) {
|
||||
// Since we can only use a single amplitude value, compute the desired amplitude
|
||||
// by taking 80% of the big motor and 33% of the small motor, then capping to 255.
|
||||
// NB: This value is now 0-255 as required by VibrationEffect.
|
||||
short lowFreqMotorMSB = (short)((lowFreqMotor >> 8) & 0xFF);
|
||||
short highFreqMotorMSB = (short)((highFreqMotor >> 8) & 0xFF);
|
||||
int simulatedAmplitude = Math.min(255, (int)((lowFreqMotorMSB * 0.80) + (highFreqMotorMSB * 0.33)));
|
||||
|
||||
if (simulatedAmplitude == 0) {
|
||||
// This case is easy - just cancel the current effect and get out.
|
||||
// NB: We cannot simply check lowFreqMotor == highFreqMotor == 0
|
||||
// because our simulatedAmplitude could be 0 even though our inputs
|
||||
// are not (ex: lowFreqMotor == 0 && highFreqMotor == 1).
|
||||
vibrator.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to use amplitude-based control if we're on Oreo and the device
|
||||
// supports amplitude-based vibration control.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (vibrator.hasAmplitudeControl()) {
|
||||
VibrationEffect effect = VibrationEffect.createOneShot(60000, simulatedAmplitude);
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.build();
|
||||
vibrator.vibrate(effect, audioAttributes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach this point, we don't have amplitude controls available, so
|
||||
// we must emulate it by PWMing the vibration. Ick.
|
||||
long pwmPeriod = 20;
|
||||
long onTime = (long)((simulatedAmplitude / 255.0) * pwmPeriod);
|
||||
long offTime = pwmPeriod - onTime;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.build();
|
||||
vibrator.vibrate(new long[]{0, onTime, offTime}, 0, audioAttributes);
|
||||
}
|
||||
else {
|
||||
vibrator.vibrate(new long[]{0, onTime, offTime}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleRumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
|
||||
boolean foundMatchingDevice = false;
|
||||
boolean vibrated = false;
|
||||
|
||||
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
||||
|
||||
if (deviceContext.controllerNumber == controllerNumber) {
|
||||
foundMatchingDevice = true;
|
||||
|
||||
if (deviceContext.vibrator != null) {
|
||||
vibrated = true;
|
||||
rumbleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < usbDeviceContexts.size(); i++) {
|
||||
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
|
||||
|
||||
if (deviceContext.controllerNumber == controllerNumber) {
|
||||
foundMatchingDevice = vibrated = true;
|
||||
deviceContext.device.rumble((short)lowFreqMotor, (short)highFreqMotor);
|
||||
}
|
||||
}
|
||||
|
||||
// We may decide to rumble the device for player 1
|
||||
if (controllerNumber == 0) {
|
||||
// If we didn't find a matching device, it must be the on-screen
|
||||
// controls that triggered the rumble. Vibrate the device if
|
||||
// the user has requested that behavior.
|
||||
if (!foundMatchingDevice && prefConfig.onscreenController && !prefConfig.onlyL3R3 && prefConfig.vibrateOsc) {
|
||||
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
else if (foundMatchingDevice && !vibrated && prefConfig.vibrateFallbackToDevice) {
|
||||
// We found a device to vibrate but it didn't have rumble support. The user
|
||||
// has requested us to vibrate the device in this case.
|
||||
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean handleButtonUp(KeyEvent event) {
|
||||
InputDeviceContext context = getContextForEvent(event);
|
||||
if (context == null) {
|
||||
@@ -1294,12 +1410,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return true;
|
||||
}
|
||||
|
||||
public void reportOscState(short buttonFlags,
|
||||
short leftStickX, short leftStickY,
|
||||
short rightStickX, short rightStickY,
|
||||
byte leftTrigger, byte rightTrigger) {
|
||||
defaultContext.leftStickX = leftStickX;
|
||||
defaultContext.leftStickY = leftStickY;
|
||||
|
||||
defaultContext.rightStickX = rightStickX;
|
||||
defaultContext.rightStickY = rightStickY;
|
||||
|
||||
defaultContext.leftTrigger = leftTrigger;
|
||||
defaultContext.rightTrigger = rightTrigger;
|
||||
|
||||
defaultContext.inputMap = buttonFlags;
|
||||
|
||||
sendControllerInputPacket(defaultContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportControllerState(int controllerId, short buttonFlags,
|
||||
float leftStickX, float leftStickY,
|
||||
float rightStickX, float rightStickY,
|
||||
float leftTrigger, float rightTrigger) {
|
||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
||||
GenericControllerContext context = usbDeviceContexts.get(controllerId);
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1334,19 +1468,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceRemoved(int controllerId) {
|
||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
||||
public void deviceRemoved(AbstractController controller) {
|
||||
UsbDeviceContext context = usbDeviceContexts.get(controller.getControllerId());
|
||||
if (context != null) {
|
||||
LimeLog.info("Removed controller: "+controllerId);
|
||||
LimeLog.info("Removed controller: "+controller.getControllerId());
|
||||
releaseControllerNumber(context);
|
||||
usbDeviceContexts.remove(controllerId);
|
||||
usbDeviceContexts.remove(controller.getControllerId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceAdded(int controllerId) {
|
||||
UsbDeviceContext context = createUsbDeviceContextForDevice(controllerId);
|
||||
usbDeviceContexts.put(controllerId, context);
|
||||
public void deviceAdded(AbstractController controller) {
|
||||
UsbDeviceContext context = createUsbDeviceContextForDevice(controller);
|
||||
usbDeviceContexts.put(controller.getControllerId(), context);
|
||||
}
|
||||
|
||||
class GenericControllerContext {
|
||||
@@ -1375,6 +1509,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
|
||||
class InputDeviceContext extends GenericControllerContext {
|
||||
public String name;
|
||||
public Vibrator vibrator;
|
||||
|
||||
public int leftStickXAxis = -1;
|
||||
public int leftStickYAxis = -1;
|
||||
@@ -1412,5 +1547,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
public long startDownTime = 0;
|
||||
}
|
||||
|
||||
class UsbDeviceContext extends GenericControllerContext {}
|
||||
class UsbDeviceContext extends GenericControllerContext {
|
||||
public AbstractController device;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,21 @@ public class KeyboardTranslator {
|
||||
public static final int VK_BACK_QUOTE = 192;
|
||||
public static final int VK_QUOTE = 222;
|
||||
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
|
||||
* @param keycode the code to be translated
|
||||
@@ -116,7 +130,8 @@ public class KeyboardTranslator {
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
translated = 0x0d;
|
||||
break;
|
||||
|
||||
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_EQUALS:
|
||||
translated = 0xbb;
|
||||
break;
|
||||
@@ -257,7 +272,19 @@ public class KeyboardTranslator {
|
||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||
translated = 0x6E;
|
||||
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:
|
||||
System.out.println("No key for "+keycode);
|
||||
return 0;
|
||||
|
||||
@@ -37,11 +37,13 @@ public abstract class AbstractController {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
||||
|
||||
protected void notifyDeviceRemoved() {
|
||||
listener.deviceRemoved(deviceId);
|
||||
listener.deviceRemoved(this);
|
||||
}
|
||||
|
||||
protected void notifyDeviceAdded() {
|
||||
listener.deviceAdded(deviceId);
|
||||
listener.deviceAdded(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,9 @@ public abstract class AbstractXboxController extends AbstractController {
|
||||
|
||||
stopped = true;
|
||||
|
||||
// Cancel any rumble effects
|
||||
rumble((short)0, (short)0);
|
||||
|
||||
// Stop the input thread
|
||||
if (inputThread != null) {
|
||||
inputThread.interrupt();
|
||||
|
||||
@@ -6,6 +6,6 @@ public interface UsbDriverListener {
|
||||
float rightStickX, float rightStickY,
|
||||
float leftTrigger, float rightTrigger);
|
||||
|
||||
void deviceRemoved(int controllerId);
|
||||
void deviceAdded(int controllerId);
|
||||
void deviceRemoved(AbstractController controller);
|
||||
void deviceAdded(AbstractController controller);
|
||||
}
|
||||
|
||||
@@ -47,26 +47,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceRemoved(int controllerId) {
|
||||
public void deviceRemoved(AbstractController controller) {
|
||||
// Remove the the controller from our list (if not removed already)
|
||||
for (AbstractController controller : controllers) {
|
||||
if (controller.getControllerId() == controllerId) {
|
||||
controllers.remove(controller);
|
||||
break;
|
||||
}
|
||||
}
|
||||
controllers.remove(controller);
|
||||
|
||||
// Call through to the client's listener
|
||||
if (listener != null) {
|
||||
listener.deviceRemoved(controllerId);
|
||||
listener.deviceRemoved(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceAdded(int controllerId) {
|
||||
public void deviceAdded(AbstractController controller) {
|
||||
// Call through to the client's listener
|
||||
if (listener != null) {
|
||||
listener.deviceAdded(controllerId);
|
||||
listener.deviceAdded(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +108,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
// Report all controllerMap that already exist
|
||||
if (listener != null) {
|
||||
for (AbstractController controller : controllers) {
|
||||
listener.deviceAdded(controller.getControllerId());
|
||||
listener.deviceAdded(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public class Xbox360Controller extends AbstractXboxController {
|
||||
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||
|
||||
private static final int[] SUPPORTED_VENDORS = {
|
||||
0x0079, // GPD Win 2
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x046d, // Logitech
|
||||
@@ -23,6 +24,7 @@ public class Xbox360Controller extends AbstractXboxController {
|
||||
0x07ff, // Mad Catz
|
||||
0x0e6f, // Unknown
|
||||
0x0f0d, // Hori
|
||||
0x1038, // SteelSeries
|
||||
0x11c9, // Nacon
|
||||
0x12ab, // Unknown
|
||||
0x1430, // RedOctane
|
||||
@@ -137,4 +139,17 @@ public class Xbox360Controller extends AbstractXboxController {
|
||||
// No need to fail init if the LED command fails
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
||||
byte[] data = {
|
||||
0x00, 0x08, 0x00,
|
||||
(byte)(lowFreqMotor >> 8), (byte)(highFreqMotor >> 8),
|
||||
0x00, 0x00, 0x00
|
||||
};
|
||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
||||
if (res != data.length) {
|
||||
LimeLog.warning("Rumble transfer failed: "+res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class XboxOneController extends AbstractXboxController {
|
||||
|
||||
@@ -23,8 +24,31 @@ public class XboxOneController extends AbstractXboxController {
|
||||
0x24c6, // PowerA
|
||||
};
|
||||
|
||||
// FIXME: odata_serial
|
||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
private static final byte[] FW2015_INIT = {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),
|
||||
};
|
||||
|
||||
private byte seqNum = 0;
|
||||
|
||||
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
super(device, connection, deviceId, listener);
|
||||
@@ -111,13 +135,55 @@ public class XboxOneController extends AbstractXboxController {
|
||||
|
||||
@Override
|
||||
protected boolean doInit() {
|
||||
// Send the initialization packet
|
||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
||||
if (res != XB1_INIT_DATA.length) {
|
||||
LimeLog.warning("Initialization transfer failed: "+res);
|
||||
return false;
|
||||
// Send all applicable init packets
|
||||
for (InitPacket pkt : INIT_PKTS) {
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
||||
byte[] data = {
|
||||
0x09, 0x00, seqNum++, 0x09, 0x00,
|
||||
0x0F, 0x00, 0x00,
|
||||
(byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9),
|
||||
(byte)0xFF, 0x00, (byte)0xFF
|
||||
};
|
||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
||||
if (res != data.length) {
|
||||
LimeLog.warning("Rumble transfer failed: "+res);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||
notifyOnRevoke();
|
||||
|
||||
// not longer pressed reset analog stick
|
||||
notifyOnMovement(0, 0);
|
||||
}
|
||||
|
||||
+37
-28
@@ -13,10 +13,13 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.R;
|
||||
import com.limelight.binding.input.ControllerHandler;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class VirtualController {
|
||||
public class ControllerInputContext {
|
||||
@@ -36,12 +39,14 @@ public class VirtualController {
|
||||
|
||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||
|
||||
private NvConnection connection = null;
|
||||
private ControllerHandler controllerHandler;
|
||||
private Context context = null;
|
||||
|
||||
private FrameLayout frame_layout = null;
|
||||
private RelativeLayout relative_layout = null;
|
||||
|
||||
private Timer retransmitTimer;
|
||||
|
||||
ControllerMode currentMode = ControllerMode.Active;
|
||||
ControllerInputContext inputContext = new ControllerInputContext();
|
||||
|
||||
@@ -49,8 +54,8 @@ public class VirtualController {
|
||||
|
||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||
|
||||
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
||||
this.connection = conn;
|
||||
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
|
||||
this.controllerHandler = controllerHandler;
|
||||
this.frame_layout = layout;
|
||||
this.context = context;
|
||||
|
||||
@@ -88,11 +93,24 @@ public class VirtualController {
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
retransmitTimer.cancel();
|
||||
relative_layout.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
relative_layout.setVisibility(View.VISIBLE);
|
||||
|
||||
// HACK: GFE sometimes discards gamepad packets when they are received
|
||||
// very shortly after another. This can be critical if an axis zeroing packet
|
||||
// is lost and causes an analog stick to get stuck. To avoid this, we send
|
||||
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
|
||||
retransmitTimer = new Timer("OSC timer", true);
|
||||
retransmitTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendControllerInputContext();
|
||||
}
|
||||
}, 100, 100);
|
||||
}
|
||||
|
||||
public void removeElements() {
|
||||
@@ -149,32 +167,23 @@ public class VirtualController {
|
||||
return inputContext;
|
||||
}
|
||||
|
||||
public void sendControllerInputContext() {
|
||||
sendControllerInputPacket();
|
||||
}
|
||||
void sendControllerInputContext() {
|
||||
_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() {
|
||||
try {
|
||||
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
||||
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
||||
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
||||
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||
|
||||
if (connection != null) {
|
||||
connection.sendControllerInput(
|
||||
inputContext.inputMap,
|
||||
inputContext.leftTrigger,
|
||||
inputContext.rightTrigger,
|
||||
inputContext.leftStickX,
|
||||
inputContext.leftStickY,
|
||||
inputContext.rightStickX,
|
||||
inputContext.rightStickY
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
if (controllerHandler != null) {
|
||||
controllerHandler.reportOscState(
|
||||
inputContext.inputMap,
|
||||
inputContext.leftStickX,
|
||||
inputContext.leftStickY,
|
||||
inputContext.rightStickX,
|
||||
inputContext.rightStickY,
|
||||
inputContext.leftTrigger,
|
||||
inputContext.rightTrigger
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-46
@@ -166,58 +166,54 @@ public abstract class VirtualControllerElement extends View {
|
||||
}
|
||||
|
||||
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[]{
|
||||
"Move",
|
||||
"Resize",
|
||||
/*election
|
||||
"Set n
|
||||
Disable color sormal color",
|
||||
"Set pressed color",
|
||||
CharSequence functions[] = new CharSequence[]{
|
||||
"Move",
|
||||
"Resize",
|
||||
/*election
|
||||
"Set n
|
||||
Disable color sormal 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"
|
||||
};
|
||||
|
||||
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();
|
||||
default: { // cancel
|
||||
actionCancel();
|
||||
break;
|
||||
}
|
||||
case 3: { // set pressed color
|
||||
actionShowPressedColorChooser();
|
||||
break;
|
||||
}
|
||||
*/
|
||||
default: { // cancel
|
||||
actionCancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
AlertDialog alert = alertBuilder.create();
|
||||
// show menu
|
||||
alert.show();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
AlertDialog alert = alertBuilder.create();
|
||||
// show menu
|
||||
alert.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -405,10 +405,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
// Render the last buffer
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !legacyFrameDropRendering) {
|
||||
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
||||
// is disabled
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (legacyFrameDropRendering) {
|
||||
// Use a PTS that will cause this frame to be dropped if another comes in within
|
||||
// the same V-sync period
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime());
|
||||
}
|
||||
else {
|
||||
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
||||
// is disabled
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
|
||||
@@ -160,8 +160,8 @@ public class MediaCodecHelper {
|
||||
|
||||
// We see a bunch of crashes on MediaTek Android TVs running
|
||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
||||
// these devices and hope they fix it in Oreo.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
// these devices and hope they fix it in Pie.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import android.content.ContentValues;
|
||||
@@ -23,6 +26,7 @@ public class ComputerDatabaseManager {
|
||||
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
|
||||
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
|
||||
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||
|
||||
private SQLiteDatabase computerDb;
|
||||
|
||||
@@ -43,13 +47,21 @@ public class ComputerDatabaseManager {
|
||||
}
|
||||
|
||||
private void initializeDb(Context c) {
|
||||
// Add cert column to the table if not present
|
||||
try {
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"ALTER TABLE %s ADD COLUMN %s TEXT",
|
||||
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
|
||||
} catch (SQLiteException e) {}
|
||||
|
||||
|
||||
// Create tables if they aren't already there
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %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_UUID_COLUMN_NAME, COMPUTER_NAME_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
|
||||
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
||||
@@ -64,32 +76,47 @@ public class ComputerDatabaseManager {
|
||||
|
||||
public boolean updateComputer(ComputerDetails details) {
|
||||
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(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
|
||||
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
||||
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
||||
try {
|
||||
if (details.serverCert != null) {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, details.serverCert.getEncoded());
|
||||
}
|
||||
else {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
String uuidStr = 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.uuid = c.getString(0);
|
||||
details.name = c.getString(1);
|
||||
details.localAddress = c.getString(2);
|
||||
details.remoteAddress = c.getString(3);
|
||||
details.manualAddress = c.getString(4);
|
||||
details.macAddress = c.getString(5);
|
||||
|
||||
try {
|
||||
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)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
|
||||
@@ -115,8 +142,8 @@ public class ComputerDatabaseManager {
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerByUUID(UUID uuid) {
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid.toString() }, null, null, null);
|
||||
public ComputerDetails getComputerByUUID(String uuid) {
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
c.close();
|
||||
|
||||
@@ -8,7 +8,6 @@ import java.net.Socket;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
@@ -18,6 +17,7 @@ import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
@@ -105,17 +105,27 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
// If it's online, update our persistent state
|
||||
if (details.state == ComputerDetails.State.ONLINE) {
|
||||
if (!newPc) {
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (dbManager.getComputerByUUID(details.uuid) == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
|
||||
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (!newPc && existingComputer == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
|
||||
dbManager.updateComputer(details);
|
||||
// If we already have an entry for this computer in the DB, we must
|
||||
// combine the existing data with this new data (which may be partially available
|
||||
// due to detecting the PC via mDNS) without the saved external address. If we
|
||||
// write to the DB without doing this first, we can overwrite our existing data.
|
||||
if (existingComputer != null) {
|
||||
existingComputer.update(details);
|
||||
dbManager.updateComputer(existingComputer);
|
||||
}
|
||||
else {
|
||||
dbManager.updateComputer(details);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't call the listener if this is a failed lookup of a new PC
|
||||
@@ -231,7 +241,7 @@ public class ComputerManagerService extends Service {
|
||||
return idManager.getUniqueId();
|
||||
}
|
||||
|
||||
public ComputerDetails getComputer(UUID uuid) {
|
||||
public ComputerDetails getComputer(String uuid) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (uuid.equals(tuple.computer.uuid)) {
|
||||
@@ -243,7 +253,7 @@ public class ComputerManagerService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void invalidateStateForComputer(UUID uuid) {
|
||||
public void invalidateStateForComputer(String uuid) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
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) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// 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
|
||||
// our WAN address, which is also very likely the WAN address
|
||||
// of the PC. We can use this later to connect remotely.
|
||||
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns();
|
||||
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||
}
|
||||
|
||||
// Block while we try to fill the details
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
@@ -365,7 +391,7 @@ public class ComputerManagerService extends Service {
|
||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||
|
||||
// Start a polling thread for this machine
|
||||
addTuple(fakeDetails, manuallyAdded);
|
||||
addTuple(fakeDetails);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
@@ -425,14 +451,18 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
||||
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
ComputerDetails newDetails = http.getComputerDetails();
|
||||
|
||||
// Check if this is the PC we expected
|
||||
if (details.uuid != null && newDetails.uuid != null &&
|
||||
!details.uuid.equals(newDetails.uuid)) {
|
||||
if (newDetails.uuid == null) {
|
||||
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!
|
||||
LimeLog.info("Polling returned the wrong PC!");
|
||||
return null;
|
||||
@@ -442,7 +472,8 @@ public class ComputerManagerService extends Service {
|
||||
newDetails.activeAddress = address;
|
||||
|
||||
return newDetails;
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
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
|
||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")");
|
||||
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
|
||||
LimeLog.info("Fast poll for "+details.name+" returned 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 (candidateAddress == null) {
|
||||
@@ -593,7 +624,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||
// Add tuples for each computer
|
||||
addTuple(computer, true);
|
||||
addTuple(computer);
|
||||
}
|
||||
|
||||
releaseLocalDatabaseReference();
|
||||
@@ -671,8 +702,9 @@ public class ComputerManagerService extends Service {
|
||||
public void run() {
|
||||
int emptyAppListResponses = 0;
|
||||
do {
|
||||
// Can't poll if it's not online
|
||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||
// Can't poll if it's not online or paired
|
||||
if (computer.state != ComputerDetails.State.ONLINE ||
|
||||
computer.pairState != PairingManager.PairState.PAIRED) {
|
||||
if (listener != null) {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
@@ -688,7 +720,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
String appList;
|
||||
if (tuple != null) {
|
||||
@@ -712,12 +744,12 @@ public class ComputerManagerService extends Service {
|
||||
// in a row, we'll go ahead and believe it.
|
||||
emptyAppListResponses++;
|
||||
}
|
||||
if (appList != null && !appList.isEmpty() &&
|
||||
if (!appList.isEmpty() &&
|
||||
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||
// Open the cache file
|
||||
OutputStream cacheOut = null;
|
||||
try {
|
||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
|
||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -744,7 +776,7 @@ public class ComputerManagerService extends Service {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
}
|
||||
else if (appList == null || appList.isEmpty()) {
|
||||
else if (appList.isEmpty()) {
|
||||
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class LegacyDatabaseReader {
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
@@ -24,14 +23,7 @@ public class LegacyDatabaseReader {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for " + details.name);
|
||||
}
|
||||
details.uuid = c.getString(1);
|
||||
|
||||
// An earlier schema defined addresses as byte blobs. We'll
|
||||
// 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.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -77,6 +78,14 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
overlayView.setAlpha(0.4f);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class DiskAssetLoader {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -65,7 +65,7 @@ public class DiskAssetLoader {
|
||||
}
|
||||
|
||||
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
|
||||
if (!file.exists()) {
|
||||
@@ -137,7 +137,7 @@ public class DiskAssetLoader {
|
||||
OutputStream out = null;
|
||||
boolean success = false;
|
||||
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);
|
||||
success = true;
|
||||
} catch (IOException e) {
|
||||
@@ -151,7 +151,7 @@ public class DiskAssetLoader {
|
||||
|
||||
if (!success) {
|
||||
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) {
|
||||
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
||||
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
||||
}
|
||||
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
|
||||
@@ -22,7 +22,8 @@ public class NetworkAssetLoader {
|
||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
InputStream in = null;
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
|
||||
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
|
||||
in = http.getBoxArt(tuple.app);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
|
||||
@@ -91,14 +91,20 @@ public class AddComputerManually extends Activity {
|
||||
}
|
||||
|
||||
private void doAddPc(String host) {
|
||||
String msg;
|
||||
boolean wrongSiteLocal = false;
|
||||
boolean success;
|
||||
|
||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||
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){
|
||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.limelight.preferences;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
@@ -7,7 +7,11 @@ import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
public class PreferenceConfiguration {
|
||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
|
||||
|
||||
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
||||
static final String FPS_PREF_STRING = "list_fps";
|
||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
||||
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||
@@ -29,18 +33,13 @@ public class PreferenceConfiguration {
|
||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
||||
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
||||
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
|
||||
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
||||
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
||||
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
||||
|
||||
private static final int BITRATE_DEFAULT_360_30 = 1000;
|
||||
private static final int BITRATE_DEFAULT_360_60 = 2000;
|
||||
private static final int BITRATE_DEFAULT_720_30 = 5000;
|
||||
private static final int BITRATE_DEFAULT_720_60 = 10000;
|
||||
private static final int BITRATE_DEFAULT_1080_30 = 10000;
|
||||
private static final int BITRATE_DEFAULT_1080_60 = 20000;
|
||||
private static final int BITRATE_DEFAULT_4K_30 = 40000;
|
||||
private static final int BITRATE_DEFAULT_4K_60 = 80000;
|
||||
|
||||
private static final String DEFAULT_RES_FPS = "720p60";
|
||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||
static final String DEFAULT_RESOLUTION = "720p";
|
||||
static final String DEFAULT_FPS = "60";
|
||||
private static final boolean DEFAULT_STRETCH = false;
|
||||
private static final boolean DEFAULT_SOPS = true;
|
||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||
@@ -54,12 +53,15 @@ public class PreferenceConfiguration {
|
||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
||||
private static final boolean DEFAULT_BATTERY_SAVER = false;
|
||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
||||
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
||||
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
||||
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
||||
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
||||
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
||||
|
||||
public static final int FORCE_H265_ON = -1;
|
||||
public static final int AUTOSELECT_H265 = 0;
|
||||
@@ -79,35 +81,97 @@ public class PreferenceConfiguration {
|
||||
public boolean enablePip;
|
||||
public boolean bindAllUsb;
|
||||
public boolean mouseEmulation;
|
||||
public boolean mouseNavButtons;
|
||||
public boolean unlockFps;
|
||||
public boolean vibrateOsc;
|
||||
public boolean vibrateFallbackToDevice;
|
||||
|
||||
public static int getDefaultBitrate(String resFpsString) {
|
||||
if (resFpsString.equals("360p30")) {
|
||||
return BITRATE_DEFAULT_360_30;
|
||||
private static int getHeightFromResolutionString(String resString) {
|
||||
if (resString.equalsIgnoreCase("360p")) {
|
||||
return 360;
|
||||
}
|
||||
else if (resFpsString.equals("360p60")) {
|
||||
return BITRATE_DEFAULT_360_60;
|
||||
else if (resString.equalsIgnoreCase("480p")) {
|
||||
return 480;
|
||||
}
|
||||
else if (resFpsString.equals("720p30")) {
|
||||
return BITRATE_DEFAULT_720_30;
|
||||
else if (resString.equalsIgnoreCase("720p")) {
|
||||
return 720;
|
||||
}
|
||||
else if (resFpsString.equals("720p60")) {
|
||||
return BITRATE_DEFAULT_720_60;
|
||||
else if (resString.equalsIgnoreCase("1080p")) {
|
||||
return 1080;
|
||||
}
|
||||
else if (resFpsString.equals("1080p30")) {
|
||||
return BITRATE_DEFAULT_1080_30;
|
||||
else if (resString.equalsIgnoreCase("1440p")) {
|
||||
return 1440;
|
||||
}
|
||||
else if (resFpsString.equals("1080p60")) {
|
||||
return BITRATE_DEFAULT_1080_60;
|
||||
}
|
||||
else if (resFpsString.equals("4K30")) {
|
||||
return BITRATE_DEFAULT_4K_30;
|
||||
}
|
||||
else if (resFpsString.equals("4K60")) {
|
||||
return BITRATE_DEFAULT_4K_60;
|
||||
else if (resString.equalsIgnoreCase("4K")) {
|
||||
return 2160;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return DEFAULT_BITRATE;
|
||||
// Should be unreachable
|
||||
return 720;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getWidthFromResolutionString(String resString) {
|
||||
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 +197,9 @@ public class PreferenceConfiguration {
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
||||
return getDefaultBitrate(
|
||||
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
|
||||
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||
}
|
||||
|
||||
private static int getVideoFormatValue(Context context) {
|
||||
@@ -161,9 +227,12 @@ public class PreferenceConfiguration {
|
||||
prefs.edit()
|
||||
.remove(BITRATE_PREF_STRING)
|
||||
.remove(BITRATE_PREF_OLD_STRING)
|
||||
.remove(RES_FPS_PREF_STRING)
|
||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||
.remove(RESOLUTION_PREF_STRING)
|
||||
.remove(FPS_PREF_STRING)
|
||||
.remove(VIDEO_FORMAT_PREF_STRING)
|
||||
.remove(ENABLE_HDR_PREF_STRING)
|
||||
.remove(UNLOCK_FPS_STRING)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -171,59 +240,76 @@ public class PreferenceConfiguration {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||
|
||||
// Migrate legacy preferences to the new locations
|
||||
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
||||
if (str != null) {
|
||||
if (str.equals("360p30")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("360p60")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("4K30")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("4K60")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||
.putString(RESOLUTION_PREF_STRING, getResolutionString(config.width, config.height))
|
||||
.putString(FPS_PREF_STRING, ""+config.fps)
|
||||
.apply();
|
||||
}
|
||||
else {
|
||||
// Use the new preference location
|
||||
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
||||
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
||||
}
|
||||
|
||||
// This must happen after the preferences migration to ensure the preferences are populated
|
||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
||||
if (config.bitrate == 0) {
|
||||
config.bitrate = getDefaultBitrate(context);
|
||||
}
|
||||
|
||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||
if (str.equals("360p30")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("360p60")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("4K30")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("4K60")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
|
||||
config.videoFormat = getVideoFormatValue(context);
|
||||
|
||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||
@@ -247,6 +333,10 @@ public class PreferenceConfiguration {
|
||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
||||
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
||||
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
||||
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
||||
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
||||
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.media.MediaCodecInfo;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
@@ -24,6 +25,12 @@ import com.limelight.utils.UiHelper;
|
||||
public class StreamSettings extends Activity {
|
||||
private PreferenceConfiguration previousPrefs;
|
||||
|
||||
void reloadSettings() {
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -33,9 +40,7 @@ public class StreamSettings extends Activity {
|
||||
UiHelper.setLocale(this);
|
||||
|
||||
setContentView(R.layout.activity_stream_settings);
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
reloadSettings();
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
}
|
||||
@@ -58,12 +63,20 @@ public class StreamSettings extends Activity {
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment {
|
||||
|
||||
private static void removeResolution(ListPreference pref, String prefix) {
|
||||
private void setValue(String preferenceKey, String value) {
|
||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||
|
||||
pref.setValue(value);
|
||||
}
|
||||
|
||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||
int matchingCount = 0;
|
||||
|
||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||
|
||||
// Count the number of matching entries we'll be removing
|
||||
for (CharSequence seq : pref.getEntryValues()) {
|
||||
if (seq.toString().startsWith(prefix)) {
|
||||
if (seq.toString().equalsIgnoreCase(value)) {
|
||||
matchingCount++;
|
||||
}
|
||||
}
|
||||
@@ -73,8 +86,8 @@ public class StreamSettings extends Activity {
|
||||
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
||||
int outIndex = 0;
|
||||
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
||||
if (pref.getEntryValues()[i].toString().startsWith(prefix)) {
|
||||
// Skip matching prefixes
|
||||
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
||||
// Skip matching values
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -83,15 +96,34 @@ public class StreamSettings extends Activity {
|
||||
outIndex++;
|
||||
}
|
||||
|
||||
if (pref.getValue().equalsIgnoreCase(value)) {
|
||||
onMatched.run();
|
||||
}
|
||||
|
||||
// Update the preference with the new list
|
||||
pref.setEntries(entries);
|
||||
pref.setEntryValues(entryValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
||||
if (res == null) {
|
||||
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||
}
|
||||
if (fps == null) {
|
||||
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(res, fps))
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
PreferenceScreen screen = getPreferenceScreen();
|
||||
|
||||
@@ -110,6 +142,8 @@ public class StreamSettings extends Activity {
|
||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
||||
}
|
||||
|
||||
int maxSupportedFps = 0;
|
||||
|
||||
// Hide non-supported resolution/FPS combinations
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||
@@ -134,9 +168,16 @@ public class StreamSettings extends Activity {
|
||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||
maxSupportedResW = 3840;
|
||||
}
|
||||
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
|
||||
maxSupportedResW = 2560;
|
||||
}
|
||||
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
||||
maxSupportedResW = 1920;
|
||||
}
|
||||
|
||||
if (candidate.getRefreshRate() > maxSupportedFps) {
|
||||
maxSupportedFps = (int)candidate.getRefreshRate();
|
||||
}
|
||||
}
|
||||
|
||||
// This must be called to do runtime initialization before calling functions that evaluate
|
||||
@@ -186,20 +227,102 @@ public class StreamSettings extends Activity {
|
||||
|
||||
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
||||
|
||||
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
|
||||
if (maxSupportedResW != 0) {
|
||||
if (maxSupportedResW < 3840) {
|
||||
// 4K is unsupported
|
||||
removeResolution(resPref, "4K");
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedResW < 2560) {
|
||||
// 1440p is unsupported
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedResW < 1920) {
|
||||
// 1080p is unsupported
|
||||
removeResolution(resPref, "1080p");
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Never remove 720p
|
||||
}
|
||||
}
|
||||
|
||||
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
||||
// We give some extra room in case the FPS is rounded down
|
||||
if (maxSupportedFps < 118) {
|
||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "120", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "90");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedFps < 88) {
|
||||
// 1080p is unsupported
|
||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "90", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "60");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Never remove 30 FPS or 60 FPS
|
||||
}
|
||||
|
||||
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
|
||||
// that the unlock FPS option relies on to not massively increase latency.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_basic_settings");
|
||||
category.removePreference(findPreference("checkbox_unlock_fps"));
|
||||
}
|
||||
else {
|
||||
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
// HACK: We need to let the preference change succeed before reinitializing to ensure
|
||||
// it's reflected in the new layout.
|
||||
final Handler h = new Handler();
|
||||
h.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Ensure the activity is still open when this timeout expires
|
||||
StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity();
|
||||
if (settingsActivity != null) {
|
||||
settingsActivity.reloadSettings();
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove HDR preference for devices below Nougat
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
LimeLog.info("Excluding HDR toggle based on OS");
|
||||
@@ -213,9 +336,12 @@ public class StreamSettings extends Activity {
|
||||
|
||||
// We must now ensure our display is compatible with HDR10
|
||||
boolean foundHdr10 = false;
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
foundHdr10 = true;
|
||||
if (hdrCaps != null) {
|
||||
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
foundHdr10 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,18 +355,27 @@ public class StreamSettings extends Activity {
|
||||
|
||||
// Add a listener to the FPS and resolution preference
|
||||
// so the bitrate can be auto-adjusted
|
||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
||||
.apply();
|
||||
resetBitrateToDefault(prefs, valueStr, null);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
findPreference(PreferenceConfiguration.FPS_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
resetBitrateToDefault(prefs, null, valueStr);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
|
||||
@@ -13,8 +13,12 @@ import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
|
||||
public class ServerHelper {
|
||||
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_HDR, app.isHdrSupported());
|
||||
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.toString());
|
||||
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid);
|
||||
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||
try {
|
||||
if (computer.serverCert != null) {
|
||||
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||
}
|
||||
|
||||
public static void doQuit(final Activity parent,
|
||||
final String address,
|
||||
final ComputerDetails computer,
|
||||
final NvApp app,
|
||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||
final Runnable onComplete) {
|
||||
@@ -52,8 +67,8 @@ public class ServerHelper {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(address,
|
||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
|
||||
if (httpConn.quitApp()) {
|
||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||
} else {
|
||||
@@ -72,8 +87,9 @@ public class ServerHelper {
|
||||
message = parent.getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = parent.getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
message = e.getMessage();
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (onComplete != null) {
|
||||
onComplete.run();
|
||||
|
||||
@@ -125,7 +125,7 @@ public class ShortcutHelper {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -158,7 +158,7 @@ public class ShortcutHelper {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
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"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -1,9 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4 -1.5,0 -2.89,0.19 -4.15,0.48L18.18,13.8 23.64,7zM17.04,15.22L3.27,1.44 2,2.72l2.05,2.06C1.91,5.76 0.59,6.82 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01 3.9,-4.86 3.32,3.32 1.27,-1.27 -3.46,-3.46z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<vector android:height="128dp" android:width="128dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
</vector>
|
||||
|
||||
@@ -10,4 +10,16 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notificationOverlay"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_gravity="right"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="right"
|
||||
android:background="#80000000"
|
||||
android:visibility="gone" />
|
||||
|
||||
</merge>
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
<string name="title_checkbox_51_surround">Activar sonido 5.1 surround</string>
|
||||
<string name="summary_checkbox_51_surround">Desmarcar si experimentas problemas de audio. Requiere GFE 2.7 o superior.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Configuración de mando</string>
|
||||
<string name="title_checkbox_multi_controller">Soporte para múltiples mandos</string>
|
||||
<string name="summary_checkbox_multi_controller">Si no está marcado, todos los mandos aparecen como uno solo</string>
|
||||
<string name="title_seekbar_deadzone">Ajustar zona muerta del stick analógico</string>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Shortcut strings -->
|
||||
<string name="scut_deleted_pc">PC supprimé</string>
|
||||
<string name="scut_not_paired">PC non appairé</string>
|
||||
@@ -80,6 +81,7 @@
|
||||
<string name="lost_connection">Perte de connexion avec le PC</string>
|
||||
<string name="title_details">Détails</string>
|
||||
<string name="help">Aide</string>
|
||||
<string name="delete_pc_msg">Êtes-vous sûr de vouloir supprimer ce PC?</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_connect_msg">Connexion au PC…</string>
|
||||
@@ -110,11 +112,15 @@
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Paramètres de base</string>
|
||||
<string name="title_resolution_list">Sélectionner la résolution et les FPS à atteindre</string>
|
||||
<string name="title_resolution_list">Résolution vidéo</string>
|
||||
<string name="summary_resolution_list">Le réglage de valeurs trop élevées pour votre appareil peut provoquer un retard ou un plantage</string>
|
||||
<string name="title_fps_list">Fréquence d\'images vidéo</string>
|
||||
<string name="summary_fps_list">Augmenter pour un flux vidéo plus lisse. Diminution pour de meilleures performances sur les périphériques bas de gamme.</string>
|
||||
<string name="title_seekbar_bitrate">Sélectionnez le bitrate vidéo à obtenir</string>
|
||||
<string name="summary_seekbar_bitrate">Bitrate inférieur pour réduire la saccade. Augmentez le bitrate pour augmenter la qualité de l\'image.</string>
|
||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
||||
<string name="title_unlock_fps">Débloquer tous les taux d\'images possibles</string>
|
||||
<string name="summary_unlock_fps">La diffusion en continu à 90 ou 120 FPS peut réduire la latence sur les périphériques haut de gamme, mais peut provoquer des retards ou des blocages sur les périphériques qui ne peuvent \pas le prendre en charge</string>
|
||||
<string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string>
|
||||
<string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string>
|
||||
<string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string>
|
||||
@@ -125,9 +131,11 @@
|
||||
<string name="title_checkbox_51_surround">Activer son surround 5.1</string>
|
||||
<string name="summary_checkbox_51_surround">Décochez si vous rencontrez des problèmes audio. Nécessite GFE 2.7 ou supérieur.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Paramètres du gamepad</string>
|
||||
<string name="category_input_settings">Paramètres d\'entrée</string>
|
||||
<string name="title_checkbox_multi_controller">Prise en charge de plusieurs contrôleurs</string>
|
||||
<string name="summary_checkbox_multi_controller">Lorsqu\'elle n\'est pas cochée, tous les contrôleurs sont regroupés</string>
|
||||
<string name="title_checkbox_vibrate_fallback">Emuler le support vibration</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">Vibre votre appareil pour émuler une vibration si votre manette ne le prend pas en charge</string>
|
||||
<string name="title_seekbar_deadzone">Régler la zone morte du stick analogique</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Pilote de contrôleur Xbox 360/One</string>
|
||||
@@ -136,10 +144,14 @@
|
||||
<string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string>
|
||||
<string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string>
|
||||
<string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Activer les boutons de souris arrière et avant</string>
|
||||
<string name="summary_checkbox_mouse_nav_buttons">L\'activation de cette option peut entraîner un clic droit sur certains périphériques.</string>
|
||||
|
||||
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
|
||||
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
|
||||
<string name="title_checkbox_vibrate_osc">Activer les vibrations</string>
|
||||
<string name="summary_checkbox_vibrate_osc">Vibre votre appareil pour émuler les vibrations des commandes à l\'écran</string>
|
||||
<string name="title_only_l3r3">Montre seulement L3 et R3</string>
|
||||
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
|
||||
<string name="title_reset_osc">Effacer la disposition des commandes à l\'écran sauvegardée</string>
|
||||
@@ -163,6 +175,8 @@
|
||||
<string name="summary_checkbox_host_audio">Lire l\'audio de l\'ordinateur et de ce périphérique</string>
|
||||
|
||||
<string name="category_advanced_settings">Réglages avancés</string>
|
||||
<string name="title_disable_frame_drop">Désactiver la suppression d\'image</string>
|
||||
<string name="summary_disable_frame_drop">Peut réduire les micro-saccades sur certains appareils, mais peut augmenter la latence</string>
|
||||
<string name="title_video_format">Modifier les paramètres H.265</string>
|
||||
<string name="summary_video_format">H.265 réduit les 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>
|
||||
|
||||
@@ -116,7 +116,6 @@
|
||||
<string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string>
|
||||
<string name="summary_checkbox_51_surround">Se riscontri problemi, disabilitalo. Richiede GFE 2.7 o versioni sucessive.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Impostazioni controller</string>
|
||||
<string name="title_checkbox_multi_controller">Supporto a più controller</string>
|
||||
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string>
|
||||
<string name="title_seekbar_deadzone">Regola i punti morti degli stick analogici</string>
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
<string name="title_checkbox_51_surround">5.1chサラウンド</string>
|
||||
<string name="summary_checkbox_51_surround">音声に問題が生じる場合はチェックを外してください。バージョン2.7以降のGFEが必要です</string>
|
||||
|
||||
<string name="category_gamepad_settings">ゲームコントローラ</string>
|
||||
<string name="title_checkbox_multi_controller">複数のゲームコントローラ</string>
|
||||
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
|
||||
<string name="title_seekbar_deadzone">アナログゲームコントローラのデッドゾーン</string>
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
<string name="title_checkbox_51_surround">5.1 서라운드 사운드 활성화</string>
|
||||
<string name="summary_checkbox_51_surround">오디오 문제가 발생한다면 체크를 해제하세요. GFE 2.7이나 그 이상 버전이 필요합니다.</string>
|
||||
|
||||
<string name="category_gamepad_settings">게임패드 설정</string>
|
||||
<string name="title_checkbox_multi_controller">다중 컨트롤러 지원</string>
|
||||
<string name="summary_checkbox_multi_controller">이 옵션을 선택하지 않으면 모든 컨트롤러가 하나로 표시됩니다</string>
|
||||
<string name="title_seekbar_deadzone">아날로그 스틱 데드존 설정</string>
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
<string name="title_checkbox_51_surround">Gebruik 5.1 surround sound</string>
|
||||
<string name="summary_checkbox_51_surround">Gebruik dit niet als er problemen zijn met de audio. Vereist GFE 2.7 of hoger.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Gamepad Instellingen</string>
|
||||
<string name="title_checkbox_multi_controller">Multi-gamepad support</string>
|
||||
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
|
||||
<string name="title_seekbar_deadzone">Pas analoge dodezone aan.</string>
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
<string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string>
|
||||
<string name="summary_checkbox_51_surround">Отключите, если появляются аудио проблемы. Требуется GFE 2.7 или выше.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Настройки Геймпада</string>
|
||||
<string name="title_checkbox_multi_controller">Поддержка нескольких контроллеров</string>
|
||||
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один</string>
|
||||
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика</string>
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
<string name="title_checkbox_51_surround"> 启用 5.1 环绕音效 </string>
|
||||
<string name="summary_checkbox_51_surround"> 如果你的声音听起来有问题请禁用。\n\n需要GeForce Experience 2.7 或更高版本 </string>
|
||||
|
||||
<string name="category_gamepad_settings"> 手柄设置 </string>
|
||||
<string name="title_checkbox_multi_controller"> 启用多手柄支持 </string>
|
||||
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄将会认作一个手柄 </string>
|
||||
<string name="title_seekbar_deadzone"> 调整摇杆死区 </string>
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
<string name="title_checkbox_51_surround"> 啟用 5.1 環繞音效 </string>
|
||||
<string name="summary_checkbox_51_surround"> 如果你的聲音聽起來有問題請禁用。\n\n需要GeForce Experience 2.7 或更高版本 </string>
|
||||
|
||||
<string name="category_gamepad_settings"> 手柄設置 </string>
|
||||
<string name="title_checkbox_multi_controller"> 啟用多手柄支持 </string>
|
||||
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄將會認作一個手柄 </string>
|
||||
<string name="title_seekbar_deadzone"> 調整搖桿死區 </string>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="resolution_names">
|
||||
<item>360p 30 FPS</item>
|
||||
<item>360p 60 FPS</item>
|
||||
<item>720p 30 FPS</item>
|
||||
<item>720p 60 FPS</item>
|
||||
<item>1080p 30 FPS</item>
|
||||
<item>1080p 60 FPS</item>
|
||||
<item>4K 30 FPS</item>
|
||||
<item>4K 60 FPS</item>
|
||||
<item>360p</item>
|
||||
<item>480p</item>
|
||||
<item>720p</item>
|
||||
<item>1080p</item>
|
||||
<item>1440p</item>
|
||||
<item>4K</item>
|
||||
</string-array>
|
||||
<string-array name="resolution_values" translatable="false">
|
||||
<item>360p30</item>
|
||||
<item>360p60</item>
|
||||
<item>720p30</item>
|
||||
<item>720p60</item>
|
||||
<item>1080p30</item>
|
||||
<item>1080p60</item>
|
||||
<item>4K30</item>
|
||||
<item>4K60</item>
|
||||
<item>360p</item>
|
||||
<item>480p</item>
|
||||
<item>720p</item>
|
||||
<item>1080p</item>
|
||||
<item>1440p</item>
|
||||
<item>4K</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="fps_names">
|
||||
<item>30 FPS</item>
|
||||
<item>60 FPS</item>
|
||||
<item>90 FPS</item>
|
||||
<item>120 FPS</item>
|
||||
</string-array>
|
||||
<string-array name="fps_values" translatable="false">
|
||||
<item>30</item>
|
||||
<item>60</item>
|
||||
<item>90</item>
|
||||
<item>120</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="language_names" translatable="false">
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
<string name="title_details">Details</string>
|
||||
<string name="help">Help</string>
|
||||
<string name="delete_pc_msg">Are you sure you want to delete this PC?</string>
|
||||
<string name="slow_connection_msg">Slow connection to PC\nReduce your bitrate</string>
|
||||
<string name="poor_connection_msg">Poor connection to PC</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_connect_msg">Connecting to PC…</string>
|
||||
@@ -114,11 +116,15 @@
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Basic Settings</string>
|
||||
<string name="title_resolution_list">Select resolution and FPS target</string>
|
||||
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</string>
|
||||
<string name="title_seekbar_bitrate">Select target video bitrate</string>
|
||||
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
|
||||
<string name="title_resolution_list">Video resolution</string>
|
||||
<string name="summary_resolution_list">Increase to improve image clarity. Decrease for better performance on lower end devices and slower networks.</string>
|
||||
<string name="title_fps_list">Video frame rate</string>
|
||||
<string name="summary_fps_list">Increase for a smoother video stream. Decrease for better performance on lower end devices.</string>
|
||||
<string name="title_seekbar_bitrate">Video bitrate</string>
|
||||
<string name="summary_seekbar_bitrate">Increase for better image quality. Decrease to improve performance on slower connections.</string>
|
||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
||||
<string name="title_unlock_fps">Unlock all possible frame rates</string>
|
||||
<string name="summary_unlock_fps">Streaming at 90 or 120 FPS may reduce latency on high-end devices but can cause lag or crashes on devices that can\'t support it</string>
|
||||
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
|
||||
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
|
||||
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
|
||||
@@ -129,9 +135,11 @@
|
||||
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
|
||||
<string name="summary_checkbox_51_surround">Uncheck if you experience audio issues. Requires GFE 2.7 or higher.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Gamepad Settings</string>
|
||||
<string name="title_checkbox_multi_controller">Multiple controller support</string>
|
||||
<string name="summary_checkbox_multi_controller">Uncheck for games with controller detection issues</string>
|
||||
<string name="category_input_settings">Input Settings</string>
|
||||
<string name="title_checkbox_multi_controller">Automatic gamepad presence detection</string>
|
||||
<string name="summary_checkbox_multi_controller">Unchecking this option forces a gamepad to always be present</string>
|
||||
<string name="title_checkbox_vibrate_fallback">Emulate rumble support with vibration</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">Vibrates your device to emulate rumble if your gamepad does not support it</string>
|
||||
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string>
|
||||
@@ -140,10 +148,14 @@
|
||||
<string name="summary_checkbox_usb_bind_all">Forces Moonlight\'s USB driver to take over all supported Xbox gamepads</string>
|
||||
<string name="title_checkbox_mouse_emulation">Mouse emulation via gamepad</string>
|
||||
<string name="summary_checkbox_mouse_emulation">Long pressing the Start button will switch the gamepad into mouse mode</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Enable back and forward mouse buttons</string>
|
||||
<string name="summary_checkbox_mouse_nav_buttons">Enabling this option may break right clicking on some buggy devices</string>
|
||||
|
||||
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
|
||||
<string name="summary_checkbox_show_onscreen_controls">Show virtual controller overlay on touchscreen</string>
|
||||
<string name="title_checkbox_vibrate_osc">Enable vibration</string>
|
||||
<string name="summary_checkbox_vibrate_osc">Vibrates your device to emulate rumble for the on-screen controls</string>
|
||||
<string name="title_only_l3r3">Only show L3 and R3</string>
|
||||
<string name="summary_only_l3r3">Hide all virtual buttons except L3 and R3</string>
|
||||
<string name="title_reset_osc">Clear saved on-screen controls layout</string>
|
||||
|
||||
@@ -5,12 +5,19 @@
|
||||
<PreferenceCategory android:title="@string/category_basic_settings"
|
||||
android:key="category_basic_settings">
|
||||
<ListPreference
|
||||
android:key="list_resolution_fps"
|
||||
android:key="list_resolution"
|
||||
android:title="@string/title_resolution_list"
|
||||
android:summary="@string/summary_resolution_list"
|
||||
android:entries="@array/resolution_names"
|
||||
android:entryValues="@array/resolution_values"
|
||||
android:defaultValue="720p60" />
|
||||
android:defaultValue="720p" />
|
||||
<ListPreference
|
||||
android:key="list_fps"
|
||||
android:title="@string/title_fps_list"
|
||||
android:summary="@string/summary_fps_list"
|
||||
android:entries="@array/fps_names"
|
||||
android:entryValues="@array/fps_values"
|
||||
android:defaultValue="60" />
|
||||
<com.limelight.preferences.SeekBarPreference
|
||||
android:key="seekbar_bitrate_kbps"
|
||||
android:dialogMessage="@string/summary_seekbar_bitrate"
|
||||
@@ -20,6 +27,11 @@
|
||||
android:summary="@string/summary_seekbar_bitrate"
|
||||
android:text="@string/suffix_seekbar_bitrate"
|
||||
android:title="@string/title_seekbar_bitrate" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_unlock_fps"
|
||||
android:title="@string/title_unlock_fps"
|
||||
android:summary="@string/summary_unlock_fps"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_stretch_video"
|
||||
android:title="@string/title_checkbox_stretch_video"
|
||||
@@ -37,7 +49,7 @@
|
||||
android:summary="@string/summary_checkbox_51_surround"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_gamepad_settings">
|
||||
<PreferenceCategory android:title="@string/category_input_settings">
|
||||
<!--com.limelight.preferences.SeekBarPreference
|
||||
android:key="seekbar_deadzone"
|
||||
android:defaultValue="15"
|
||||
@@ -49,6 +61,11 @@
|
||||
android:title="@string/title_checkbox_multi_controller"
|
||||
android:summary="@string/summary_checkbox_multi_controller"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_mouse_nav_buttons"
|
||||
android:title="@string/title_checkbox_mouse_nav_buttons"
|
||||
android:summary="@string/summary_checkbox_mouse_nav_buttons"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_usb_driver"
|
||||
android:title="@string/title_checkbox_xb1_driver"
|
||||
@@ -65,6 +82,11 @@
|
||||
android:title="@string/title_checkbox_mouse_emulation"
|
||||
android:summary="@string/summary_checkbox_mouse_emulation"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_vibrate_fallback"
|
||||
android:title="@string/title_checkbox_vibrate_fallback"
|
||||
android:summary="@string/summary_checkbox_vibrate_fallback"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
|
||||
android:key="category_onscreen_controls">
|
||||
@@ -73,6 +95,12 @@
|
||||
android:key="checkbox_show_onscreen_controls"
|
||||
android:summary="@string/summary_checkbox_show_onscreen_controls"
|
||||
android:title="@string/title_checkbox_show_onscreen_controls" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_vibrate_osc"
|
||||
android:dependency="checkbox_show_onscreen_controls"
|
||||
android:title="@string/title_checkbox_vibrate_osc"
|
||||
android:summary="@string/summary_checkbox_vibrate_osc"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:dependency="checkbox_show_onscreen_controls"
|
||||
@@ -121,6 +149,11 @@
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_advanced_settings"
|
||||
android:key="category_advanced_settings">
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_disable_warnings"
|
||||
android:title="@string/title_checkbox_disable_warnings"
|
||||
android:summary="@string/summary_checkbox_disable_warnings"
|
||||
android:defaultValue="false" />
|
||||
<ListPreference
|
||||
android:key="video_format"
|
||||
android:title="@string/title_video_format"
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
classpath 'com.android.tools.build:gradle:3.4.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#Tue May 08 18:56:31 PDT 2018
|
||||
#Fri Apr 26 18:29:34 PDT 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
||||
|
||||
+1
-1
Submodule moonlight-common updated: dd6e76c0f2...9f409da618
Reference in New Issue
Block a user