Compare commits

..

10 Commits

Author SHA1 Message Date
Cameron Gutman babfc99c35 Version 10.6 2022-07-07 23:17:08 -05:00
Cameron Gutman 1eca461cb1 Merge remote-tracking branch 'origin/weblate' 2022-07-04 17:51:36 -05:00
Cameron Gutman ebd327c7a6 Use new ShieldControllerExtensions library for Shield Controller rumble support
https://github.com/cgutman/ShieldControllerExtensions
2022-06-30 18:04:02 -05:00
Cameron Gutman 602febe876 Use onPictureInPictureRequested() to enter PiP on Android 11 2022-06-29 23:28:52 -05:00
Cameron Gutman 84fcd3ae6a Use requestMetaKeyEvent API on Samsung devices
Inspired by #1078
2022-06-28 22:07:40 -05:00
Cameron Gutman 84296c6e1c Toggle the IME with a 3 finger tap rather than only showing it 2022-06-28 21:40:59 -05:00
Jorys Paulin 6012e0ea8c Translated using Weblate (French)
Currently translated at 100.0% (219 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-06-27 15:16:50 +02:00
Cameron Gutman 9c76defad0 Add workaround for Galaxy S10 devices crashing during WifiLock acquisition 2022-06-26 13:59:39 -05:00
Cameron Gutman ffd6fab35c Prevent use of proxies 2022-06-25 14:18:38 -05:00
Artem 9cbef34f29 Translated using Weblate (Ukrainian)
Currently translated at 94.5% (207 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/uk/
2022-06-22 22:20:52 +02:00
11 changed files with 87 additions and 395 deletions
+3 -2
View File
@@ -9,8 +9,8 @@ android {
minSdk 16
targetSdk 33
versionName "10.5"
versionCode = 282
versionName "10.6"
versionCode = 283
// Generate native debug symbols to allow Google Play to symbolicate our native crashes
ndk.debugSymbolLevel = 'FULL'
@@ -130,4 +130,5 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:3.12.13'
implementation 'com.squareup.okio:okio:1.17.5'
implementation 'org.jmdns:jmdns:3.5.7'
implementation 'com.github.cgutman:ShieldControllerExtensions:1.0'
}
+66 -13
View File
@@ -78,6 +78,8 @@ import android.widget.TextView;
import android.widget.Toast;
import java.io.ByteArrayInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -265,14 +267,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
highPerfWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Moonlight High Perf Lock");
highPerfWifiLock.setReferenceCounted(false);
highPerfWifiLock.acquire();
try {
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();
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();
}
} catch (SecurityException e) {
// Some Samsung Galaxy S10+/S10e devices throw a SecurityException from
// WifiLock.acquire() even though we have android.permission.WAKE_LOCK in our manifest.
e.printStackTrace();
}
appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
@@ -591,13 +599,42 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
public void setMetaKeyCaptureState(boolean enabled) {
// This uses custom APIs present on some Samsung devices to allow capture of
// meta key events while streaming.
try {
Class<?> semWindowManager = Class.forName("com.samsung.android.view.SemWindowManager");
Method getInstanceMethod = semWindowManager.getMethod("getInstance");
Object manager = getInstanceMethod.invoke(null);
if (manager != null) {
Class<?>[] parameterTypes = new Class<?>[2];
parameterTypes[0] = String.class;
parameterTypes[1] = boolean.class;
Method requestMetaKeyEventMethod = semWindowManager.getDeclaredMethod("requestMetaKeyEvent", parameterTypes);
requestMetaKeyEventMethod.invoke(manager, this.getComponentName(), enabled);
}
else {
LimeLog.warning("SemWindowManager.getInstance() returned null");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public void onUserLeaveHint() {
super.onUserLeaveHint();
// PiP is only supported on Oreo and later, and we don't need to manually enter PiP on
// Android S and later.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Android S and later. On Android R, we will use onPictureInPictureRequested() instead.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (autoEnterPip) {
try {
// This has thrown all sorts of weird exceptions on Samsung devices
@@ -611,6 +648,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
@Override
@TargetApi(Build.VERSION_CODES.R)
public boolean onPictureInPictureRequested() {
// Enter PiP when requested unless we're on Android 12 which supports auto-enter.
if (autoEnterPip && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
enterPictureInPictureMode(getPictureInPictureParams(false));
}
return true;
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
@@ -1225,10 +1272,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@Override
public void showKeyboard() {
LimeLog.info("Showing keyboard overlay");
public void toggleKeyboard() {
LimeLog.info("Toggling keyboard overlay");
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
inputManager.toggleSoftInput(0, 0);
}
// Returns true if the event was consumed
@@ -1465,7 +1512,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// All fingers up
if (SystemClock.uptimeMillis() - threeFingerDownTime < THREE_FINGER_TAP_THRESHOLD) {
// This is a 3 finger tap to bring up the keyboard
showKeyboard();
toggleKeyboard();
return true;
}
}
@@ -1690,6 +1737,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
// Disable meta key capture
setMetaKeyCaptureState(false);
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe("Connection terminated: " + errorCode);
@@ -1801,6 +1851,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Keep the display on
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// Enable meta key capture
setMetaKeyCaptureState(true);
// Update GameManager state to indicate we're in game
UiHelper.notifyStreamConnected(Game.this);
@@ -25,7 +25,6 @@ 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.binding.input.shield.ShieldControllerExtensionsHandler;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
@@ -33,6 +32,8 @@ import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
import org.cgutman.shieldcontrollerextensions.SceManager;
import java.lang.reflect.InvocationTargetException;
import java.util.Timer;
import java.util.TimerTask;
@@ -62,7 +63,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
private final Vibrator deviceVibrator;
private final ShieldControllerExtensionsHandler shieldControllerExtensionsHandler;
private final SceManager sceManager;
private boolean hasGameController;
private final PreferenceConfiguration prefConfig;
@@ -74,7 +75,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
this.gestures = gestures;
this.prefConfig = prefConfig;
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
this.shieldControllerExtensionsHandler = new ShieldControllerExtensionsHandler(activityContext);
this.sceManager = new SceManager(activityContext);
this.sceManager.start();
int deadzonePercentage = prefConfig.deadzonePercentage;
@@ -203,7 +206,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
deviceContext.destroy();
}
shieldControllerExtensionsHandler.destroy();
sceManager.stop();
deviceVibrator.cancel();
}
@@ -1445,32 +1448,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (deviceContext.controllerNumber == controllerNumber) {
foundMatchingDevice = true;
// Cancel pending rumble repeat timer if one exists
if (deviceContext.rumbleRepeatTimer != null) {
deviceContext.rumbleRepeatTimer.cancel();
deviceContext.rumbleRepeatTimer = null;
}
// Prefer the documented Android 12 rumble API which can handle dual vibrators on PS/Xbox controllers
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && deviceContext.vibratorManager != null) {
vibrated = true;
rumbleDualVibrators(deviceContext.vibratorManager, lowFreqMotor, highFreqMotor);
}
// On Shield devices, we can use their special API to rumble Shield controllers
else if (shieldControllerExtensionsHandler.rumble(deviceContext.inputDevice, lowFreqMotor, highFreqMotor)) {
else if (sceManager.rumble(deviceContext.inputDevice, lowFreqMotor, highFreqMotor)) {
vibrated = true;
// The Shield controller can only rumble up to 1 second at a time, so we will call rumble again
// every 500 ms until the host PC gives us another rumble value.
if (lowFreqMotor != 0 || highFreqMotor != 0) {
deviceContext.rumbleRepeatTimer = new Timer("Rumble Repeat - "+deviceContext.name, true);
deviceContext.rumbleRepeatTimer.schedule(new TimerTask() {
@Override
public void run() {
shieldControllerExtensionsHandler.rumble(deviceContext.inputDevice, lowFreqMotor, highFreqMotor);
}
}, 500, 500);
}
}
// If all else fails, we have to try the old Vibrator API
else if (deviceContext.vibrator != null) {
@@ -1961,7 +1946,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public VibratorManager vibratorManager;
public Vibrator vibrator;
public InputDevice inputDevice;
public Timer rumbleRepeatTimer;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
@@ -2013,10 +1997,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
else if (vibrator != null) {
vibrator.cancel();
}
if (rumbleRepeatTimer != null) {
rumbleRepeatTimer.cancel();
}
}
}
@@ -1,51 +0,0 @@
package com.limelight.binding.input.shield;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
public interface IExposedControllerManagerListener extends IInterface {
void onDeviceAdded(String controllerToken);
void onDeviceChanged(String controllerToken, int i);
void onDeviceRemoved(String controllerToken);
public static abstract class Stub extends Binder implements IExposedControllerManagerListener {
public Stub() {
attachInterface(this, "com.nvidia.blakepairing.IExposedControllerManagerListener");
}
@Override
public IBinder asBinder() {
return this;
}
public boolean onTransact(int code, Parcel input, Parcel output, int flags) throws RemoteException {
switch (code) {
case 1:
input.enforceInterface("com.nvidia.blakepairing.IExposedControllerManagerListener");
onDeviceAdded(input.readString());
break;
case 2:
input.enforceInterface("com.nvidia.blakepairing.IExposedControllerManagerListener");
onDeviceChanged(input.readString(), input.readInt());
break;
case 3:
input.enforceInterface("com.nvidia.blakepairing.IExposedControllerManagerListener");
onDeviceRemoved(input.readString());
break;
case 4:
case 5:
input.enforceInterface("com.nvidia.blakepairing.IExposedControllerManagerListener");
// Don't care
break;
default:
return super.onTransact(code, input, output, flags);
}
return true;
}
}
}
@@ -1,298 +0,0 @@
package com.limelight.binding.input.shield;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.hardware.input.InputManager;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.view.InputDevice;
import com.limelight.LimeLog;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShieldControllerExtensionsHandler implements InputManager.InputDeviceListener {
private Context context;
private IBinder binder;
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
binder = iBinder;
try {
listenerId = registerListener();
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
listenerId = 0;
tokenToDeviceIdMap.clear();
deviceIdToTokenMap.clear();
binder = null;
}
};
// ConcurrentHashMap handles synchronization between the Binder thread adding/removing
// entries and callers on arbitrary threads that are doing device lookups.
//
// Since these are separate maps, they can be temporarily inconsistent (only one-way
// of the two-way mapping present). This is fine for our purposes here.
private ConcurrentHashMap<String, Integer> tokenToDeviceIdMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer, String> deviceIdToTokenMap = new ConcurrentHashMap<>();
private AtomicBoolean needsRefresh = new AtomicBoolean(false);
private int listenerId;
private IExposedControllerManagerListener.Stub controllerListener = new IExposedControllerManagerListener.Stub() {
@Override
public void onDeviceAdded(String controllerToken) {
try {
int inputDeviceId = getInputDeviceId(controllerToken);
LimeLog.info("Shield controller added: " + controllerToken + " -> " + inputDeviceId);
tokenToDeviceIdMap.put(controllerToken, inputDeviceId);
deviceIdToTokenMap.put(inputDeviceId, controllerToken);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onDeviceChanged(String controllerToken, int i) {
LimeLog.info("Shield controller changed: " + controllerToken + " " + i);
}
@Override
public void onDeviceRemoved(String controllerToken) {
LimeLog.info("Shield controller removed: " + controllerToken);
Integer deviceId = tokenToDeviceIdMap.remove(controllerToken);
if (deviceId != null) {
deviceIdToTokenMap.remove(deviceId);
}
}
};
public ShieldControllerExtensionsHandler(Context context) {
this.context = context;
InputManager inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(this, null);
Intent intent = new Intent();
intent.setClassName("com.nvidia.blakepairing", "com.nvidia.blakepairing.AccessoryService");
try {
// The docs say to call unbindService() even if the bindService() call returns false
// or throws a SecurityException.
if (!context.bindService(intent, serviceConnection, Service.BIND_AUTO_CREATE)) {
LimeLog.info("com.nvidia.blakepairing.AccessoryService is not available on this device");
context.unbindService(serviceConnection);
}
} catch (SecurityException e) {
context.unbindService(serviceConnection);
}
}
private String getControllerToken(InputDevice device) {
// Refresh device ID <-> token mappings if one of our devices was removed
if (needsRefresh.compareAndSet(true, false)) {
try {
LimeLog.info("Refreshing controller token mappings");
// We have to enumerate tokenToDeviceIdMap rather than deviceIdToTokenMap
// because we remove the deviceIdToTokenMap entry when the device goes away.
HashMap<String, Integer> newTokenToDeviceIdMap = new HashMap<>();
HashMap<Integer, String> newDeviceIdToTokenMap = new HashMap<>();
for (String existingToken : tokenToDeviceIdMap.keySet()) {
int deviceId = getInputDeviceId(existingToken);
if (deviceId != 0) {
newTokenToDeviceIdMap.put(existingToken, deviceId);
newDeviceIdToTokenMap.put(deviceId, existingToken);
}
}
tokenToDeviceIdMap.clear();
deviceIdToTokenMap.clear();
tokenToDeviceIdMap.putAll(newTokenToDeviceIdMap);
deviceIdToTokenMap.putAll(newDeviceIdToTokenMap);
} catch (RemoteException e) {
e.printStackTrace();
}
}
return deviceIdToTokenMap.get(device.getId());
}
public boolean rumble(InputDevice device, int lowFreqMotor, int highFreqMotor) {
String controllerToken = getControllerToken(device);
if (controllerToken != null) {
try {
return rumble(controllerToken, lowFreqMotor, highFreqMotor);
} catch (RemoteException e) {
e.printStackTrace();
}
}
return false;
}
public void destroy() {
InputManager inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(this);
tokenToDeviceIdMap.clear();
deviceIdToTokenMap.clear();
if (listenerId != 0) {
try {
unregisterListener(listenerId);
} catch (RemoteException e) {
e.printStackTrace();
}
listenerId = 0;
}
if (binder != null) {
context.unbindService(serviceConnection);
binder = null;
}
}
private int registerListener() throws RemoteException {
if (binder == null) {
return 0;
}
Parcel input = Parcel.obtain();
Parcel output = Parcel.obtain();
try {
input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder");
input.writeStrongBinder(controllerListener);
binder.transact(20, input, output, 0);
output.readException();
return output.readInt();
} finally {
input.recycle();
output.recycle();
}
}
private boolean unregisterListener(int listenerId) throws RemoteException {
if (binder == null) {
return false;
}
Parcel input = Parcel.obtain();
Parcel output = Parcel.obtain();
try {
input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder");
input.writeInt(listenerId);
binder.transact(21, input, output, 0);
output.readException();
return output.readInt() != 0;
} finally {
input.recycle();
output.recycle();
}
}
private int getInputDeviceId(String controllerToken) throws RemoteException {
if (binder == null) {
return 0;
}
Parcel input = Parcel.obtain();
Parcel output = Parcel.obtain();
try {
input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder");
input.writeString(controllerToken);
binder.transact(13, input, output, 0);
output.readException();
return output.readInt();
} finally {
input.recycle();
output.recycle();
}
}
// Rumble duration maximum of 1 second
private boolean rumble(String controllerToken, int lowFreqMotor, int highFreqMotor) throws RemoteException {
if (binder == null) {
return false;
}
Parcel input = Parcel.obtain();
Parcel output = Parcel.obtain();
try {
input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder");
input.writeString(controllerToken);
input.writeInt(lowFreqMotor);
input.writeInt(highFreqMotor);
binder.transact(18, input, output, 0);
output.readException();
return output.readInt() != 0;
} finally {
input.recycle();
output.recycle();
}
}
// Rumble duration maximum of 1.5 seconds
private boolean rumbleWithDuration(String controllerToken, int lowFreqMotor, int highFreqMotor, long durationMs) throws RemoteException {
if (binder == null) {
return false;
}
Parcel input = Parcel.obtain();
Parcel output = Parcel.obtain();
try {
input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder");
input.writeString(controllerToken);
input.writeInt(lowFreqMotor);
input.writeInt(highFreqMotor);
input.writeLong(durationMs);
binder.transact(19, input, output, 0);
output.readException();
return output.readInt() != 0;
} finally {
input.recycle();
output.recycle();
}
}
@Override
public void onInputDeviceAdded(int deviceId) {}
@Override
public void onInputDeviceChanged(int deviceId) {}
@Override
public void onInputDeviceRemoved(int deviceId) {
// Remove the device ID to token mapping, but leave the token mapping to device ID
// mapping so we will re-enumerate it when we next try to rumble a controller.
if (deviceIdToTokenMap.remove(deviceId) != null) {
needsRefresh.set(true);
}
}
}
@@ -9,6 +9,7 @@ import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.Proxy;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
@@ -171,6 +172,7 @@ public class NvHTTP {
.hostnameVerifier(hv)
.readTimeout(0, TimeUnit.MILLISECONDS)
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
.proxy(Proxy.NO_PROXY)
.build();
httpClientWithReadTimeout = httpClient.newBuilder()
@@ -1,5 +1,5 @@
package com.limelight.ui;
public interface GameGestures {
void showKeyboard();
void toggleKeyboard();
}
+1 -1
View File
@@ -228,7 +228,7 @@
<string name="audioconf_stereo">Stéréo</string>
<string name="audioconf_51surround">Son surround 5.1</string>
<string name="audioconf_71surround">Son surround 7.1</string>
<string name="videoformat_hevcauto">Utiliser HEVC uniquement s\'il est stable</string>
<string name="videoformat_hevcauto">Automatique</string>
<string name="videoformat_hevcalways">Utilisez toujours HEVC (mais il peut planter)</string>
<string name="videoformat_hevcnever">N\'utilisez jamais HEVC</string>
<string name="title_frame_pacing">Frame-pacing vidéo</string>
+1 -1
View File
@@ -152,7 +152,7 @@
<string name="title_checkbox_vibrate_fallback">Емуляція вібровіддачі</string>
<string name="summary_checkbox_vibrate_osc">Вібрація пристрою для емуляції вібровіддачі при екранному управлінні</string>
<string name="summary_checkbox_vibrate_fallback">Вібрує пристрій для емуляції вібровіддачі, якщо під\'єднаний контролер не підтримує її</string>
<string name="summary_checkbox_mouse_nav_buttons">Включення цієї опції може привести до неправильної роботи правої клавіші миші на деяких пристроях</string>
<string name="summary_checkbox_mouse_nav_buttons">Вмикання цієї опції може привести до неправильної роботи правої клавіші миші на деяких пристроях</string>
<string name="title_checkbox_flip_face_buttons">Перевернути ґудзики</string>
<string name="summary_checkbox_flip_face_buttons">Перемикає ґудзики A/B та X/Y для контролерів та екранних елементів керування</string>
<string name="scut_pc_not_found">Пристрій не знайдено</string>
+1
View File
@@ -13,5 +13,6 @@ allprojects {
repositories {
mavenCentral()
google()
maven { url 'https://jitpack.io' }
}
}
@@ -0,0 +1,4 @@
- 3 finger tap can now dismiss the keyboard too
- Fixed crash on some Samsung devices when starting to stream
- Added meta key handling for DeX on newer Samsung devices
- Updated community contributed translations from Weblate