Compare commits

...

62 Commits

Author SHA1 Message Date
Cameron Gutman deb78e1c64 Version 7.4 2019-06-05 23:02:06 -07:00
Cameron Gutman 9aec6b1d31 Target API 29 2019-06-05 22:59:39 -07:00
Cameron Gutman 97702b8861 Fix mouse capture after returning focus to the window on Android Q 2019-06-05 22:43:16 -07:00
Cameron Gutman 832e7197c5 Delay a bit before reporting USB devices to allow the old InputDevice to go away 2019-06-05 22:26:06 -07:00
Cameron Gutman 26b992726c Use transparent status bar and navigation bar on Android Q 2019-06-05 21:50:03 -07:00
Cameron Gutman 1cb3588841 Use low latency WifiLock on Android Q 2019-06-05 21:09:55 -07:00
Cameron Gutman b461d546d6 Use new MediaCodecInfo helper to blacklist software codecs 2019-06-05 21:05:33 -07:00
Cameron Gutman b7810d6eb6 Use the newly public InputDevice.isExternal() function on Android Q 2019-06-05 20:23:22 -07:00
Cameron Gutman 6fb3a8e57d Build with the Android Q SDK 2019-06-05 20:21:19 -07:00
Cameron Gutman b521c784bc Version 7.3.1 2019-05-27 01:48:00 -07:00
twboyii 8e1641af5f Add untranslated string in zh-rTW (#701) 2019-05-27 01:47:10 -07:00
Cameron Gutman c0aac01d33 Update AGP to 3.4.1 2019-05-27 01:43:38 -07:00
Cameron Gutman 4f8b0adcbb Fix video on GFE 3.19 2019-05-27 01:42:39 -07:00
Cameron Gutman 393a4c9c8a Fix pointer capture on Android Q Beta 3 2019-05-16 21:27:01 -07:00
Cameron Gutman 99b53f9a6a Version 7.3 2019-05-07 20:53:12 -07:00
Cameron Gutman 8da563b280 Bound queued audio data to prevent excessive latency 2019-05-07 20:39:45 -07:00
Cameron Gutman d5b950e5cf Version 7.2.1 2019-05-01 20:14:21 -07:00
Cameron Gutman c46b9acf6b Update common to fix receive time 2019-04-30 23:19:19 -07:00
Cameron Gutman d8e322bac9 Sync PC offline icon with Moonlight Qt 2019-04-30 22:27:22 -07:00
Cameron Gutman 44871626cf Version 7.2 2019-04-27 22:11:02 -07:00
Cameron Gutman f661522b5d Update moonlight-common with additional perf improvements 2019-04-27 22:00:27 -07:00
Cameron Gutman a454b0ab78 Update moonlight-common with perf improvements 2019-04-26 18:37:27 -07:00
Cameron Gutman 75bf84d0d9 Update Gradle for AS 3.4 2019-04-26 18:34:16 -07:00
Cameron Gutman c248994ed4 Version 7.1 2019-04-07 14:09:58 -07:00
Cameron Gutman a7a34ec629 Update vibration weights to match Moonlight Qt 2019-04-06 01:02:03 -07:00
Cameron Gutman 8d469c5d0a Add on-screen connection warnings 2019-04-06 00:56:45 -07:00
Cameron Gutman e6979d50b5 Update AGP to 3.3.2 2019-04-06 00:48:40 -07:00
Cameron Gutman 6e25b135a3 Update ProGuard rules to avoid slf4j warnings 2019-03-20 18:57:40 -07:00
Cameron Gutman 04e093a2c2 Update moonlight-common 2019-03-20 18:51:08 -07:00
bubuleur 813f2edd95 Update French Language (#676) 2019-03-02 20:01:21 -08:00
Cameron Gutman 337d753a33 Reduce gamepad deadzone to 7% 2019-03-02 17:23:01 -08:00
Cameron Gutman 1137c74f76 Pass AudioAttributes on L+ when vibrating 2019-03-02 17:20:39 -08:00
Cameron Gutman 0c1451f757 Improve scaling of lock icon by increasing dimensions 2019-02-18 20:46:34 -08:00
Cameron Gutman 5ab9ea48fd Version 7.0.1 2019-02-17 18:20:40 -08:00
Cameron Gutman ffcb623040 Fix crash when a rumble effect only uses the high-frequency motor 2019-02-17 18:18:00 -08:00
Cameron Gutman bfe6929642 Version 7.0 2019-02-16 19:44:45 -08:00
Cameron Gutman 50d45011a8 Add device vibration and other fixes 2019-02-16 19:13:01 -08:00
Cameron Gutman 2f7087d6d3 Stop vibration on stream end 2019-02-16 18:05:08 -08:00
Cameron Gutman 92b71588d0 Implement rumble on Android InputDevice 2019-02-16 17:56:34 -08:00
Cameron Gutman 4f3d018764 Fix OSC colliding with player 1 2019-02-16 17:29:05 -08:00
Cameron Gutman a22e33eeb9 Add rumble support for the in-app Xbox driver 2019-02-16 17:03:10 -08:00
Cameron Gutman 6a939e7495 Don't display the termination dialog for intended terminations 2019-02-10 02:28:11 -08:00
Cameron Gutman f8ba7cf190 Update common with SOPS fixes 2019-02-09 20:59:59 -08:00
Cameron Gutman d1e135db4d Version 6.2 2019-02-06 22:10:29 -08:00
Cameron Gutman 61a17afe69 Fix *, @, #, and + keys on software keyboard 2019-02-06 21:40:28 -08:00
Cameron Gutman 47fd691884 Update to AGP 3.3.1 2019-02-06 21:14:50 -08:00
Cameron Gutman 0d171c6b28 Fix lock icon drawing on top of the loading spinner 2019-02-06 21:14:01 -08:00
Cameron Gutman f0c69d08b8 Add 480p option 2019-02-06 21:09:04 -08:00
Cameron Gutman 629bf5766d Fix a couple crash reports 2019-02-05 22:51:48 -08:00
Cameron Gutman 233bceeece Update common for GFE 3.17 2019-02-05 22:10:11 -08:00
Cameron Gutman 6660ea7d91 Update Xbox driver with Linux xpad.c and init quirks 2019-02-05 21:52:53 -08:00
Cameron Gutman 4864b2ca45 Add lock icon when PC is unpaired 2019-02-05 21:10:09 -08:00
Cameron Gutman 92097b318d Update Gradle and AGP 2019-02-05 20:58:49 -08:00
Cameron Gutman 997898c99d Version 6.1.3 2019-01-04 18:20:28 -08:00
Cameron Gutman 1174e03885 Fix incorrectly persisting host with missing server cert 2019-01-04 18:18:32 -08:00
Cameron Gutman ff0f54d541 Switch to using stun.moonlight-stream.org for STUN 2019-01-04 18:05:28 -08:00
Cameron Gutman 814964a100 Fix exception adding PCs 2019-01-01 23:32:16 -08:00
Cameron Gutman 7e154292a9 Stop suppressing exceptions 2019-01-01 23:31:38 -08:00
Cameron Gutman 0f9cba1053 Fix crash due to a null computer uuid 2019-01-01 22:34:27 -08:00
Cameron Gutman a4e134589d Version 6.1.1 2018-12-27 23:58:30 -08:00
Cameron Gutman cd80a94f28 Fix IllegalStateException caused by making HTTPS request without a pinned cert 2018-12-27 23:55:59 -08:00
Cameron Gutman 57c645a291 Change uuid field to String type due to new format UUIDs that fail to parse on GFE 3.16 2018-12-27 23:48:12 -08:00
43 changed files with 732 additions and 243 deletions
+5 -5
View File
@@ -1,15 +1,15 @@
apply plugin: 'com.android.application'
android {
buildToolsVersion '28.0.3'
compileSdkVersion 28
buildToolsVersion '29.0.0'
compileSdkVersion 29
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
targetSdkVersion 29
versionName "6.1"
versionCode = 182
versionName "7.4"
versionCode = 195
}
flavorDimensions "root"
+1
View File
@@ -25,3 +25,4 @@
# jMDNS
-dontwarn javax.jmdns.impl.DNSCache
-dontwarn org.slf4j.**
+1
View File
@@ -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" />
+10 -7
View File
@@ -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;
@@ -82,7 +83,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
localBinder.waitForReady();
// Get the computer object
computer = localBinder.getComputer(UUID.fromString(uuidString));
computer = localBinder.getComputer(uuidString);
if (computer == null) {
finish();
return;
@@ -156,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;
}
@@ -180,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
@@ -216,7 +217,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
}
});
@@ -280,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();
+104 -16
View File
@@ -66,12 +66,14 @@ 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,
@@ -110,13 +112,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean grabbedInput = true;
private boolean grabComboDown = false;
private StreamView streamView;
private TextView notificationOverlayView;
private ShortcutHelper shortcutHelper;
private MediaCodecDecoderRenderer decoderRenderer;
private boolean reportedCrash;
private WifiManager.WifiLock wifiLock;
private WifiManager.WifiLock highPerfWifiLock;
private WifiManager.WifiLock lowLatencyWifiLock;
private boolean connectedToUsbDriverService = false;
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
@@ -201,6 +205,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) {
@@ -223,9 +229,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
wifiLock.setReferenceCounted(false);
wifiLock.acquire();
highPerfWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Moonlight High Perf Lock");
highPerfWifiLock.setReferenceCounted(false);
highPerfWifiLock.acquire();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
lowLatencyWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Moonlight Low Latency Lock");
lowLatencyWifiLock.setReferenceCounted(false);
lowLatencyWifiLock.acquire();
}
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
@@ -424,7 +436,7 @@ 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();
@@ -505,8 +517,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Capture is lost when focus is lost, so it must be requested again
// when focus is regained.
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
// Recapture the pointer if focus was regained
streamView.requestPointerCapture();
// Recapture the pointer if focus was regained. On Android Q,
// we have to delay a bit before requesting capture because otherwise
// we'll hit the "requestPointerCapture called for a window that has no focus"
// error and it will not actually capture the cursor.
Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
streamView.requestPointerCapture();
}
}, 500);
}
}
}
@@ -697,7 +718,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
inputManager.unregisterInputDeviceListener(controllerHandler);
}
wifiLock.release();
if (lowLatencyWifiLock != null) {
lowLatencyWifiLock.release();
}
if (highPerfWifiLock != null) {
highPerfWifiLock.release();
}
if (connectedToUsbDriverService) {
// Unbind from the discovery service
@@ -908,7 +934,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;
@@ -959,7 +990,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;
@@ -1255,6 +1293,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,
@@ -1310,8 +1350,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);
}
}
});
@@ -1330,10 +1403,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
connected = true;
connecting = false;
// Hide the mouse cursor now. Doing it before
// dismissing the spinner seems to be undone
// when the spinner gets displayed.
inputCaptureProvider.enableCapture();
// Hide the mouse cursor now after a short delay.
// Doing it before dismissing the spinner seems to be undone
// when the spinner gets displayed. On Android Q, even now
// is too early to capture. We will delay a second to allow
// the spinner to dismiss before capturing.
Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
inputCaptureProvider.enableCapture();
}
}, 500);
// Keep the display on
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -1365,6 +1446,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) {
+8 -5
View File
@@ -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;
@@ -420,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();
}
@@ -521,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;
@@ -548,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);
}
@@ -631,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);
@@ -662,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;
@@ -311,17 +335,23 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
private static boolean isExternal(InputDevice dev) {
try {
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
return dev.isExternal();
}
else {
try {
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
}
}
// Answer true if we don't know
@@ -393,6 +423,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 +648,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 +1077,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 +1416,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 +1474,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 +1515,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 +1553,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);
}
}
@@ -30,6 +30,18 @@ public abstract class AbstractXboxController extends AbstractController {
private Thread createInputThread() {
return new Thread() {
public void run() {
try {
// Delay for a moment before reporting the new gamepad and
// accepting new input. This allows time for the old InputDevice
// to go away before we reclaim its spot. If the old device is still
// around when we call notifyDeviceAdded(), we won't be able to claim
// the controller number used by the original InputDevice.
Thread.sleep(1000);
} catch (InterruptedException e) {}
// Report that we're added _before_ reporting input
notifyDeviceAdded();
while (!isInterrupted() && !stopped) {
byte[] buffer = new byte[64];
@@ -114,9 +126,6 @@ public abstract class AbstractXboxController extends AbstractController {
return false;
}
// Report that we're added _before_ starting the input thread
notifyDeviceAdded();
// Start listening for controller input
inputThread = createInputThread();
inputThread.start();
@@ -131,6 +140,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;
}
}
}
@@ -13,6 +13,7 @@ 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;
@@ -38,7 +39,7 @@ 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;
@@ -53,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;
@@ -167,26 +168,22 @@ public class VirtualController {
}
void sendControllerInputContext() {
try {
_DBG("INPUT_MAP + " + inputContext.inputMap);
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
_DBG("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);
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
);
}
}
}
@@ -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
@@ -474,6 +474,24 @@ public class MediaCodecHelper {
return null;
}
private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) {
// Use the new isSoftwareOnly() function on Android Q
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (codecInfo.isSoftwareOnly()) {
LimeLog.info("Skipping software-only decoder: "+codecInfo.getName());
return true;
}
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
return true;
}
return false;
}
public static MediaCodecInfo findFirstDecoder(String mimeType) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
@@ -482,15 +500,14 @@ public class MediaCodecHelper {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports the specified video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase(mimeType)) {
// Skip blacklisted codecs
if (isCodecBlacklisted(codecInfo)) {
continue;
}
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
@@ -530,17 +547,16 @@ public class MediaCodecHelper {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports the requested video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase(mimeType)) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
// Skip blacklisted codecs
if (isCodecBlacklisted(codecInfo)) {
//continue;
}
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
if (requiredProfile != -1) {
@@ -1,7 +1,6 @@
package com.limelight.computers;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@@ -9,9 +8,7 @@ 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;
@@ -79,7 +76,7 @@ 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);
@@ -102,14 +99,7 @@ public class ComputerDatabaseManager {
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);
@@ -152,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;
@@ -241,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)) {
@@ -253,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)) {
@@ -360,13 +360,14 @@ 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);
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
// 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) {
@@ -377,10 +378,9 @@ public class ComputerManagerService extends Service {
}
}
if (fakeDetails.serverCert != null) {
// Poll again with the pinned cert to get accurate pairing information
runPoll(fakeDetails, true, 0);
}
// 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;
@@ -457,8 +457,12 @@ public class ComputerManagerService extends Service {
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;
@@ -468,7 +472,8 @@ public class ComputerManagerService extends Service {
newDetails.activeAddress = address;
return newDetails;
} catch (Exception e) {
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
return null;
}
}
@@ -697,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);
}
@@ -738,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();
@@ -770,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) {
@@ -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;
@@ -35,6 +35,8 @@ public class PreferenceConfiguration {
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";
static final String DEFAULT_RESOLUTION = "720p";
static final String DEFAULT_FPS = "60";
@@ -58,6 +60,8 @@ public class PreferenceConfiguration {
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,11 +83,16 @@ public class PreferenceConfiguration {
public boolean mouseEmulation;
public boolean mouseNavButtons;
public boolean unlockFps;
public boolean vibrateOsc;
public boolean vibrateFallbackToDevice;
private static int getHeightFromResolutionString(String resString) {
if (resString.equalsIgnoreCase("360p")) {
return 360;
}
else if (resString.equalsIgnoreCase("480p")) {
return 480;
}
else if (resString.equalsIgnoreCase("720p")) {
return 720;
}
@@ -103,13 +112,22 @@ public class PreferenceConfiguration {
}
private static int getWidthFromResolutionString(String resString) {
return (getHeightFromResolutionString(resString) * 16) / 9;
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";
@@ -139,6 +157,9 @@ public class PreferenceConfiguration {
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));
@@ -314,6 +335,8 @@ public class PreferenceConfiguration {
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;
}
@@ -13,7 +13,10 @@ 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;
@@ -30,7 +33,7 @@ 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_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) {
@@ -84,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) {
+9
View File
@@ -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>
+4 -8
View File
@@ -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>
+12
View File
@@ -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>
+5
View File
@@ -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>
@@ -133,6 +134,8 @@
<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>
@@ -147,6 +150,8 @@
<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>
+6
View File
@@ -0,0 +1,6 @@
<resources>
<style name="AppBaseTheme" parent="android:Theme.Material">
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
+32 -5
View File
@@ -97,27 +97,51 @@
<string name="category_basic_settings"> 基本設置 </string>
<string name="title_resolution_list"> 選擇目標解析度和幀數 </string>
<string name="summary_resolution_list"> 過高的設定會引起串流卡頓甚至軟體閃退 </string>
<string name="title_seekbar_bitrate"> 選擇目標視頻碼率 </string>
<string name="title_fps_list">影像幀數</string>
<string name="summary_fps_list">增加以提供更流暢的影像串流. 減少以在較低的配備上獲得較好的效能.</string>
<string name="title_seekbar_bitrate"> 選擇目標影像碼率 </string>
<string name="summary_seekbar_bitrate"> 低碼率減少卡頓,高碼率提高畫質 </string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_unlock_fps">解鎖所有可使用的幀率</string>
<string name="summary_unlock_fps">串流於90或120幀下可以減少高級配備的延遲, 但可能導致無法支持的配備出現延遲或崩潰</string>
<string name="title_checkbox_stretch_video"> 將畫面拉伸至全屏 </string>
<string name="title_checkbox_disable_warnings"> 禁用錯誤提示 </string>
<string name="summary_checkbox_disable_warnings"> 串流過程中禁用連接錯誤提示 </string>
<string name="title_checkbox_enable_pip">啟用子母畫面</string>
<string name="summary_checkbox_enable_pip">當多工處理時, 允許觀看串流畫面(無法控制)</string>
<string name="category_audio_settings"> 音頻設置 </string>
<string name="title_checkbox_51_surround"> 啟用 5.1 環繞音效 </string>
<string name="summary_checkbox_51_surround"> 如果你的聲音聽起來有問題請禁用。\n\n需要GeForce Experience 2.7 或更高版本 </string>
<string name="category_input_settings">輸入設定</string>
<string name="title_checkbox_multi_controller"> 啟用多手柄支持 </string>
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄將會認作一個手柄 </string>
<string name="title_seekbar_deadzone"> 調整搖桿死區 </string>
<string name="title_checkbox_vibrate_fallback">以手機模擬手柄震動</string>
<string name="summary_checkbox_vibrate_fallback">當手柄不支援震動時, 以手機來模擬</string>
<string name="title_seekbar_deadzone"> 調整手柄死區 </string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One 手柄驅動 </string>
<string name="summary_checkbox_xb1_driver"> 若要在那些沒有原生Xbox手柄驅動的設備上使用Xbox手柄,請勾上此複選框 </string>
<string name="title_checkbox_usb_bind_all">覆蓋Android控制器支援</string>
<string name="summary_checkbox_usb_bind_all">強制 Moonlight USB驅動程式接管所有支持的Xbox手柄</string>
<string name="title_checkbox_mouse_emulation">透過手柄模擬滑鼠</string>
<string name="summary_checkbox_mouse_emulation">長按 [Start] 鍵將會切換至滑鼠模式</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">設置 </string>
<string name="category_on_screen_controls_settings">設置 </string>
<string name="title_checkbox_show_onscreen_controls"> 啟用虛擬手柄 </string>
<string name="summary_checkbox_show_onscreen_controls"> 將在串流畫面上顯示一層虛擬手柄 </string>
<string name="title_checkbox_vibrate_osc">啟用震動</string>
<string name="summary_checkbox_vibrate_osc">在手機上模擬搖桿震動</string>
<string name="title_only_l3r3">只顯示[L3]及[R3]</string>
<string name="summary_only_l3r3">隱藏所有虛擬按鈕除了L3和R3</string>
<string name="title_reset_osc">重設已經保存的觸控按鈕布局</string>
<string name="summary_reset_osc">將所有觸控按鈕重設為默認之大小和位置</string>
<string name="dialog_title_reset_osc">重設按鈕布局</string>
<string name="dialog_text_reset_osc">你確定要刪除所保存的按鈕布局嗎?</string>
<string name="toast_reset_osc_success">按鈕布局已經重設</string>
<string name="category_ui_settings"> 界面設置 </string>
<string name="title_language_list">語言</string>
@@ -134,7 +158,10 @@
<string name="summary_checkbox_host_audio"> 將在電腦和本設備同時輸出聲音 </string>
<string name="category_advanced_settings"> 高級設置 </string>
<string name="title_disable_frame_drop">永遠不掉幀</string>
<string name="summary_disable_frame_drop">在一些手機上可能可以減少micro-stuttering, 但可能導致延遲</string>
<string name="title_video_format"> H.265設置 </string>
<string name="summary_video_format">H.265能降低寬需求,但是需要設備支持 </string>
<string name="summary_video_format">H.265能降低寬需求,但是需要設備支持 </string>
<string name="title_enable_hdr">啟用 HDR (實驗中)</string>
<string name="summary_enable_hdr">啟用 HDR 當遊戲與電腦的顯示卡支援時. HDR 需要 GTX 1000 系列之顯示卡或更高.</string>
</resources>
+2
View File
@@ -2,6 +2,7 @@
<resources>
<string-array name="resolution_names">
<item>360p</item>
<item>480p</item>
<item>720p</item>
<item>1080p</item>
<item>1440p</item>
@@ -9,6 +10,7 @@
</string-array>
<string-array name="resolution_values" translatable="false">
<item>360p</item>
<item>480p</item>
<item>720p</item>
<item>1080p</item>
<item>1440p</item>
+6
View File
@@ -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>
@@ -136,6 +138,8 @@
<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>
@@ -150,6 +154,8 @@
<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>
+16
View File
@@ -82,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">
@@ -90,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"
@@ -138,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
View File
@@ -5,7 +5,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.android.tools.build:gradle:3.4.1'
}
}
+2 -2
View File
@@ -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