From cc877480ff0102c61ffdcd8cf7a65579605c1b49 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 11 May 2020 23:53:33 -0700 Subject: [PATCH] Add an option for absolute touch mode --- app/src/main/java/com/limelight/Game.java | 26 +- .../input/touch/AbsoluteTouchContext.java | 272 ++++++++++++++++++ .../RelativeTouchContext.java} | 26 +- .../binding/input/touch/TouchContext.java | 11 + .../com/limelight/nvstream/NvConnection.java | 6 + .../limelight/nvstream/jni/MoonBridge.java | 2 + .../preferences/PreferenceConfiguration.java | 8 +- .../limelight/preferences/StreamSettings.java | 14 +- .../jni/moonlight-core/moonlight-common-c | 2 +- app/src/main/jni/moonlight-core/simplejni.c | 5 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 8 +- 12 files changed, 361 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/limelight/binding/input/touch/AbsoluteTouchContext.java rename app/src/main/java/com/limelight/binding/input/{TouchContext.java => touch/RelativeTouchContext.java} (90%) create mode 100644 app/src/main/java/com/limelight/binding/input/touch/TouchContext.java diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 331bb7c3..568d07b9 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -6,9 +6,11 @@ import com.limelight.binding.input.ControllerHandler; import com.limelight.binding.input.KeyboardTranslator; import com.limelight.binding.input.capture.InputCaptureManager; import com.limelight.binding.input.capture.InputCaptureProvider; -import com.limelight.binding.input.TouchContext; +import com.limelight.binding.input.touch.AbsoluteTouchContext; +import com.limelight.binding.input.touch.RelativeTouchContext; import com.limelight.binding.input.driver.UsbDriverService; import com.limelight.binding.input.evdev.EvdevListener; +import com.limelight.binding.input.touch.TouchContext; import com.limelight.binding.input.virtual_controller.VirtualController; import com.limelight.binding.video.CrashListener; import com.limelight.binding.video.MediaCodecDecoderRenderer; @@ -474,9 +476,14 @@ public class Game extends Activity implements SurfaceHolder.Callback, // Initialize touch contexts for (int i = 0; i < touchContextMap.length; i++) { - touchContextMap[i] = new TouchContext(conn, i, - REFERENCE_HORIZ_RES, REFERENCE_VERT_RES, - streamView); + if (!prefConfig.touchscreenTrackpad) { + touchContextMap[i] = new AbsoluteTouchContext(conn, i, streamView); + } + else { + touchContextMap[i] = new RelativeTouchContext(conn, i, + REFERENCE_HORIZ_RES, REFERENCE_VERT_RES, + streamView); + } } // Use sustained performance mode on N+ to ensure consistent @@ -1332,7 +1339,10 @@ public class Game extends Activity implements SurfaceHolder.Callback, { case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_DOWN: - context.touchDownEvent(eventX, eventY); + for (TouchContext touchContext : touchContextMap) { + touchContext.setPointerCount(event.getPointerCount()); + } + context.touchDownEvent(eventX, eventY, true); break; case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_UP: @@ -1345,9 +1355,12 @@ public class Game extends Activity implements SurfaceHolder.Callback, } } context.touchUpEvent(eventX, eventY); + for (TouchContext touchContext : touchContextMap) { + touchContext.setPointerCount(event.getPointerCount() - 1); + } if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) { // The original secondary touch now becomes primary - context.touchDownEvent((int)event.getX(1), (int)event.getY(1)); + context.touchDownEvent((int)event.getX(1), (int)event.getY(1), false); } break; case MotionEvent.ACTION_MOVE: @@ -1379,6 +1392,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, case MotionEvent.ACTION_CANCEL: for (TouchContext aTouchContext : touchContextMap) { aTouchContext.cancelTouch(); + aTouchContext.setPointerCount(0); } break; default: diff --git a/app/src/main/java/com/limelight/binding/input/touch/AbsoluteTouchContext.java b/app/src/main/java/com/limelight/binding/input/touch/AbsoluteTouchContext.java new file mode 100644 index 00000000..df4b4bb2 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/touch/AbsoluteTouchContext.java @@ -0,0 +1,272 @@ +package com.limelight.binding.input.touch; + +import android.os.SystemClock; +import android.view.View; + +import com.limelight.nvstream.NvConnection; +import com.limelight.nvstream.input.MouseButtonPacket; + +import java.util.Timer; +import java.util.TimerTask; + +public class AbsoluteTouchContext implements TouchContext { + private int lastTouchDownX = 0; + private int lastTouchDownY = 0; + private long lastTouchDownTime = 0; + private int lastTouchUpX = 0; + private int lastTouchUpY = 0; + private long lastTouchUpTime = 0; + private int lastTouchLocationX = 0; + private int lastTouchLocationY = 0; + private boolean cancelled; + private boolean confirmedLongPress; + private boolean confirmedTap; + private Timer longPressTimer; + private Timer tapDownTimer; + private float accumulatedScrollDelta; + + private final NvConnection conn; + private final int actionIndex; + private final View targetView; + + private static final int SCROLL_SPEED_DIVISOR = 20; + + private static final int LONG_PRESS_TIME_THRESHOLD = 650; + private static final int LONG_PRESS_DISTANCE_THRESHOLD = 30; + + private static final int DOUBLE_TAP_TIME_THRESHOLD = 250; + private static final int DOUBLE_TAP_DISTANCE_THRESHOLD = 60; + + private static final int TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD = 100; + private static final int TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD = 20; + + public AbsoluteTouchContext(NvConnection conn, int actionIndex, View view) + { + this.conn = conn; + this.actionIndex = actionIndex; + this.targetView = view; + } + + @Override + public int getActionIndex() + { + return actionIndex; + } + + @Override + public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger) + { + if (!isNewFinger) { + // We don't handle finger transitions for absolute mode + return true; + } + + lastTouchLocationX = lastTouchDownX = eventX; + lastTouchLocationY = lastTouchDownY = eventY; + lastTouchDownTime = SystemClock.uptimeMillis(); + cancelled = confirmedTap = confirmedLongPress = false; + accumulatedScrollDelta = 0; + + if (actionIndex == 0) { + // Start the timers + startTapDownTimer(); + startLongPressTimer(); + } + + return true; + } + + private boolean distanceExceeds(int deltaX, int deltaY, double limit) { + return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) > limit; + } + + private void updatePosition(int eventX, int eventY) { + // We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT. + // Normalize these to the view size. We can't just drop them because we won't always get an event + // right at the boundary of the view, so dropping them would result in our cursor never really + // reaching the sides of the screen. + eventX = Math.min(Math.max(eventX, 0), targetView.getWidth()); + eventY = Math.min(Math.max(eventY, 0), targetView.getHeight()); + + conn.sendMousePosition((short)eventX, (short)eventY, (short)targetView.getWidth(), (short)targetView.getHeight()); + } + + @Override + public void touchUpEvent(int eventX, int eventY) + { + if (cancelled) { + return; + } + + if (actionIndex == 0) { + // Cancel the timers + cancelLongPressTimer(); + cancelTapDownTimer(); + + // Raise the mouse buttons that we currently have down + if (confirmedLongPress) { + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT); + } + else if (confirmedTap) { + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT); + } + else { + // If we get here, this means that the tap completed within the touch down + // deadzone time. We'll need to send the touch down and up events now at the + // original touch down position. + tapConfirmed(); + try { + // FIXME: Sleeping on the main thread sucks + Thread.sleep(50); + } catch (InterruptedException ignored) {} + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT); + } + } + + lastTouchLocationX = lastTouchUpX = eventX; + lastTouchLocationY = lastTouchUpY = eventY; + lastTouchUpTime = SystemClock.uptimeMillis(); + } + + private synchronized void startLongPressTimer() { + longPressTimer = new Timer(true); + longPressTimer.schedule(new TimerTask() { + @Override + public void run() { + synchronized (AbsoluteTouchContext.this) { + // Check if someone cancelled us + if (longPressTimer == null) { + return; + } + + // Uncancellable now + longPressTimer = null; + + // This timer should have already expired, but cancel it just in case + cancelTapDownTimer(); + + // Switch from a left click to a right click after a long press + confirmedLongPress = true; + if (confirmedTap) { + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT); + } + conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT); + } + } + }, LONG_PRESS_TIME_THRESHOLD); + } + + private synchronized void cancelLongPressTimer() { + if (longPressTimer != null) { + longPressTimer.cancel(); + longPressTimer = null; + } + } + + private synchronized void startTapDownTimer() { + tapDownTimer = new Timer(true); + tapDownTimer.schedule(new TimerTask() { + @Override + public void run() { + synchronized (AbsoluteTouchContext.this) { + // Check if someone cancelled us + if (tapDownTimer == null) { + return; + } + + // Uncancellable now + tapDownTimer = null; + + // Start our tap + tapConfirmed(); + } + } + }, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD); + } + + private synchronized void cancelTapDownTimer() { + if (tapDownTimer != null) { + tapDownTimer.cancel(); + tapDownTimer = null; + } + } + + private void tapConfirmed() { + if (confirmedTap || confirmedLongPress) { + return; + } + + confirmedTap = true; + cancelTapDownTimer(); + + // Left button down at original position + if (lastTouchDownTime - lastTouchUpTime > DOUBLE_TAP_TIME_THRESHOLD || + distanceExceeds(lastTouchDownX - lastTouchUpX, lastTouchDownY - lastTouchUpY, DOUBLE_TAP_DISTANCE_THRESHOLD)) { + // Don't reposition for finger down events within the deadzone. This makes double-clicking easier. + updatePosition(lastTouchDownX, lastTouchDownY); + } + conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT); + } + + @Override + public boolean touchMoveEvent(int eventX, int eventY) + { + if (cancelled) { + return true; + } + + if (actionIndex == 0) { + if (distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, LONG_PRESS_DISTANCE_THRESHOLD)) { + // Moved too far since touch down. Cancel the long press timer. + cancelLongPressTimer(); + } + + // Ignore motion within the deadzone period after touch down + if (confirmedTap || distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD)) { + tapConfirmed(); + updatePosition(eventX, eventY); + } + } + else if (actionIndex == 1) { + accumulatedScrollDelta += (eventY - lastTouchLocationY) / (float)SCROLL_SPEED_DIVISOR; + if ((short)accumulatedScrollDelta != 0) { + conn.sendMouseHighResScroll((short)accumulatedScrollDelta); + accumulatedScrollDelta -= (short)accumulatedScrollDelta; + } + } + + lastTouchLocationX = eventX; + lastTouchLocationY = eventY; + + return true; + } + + @Override + public void cancelTouch() { + cancelled = true; + + // Cancel the timers + cancelLongPressTimer(); + cancelTapDownTimer(); + + // Raise the mouse buttons + if (confirmedLongPress) { + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT); + } + else if (confirmedTap) { + conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT); + } + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setPointerCount(int pointerCount) { + if (actionIndex == 0 && pointerCount > 1) { + cancelTouch(); + } + } +} diff --git a/app/src/main/java/com/limelight/binding/input/TouchContext.java b/app/src/main/java/com/limelight/binding/input/touch/RelativeTouchContext.java similarity index 90% rename from app/src/main/java/com/limelight/binding/input/TouchContext.java rename to app/src/main/java/com/limelight/binding/input/touch/RelativeTouchContext.java index 6c7cc59a..be0d8a1b 100644 --- a/app/src/main/java/com/limelight/binding/input/TouchContext.java +++ b/app/src/main/java/com/limelight/binding/input/touch/RelativeTouchContext.java @@ -1,5 +1,6 @@ -package com.limelight.binding.input; +package com.limelight.binding.input.touch; +import android.os.SystemClock; import android.view.View; import com.limelight.nvstream.NvConnection; @@ -8,7 +9,7 @@ import com.limelight.nvstream.input.MouseButtonPacket; import java.util.Timer; import java.util.TimerTask; -public class TouchContext { +public class RelativeTouchContext implements TouchContext { private int lastTouchX = 0; private int lastTouchY = 0; private int originalTouchX = 0; @@ -32,8 +33,8 @@ public class TouchContext { private static final int TAP_TIME_THRESHOLD = 250; private static final int DRAG_TIME_THRESHOLD = 650; - public TouchContext(NvConnection conn, int actionIndex, - int referenceWidth, int referenceHeight, View view) + public RelativeTouchContext(NvConnection conn, int actionIndex, + int referenceWidth, int referenceHeight, View view) { this.conn = conn; this.actionIndex = actionIndex; @@ -42,6 +43,7 @@ public class TouchContext { this.targetView = view; } + @Override public int getActionIndex() { return actionIndex; @@ -57,7 +59,7 @@ public class TouchContext { private boolean isTap() { - long timeDelta = System.currentTimeMillis() - originalTouchTime; + long timeDelta = SystemClock.uptimeMillis() - originalTouchTime; return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD; } @@ -72,7 +74,8 @@ public class TouchContext { } } - public boolean touchDownEvent(int eventX, int eventY) + @Override + public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger) { // Get the view dimensions to scale inputs on this touch xFactor = referenceWidth / (double)targetView.getWidth(); @@ -80,7 +83,7 @@ public class TouchContext { originalTouchX = lastTouchX = eventX; originalTouchY = lastTouchY = eventY; - originalTouchTime = System.currentTimeMillis(); + originalTouchTime = SystemClock.uptimeMillis(); cancelled = confirmedDrag = confirmedMove = false; distanceMoved = 0; @@ -92,6 +95,7 @@ public class TouchContext { return true; } + @Override public void touchUpEvent(int eventX, int eventY) { if (cancelled) { @@ -128,7 +132,7 @@ public class TouchContext { dragTimer.schedule(new TimerTask() { @Override public void run() { - synchronized (TouchContext.this) { + synchronized (RelativeTouchContext.this) { // Check if someone already set move if (confirmedMove) { return; @@ -179,6 +183,7 @@ public class TouchContext { } } + @Override public boolean touchMoveEvent(int eventX, int eventY) { if (eventX != lastTouchX || eventY != lastTouchY) @@ -223,6 +228,7 @@ public class TouchContext { return true; } + @Override public void cancelTouch() { cancelled = true; @@ -235,7 +241,11 @@ public class TouchContext { } } + @Override public boolean isCancelled() { return cancelled; } + + @Override + public void setPointerCount(int pointerCount) {} } diff --git a/app/src/main/java/com/limelight/binding/input/touch/TouchContext.java b/app/src/main/java/com/limelight/binding/input/touch/TouchContext.java new file mode 100644 index 00000000..d08ab3c6 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/touch/TouchContext.java @@ -0,0 +1,11 @@ +package com.limelight.binding.input.touch; + +public interface TouchContext { + int getActionIndex(); + void setPointerCount(int pointerCount); + boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger); + boolean touchMoveEvent(int eventX, int eventY); + void touchUpEvent(int eventX, int eventY); + void cancelTouch(); + boolean isCancelled(); +} diff --git a/app/src/main/java/com/limelight/nvstream/NvConnection.java b/app/src/main/java/com/limelight/nvstream/NvConnection.java index 82ec2b05..5b67b0b7 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnection.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnection.java @@ -348,6 +348,12 @@ public class NvConnection { } } + public void sendMouseHighResScroll(final short scrollAmount) { + if (!isMonkey) { + MoonBridge.sendMouseHighResScroll(scrollAmount); + } + } + public static String findExternalAddressForMdns(String stunHostname, int stunPort) { return MoonBridge.findExternalAddressIP4(stunHostname, stunPort); } diff --git a/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java b/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java index fb4c041e..71c6807e 100644 --- a/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java +++ b/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java @@ -261,6 +261,8 @@ public class MoonBridge { public static native void sendMouseScroll(byte scrollClicks); + public static native void sendMouseHighResScroll(short scrollAmount); + public static native String getStageName(int stage); public static native String findExternalAddressIP4(String stunHostName, int stunPort); diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index 4991c9e1..692a9bea 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -41,7 +41,8 @@ public class PreferenceConfiguration { static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps"; private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc"; private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback"; - private static final String FLIP_FACE_BUTTONS_PERF_STRING = "checkbox_flip_face_buttons"; + private static final String FLIP_FACE_BUTTONS_PREF_STRING = "checkbox_flip_face_buttons"; + private static final String TOUCHSCREEN_TRACKPAD_PREF_STRING = "checkbox_touchscreen_trackpad"; static final String DEFAULT_RESOLUTION = "720p"; static final String DEFAULT_FPS = "60"; @@ -69,6 +70,7 @@ public class PreferenceConfiguration { private static final boolean DEFAULT_VIBRATE_OSC = true; private static final boolean DEFAULT_VIBRATE_FALLBACK = false; private static final boolean DEFAULT_FLIP_FACE_BUTTONS = false; + private static final boolean DEFAULT_TOUCHSCREEN_TRACKPAD = true; private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo public static final int FORCE_H265_ON = -1; @@ -95,6 +97,7 @@ public class PreferenceConfiguration { public boolean unlockFps; public boolean vibrateOsc; public boolean vibrateFallbackToDevice; + public boolean touchscreenTrackpad; public MoonBridge.AudioConfiguration audioConfiguration; private static int getHeightFromResolutionString(String resString) { @@ -370,7 +373,8 @@ public class PreferenceConfiguration { 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); - config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PERF_STRING, DEFAULT_FLIP_FACE_BUTTONS); + config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PREF_STRING, DEFAULT_FLIP_FACE_BUTTONS); + config.touchscreenTrackpad = prefs.getBoolean(TOUCHSCREEN_TRACKPAD_PREF_STRING, DEFAULT_TOUCHSCREEN_TRACKPAD); return config; } diff --git a/app/src/main/java/com/limelight/preferences/StreamSettings.java b/app/src/main/java/com/limelight/preferences/StreamSettings.java index 3237f1d2..5f33453d 100644 --- a/app/src/main/java/com/limelight/preferences/StreamSettings.java +++ b/app/src/main/java/com/limelight/preferences/StreamSettings.java @@ -141,9 +141,17 @@ public class StreamSettings extends Activity { // hide on-screen controls category on non touch screen devices if (!getActivity().getPackageManager(). hasSystemFeature("android.hardware.touchscreen")) { - PreferenceCategory category = - (PreferenceCategory) findPreference("category_onscreen_controls"); - screen.removePreference(category); + { + PreferenceCategory category = + (PreferenceCategory) findPreference("category_onscreen_controls"); + screen.removePreference(category); + } + + { + PreferenceCategory category = + (PreferenceCategory) findPreference("category_input_settings"); + category.removePreference(findPreference("checkbox_touchscreen_trackpad")); + } } // Remove PiP mode on devices pre-Oreo diff --git a/app/src/main/jni/moonlight-core/moonlight-common-c b/app/src/main/jni/moonlight-core/moonlight-common-c index f596e805..c8faf353 160000 --- a/app/src/main/jni/moonlight-core/moonlight-common-c +++ b/app/src/main/jni/moonlight-core/moonlight-common-c @@ -1 +1 @@ -Subproject commit f596e805755cf0b83c14375bc5c9aa182b1766e0 +Subproject commit c8faf3539b0c33b9e5eb8f5cf99c04d307c0e608 diff --git a/app/src/main/jni/moonlight-core/simplejni.c b/app/src/main/jni/moonlight-core/simplejni.c index f427e341..574b72f4 100644 --- a/app/src/main/jni/moonlight-core/simplejni.c +++ b/app/src/main/jni/moonlight-core/simplejni.c @@ -49,6 +49,11 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendMouseScroll(JNIEnv *env, jclass c LiSendScrollEvent(scrollClicks); } +JNIEXPORT void JNICALL +Java_com_limelight_nvstream_jni_MoonBridge_sendMouseHighResScroll(JNIEnv *env, jclass clazz, jshort scrollAmount) { + LiSendHighResScrollEvent(scrollAmount); +} + JNIEXPORT void JNICALL Java_com_limelight_nvstream_jni_MoonBridge_stopConnection(JNIEnv *env, jclass clazz) { LiStopConnection(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 77653c63..8120b14f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -140,6 +140,8 @@ Enable 5.1 or 7.1 surround sound for home-theater systems Input Settings + Use the touchscreen as a trackpad + If enabled, the touchscreen acts like a trackpad. If disabled, the touchscreen directly controls the mouse cursor. Automatic gamepad presence detection Unchecking this option forces a gamepad to always be present Emulate rumble support with vibration diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index c9271a11..8646da56 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -51,13 +51,19 @@ android:entryValues="@array/audio_config_values" android:defaultValue="2" /> - + +