mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
feat(input): pass held Back through as Steam QAM
This commit is contained in:
1 parent
53d643dd00
commit
dde1a55d00
8 files changed
+285
-3
No files matched your search
@@ -257,6 +257,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private val cursorResync = Runnable { if (syncClientCursor() && clientHidesCursor) cursorVisible = false }
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
private var pendingBackAction: Runnable? = null
|
||||
private val backQamInput = com.droiddeck.launcher.input.BackQamInput { padBridge?.setBackQamPressed(it) }
|
||||
private var drawerDirectionKey = KeyEvent.KEYCODE_UNKNOWN
|
||||
private var drawerDirectionDownTime = 0L
|
||||
private var drawerDirectionDeviceId = -1
|
||||
@@ -446,6 +447,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
override fun onInputDeviceRemoved(deviceId: Int) {
|
||||
Log.i(TAG, "input device $deviceId disconnected")
|
||||
backQamInput.onDeviceRemoved(deviceId)
|
||||
joyConVr?.refresh()
|
||||
padBridge?.onDeviceRemoved(deviceId)
|
||||
if (::padMouse.isInitialized) padMouse.reset()
|
||||
@@ -773,6 +775,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
LaunchedEffect(drawerOpen, pcKeyboardOpen, achievementsOpen, gestureSettingsOpen, pipUi, SessionState.suspended) {
|
||||
if (drawerOpen || pcKeyboardOpen || achievementsOpen || gestureSettingsOpen || pipUi || SessionState.suspended) {
|
||||
backQamInput.cancel()
|
||||
cancelTouchGestures()
|
||||
screenKeys?.stopEditing()
|
||||
}
|
||||
@@ -2106,6 +2109,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun releasePads() {
|
||||
backQamInput.cancel()
|
||||
padBridge?.releaseAll()
|
||||
joyConVr?.releaseButtons()
|
||||
}
|
||||
@@ -2184,6 +2188,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE ->
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
val backToQam = !pipUi && !screenKeyTextFocus && !drawerOpen && !pcKeyboardOpen && !achievementsOpen &&
|
||||
SessionState.mode == SessionService.MODE_STEAM && SessionState.running && SessionState.firstFrameSeen &&
|
||||
!SessionState.suspended && padBridge?.canHoldQam() == true
|
||||
if (backQamInput.onKeyEvent(event, backToQam)) {
|
||||
pendingBackAction?.let(uiHandler::removeCallbacks)
|
||||
pendingBackAction = null
|
||||
return true
|
||||
}
|
||||
if (pipUi) return true
|
||||
if (screenKeyTextFocus) return super.dispatchKeyEvent(event)
|
||||
val pad = com.droiddeck.launcher.input.JoyCon.normalize(event)
|
||||
@@ -3253,6 +3265,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onPause() {
|
||||
if (runtimeRemovalBlocked) { super.onPause(); return }
|
||||
backQamInput.cancel()
|
||||
capturePausedFrame()
|
||||
sessionClipboard.stop()
|
||||
if (::touchscreenGestures.isInitialized) touchscreenGestures.cancel()
|
||||
@@ -3636,6 +3649,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (!hasFocus) backQamInput.cancel()
|
||||
if (!hasFocus && ::padMouse.isInitialized) padMouse.reset()
|
||||
if (runtimeRemovalBlocked) return
|
||||
if (!hasFocus) {
|
||||
@@ -3683,6 +3697,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
backQamInput.cancel()
|
||||
steamVrRecovery.close()
|
||||
uiHandler.removeCallbacks(steamTouchRetry)
|
||||
if (runtimeRemovalBlocked) { super.onDestroy(); return }
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.view.KeyEvent
|
||||
|
||||
class BackQamInput(private val setPressed: (Boolean) -> Unit) {
|
||||
private val held = mutableSetOf<Int>()
|
||||
private val cancelled = mutableSetOf<Int>()
|
||||
|
||||
fun onKeyEvent(event: KeyEvent, enabled: Boolean): Boolean {
|
||||
if (!enabled) cancel()
|
||||
if (event.keyCode != KeyEvent.KEYCODE_BACK) return false
|
||||
val device = event.deviceId
|
||||
return when (event.action) {
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
if (event.repeatCount > 0) return enabled || device in held || device in cancelled
|
||||
cancelled.remove(device)
|
||||
if (!enabled || event.isCanceled) return false
|
||||
if (held.add(device) && held.size == 1) setPressed(true)
|
||||
true
|
||||
}
|
||||
KeyEvent.ACTION_UP -> {
|
||||
val released = held.remove(device)
|
||||
val wasCancelled = cancelled.remove(device)
|
||||
if (released && held.isEmpty()) setPressed(false)
|
||||
released || wasCancelled || enabled
|
||||
}
|
||||
else -> device in held || device in cancelled
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
if (held.isEmpty()) return
|
||||
cancelled.addAll(held)
|
||||
held.clear()
|
||||
setPressed(false)
|
||||
}
|
||||
|
||||
fun onDeviceRemoved(device: Int) {
|
||||
cancelled.remove(device)
|
||||
if (held.remove(device) && held.isEmpty()) setPressed(false)
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ public final class PadBridge {
|
||||
private final PadState routedTouch = new PadState();
|
||||
private boolean systemGuidePressed;
|
||||
private boolean systemQamPressed;
|
||||
private boolean backQamPressed;
|
||||
private boolean qamChordActive;
|
||||
private int chordButton = -1;
|
||||
private boolean chordButtonPressed;
|
||||
@@ -132,6 +133,7 @@ public final class PadBridge {
|
||||
routedTouch.clear();
|
||||
systemGuidePressed = false;
|
||||
systemQamPressed = false;
|
||||
backQamPressed = false;
|
||||
qamTapPressed = false;
|
||||
qamChordActive = false;
|
||||
chordButtonPressed = false;
|
||||
@@ -165,6 +167,7 @@ public final class PadBridge {
|
||||
}
|
||||
systemGuidePressed = false;
|
||||
systemQamPressed = false;
|
||||
backQamPressed = false;
|
||||
qamChordActive = false;
|
||||
chordButtonPressed = false;
|
||||
qamTapPressed = false;
|
||||
@@ -459,6 +462,7 @@ public final class PadBridge {
|
||||
|
||||
public synchronized void releaseAll() {
|
||||
routedTouch.clear();
|
||||
backQamPressed = false;
|
||||
releaseBackPaddles();
|
||||
state.clear();
|
||||
publish();
|
||||
@@ -479,6 +483,17 @@ public final class PadBridge {
|
||||
return SessionState.getDeckPad() && FakeInputWriter.padHasQam();
|
||||
}
|
||||
|
||||
public boolean canHoldQam() {
|
||||
return builtinPad && nativeQam();
|
||||
}
|
||||
|
||||
public synchronized void setBackQamPressed(boolean pressed) {
|
||||
if (pressed && !canHoldQam()) return;
|
||||
if (backQamPressed == pressed) return;
|
||||
backQamPressed = pressed;
|
||||
publish();
|
||||
}
|
||||
|
||||
public synchronized void triggerQam() {
|
||||
if (!builtinPad) return;
|
||||
if (!nativeQam()) {
|
||||
@@ -684,7 +699,7 @@ public final class PadBridge {
|
||||
return;
|
||||
}
|
||||
boolean guide = systemGuidePressed || qamChordActive;
|
||||
boolean qam = nativeQam() && (systemQamPressed || qamTapPressed);
|
||||
boolean qam = nativeQam() && (systemQamPressed || qamTapPressed || backQamPressed);
|
||||
if (!guide && !qam) {
|
||||
writer.writePad(state);
|
||||
return;
|
||||
|
||||
@@ -306,7 +306,9 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras:
|
||||
stringResource(R.string.unfolded_controls_with_controller_hint),
|
||||
s.unfoldedControlsWithController, onChange = a.onUnfoldedControlsWithController,
|
||||
)
|
||||
if (steam) ChoiceRow(
|
||||
if (steam && s.steamController in listOf(SessionPrefs.CONTROLLER_DECK, SessionPrefs.CONTROLLER_STEAM)) {
|
||||
Note(stringResource(R.string.back_native_qam_hint))
|
||||
} else if (steam) ChoiceRow(
|
||||
host, "back-actions", stringResource(R.string.mode_back), stringResource(SessionPrefs.backActionsOrder(s.backActionsInverted)),
|
||||
listOf(
|
||||
false to stringResource(SessionPrefs.BACK_MENU_THEN_QAM),
|
||||
|
||||
@@ -604,7 +604,9 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
chipModifier = focus.track(page, "keyboard-button"), onChange = a.onKeyboardButton)
|
||||
}
|
||||
}
|
||||
if (a.steam) ChoiceRow(host, "back-actions", stringResource(R.string.mode_back), null,
|
||||
if (a.steam && a.padType in listOf(SessionPrefs.CONTROLLER_DECK, SessionPrefs.CONTROLLER_STEAM)) {
|
||||
Note(stringResource(R.string.back_native_qam_hint))
|
||||
} else if (a.steam) ChoiceRow(host, "back-actions", stringResource(R.string.mode_back), null,
|
||||
listOf(false to stringResource(SessionPrefs.BACK_MENU_THEN_QAM), true to stringResource(SessionPrefs.BACK_QAM_THEN_MENU)),
|
||||
a.backActionsInverted, chipModifier = focus.track(page, "back-actions"), onPick = a.onBackActionsInverted)
|
||||
if (a.steam && a.unfoldedControls != null) ToggleRow(host, "unfolded-controls", stringResource(R.string.unfolded_controls),
|
||||
|
||||
@@ -306,6 +306,7 @@
|
||||
<string name="mode_controller_x360">Xbox 360 controller</string>
|
||||
<string name="mode_controller_note">Deck: Quick Access button and gyro. Steam Controller: the same, with grip sense on the back buttons. DualShock 4 and DualSense: gyro, and the trackpads as their touchpad. Xbox 360: open Quick Access with Guide+A.</string>
|
||||
<string name="mode_back">Back</string>
|
||||
<string name="back_native_qam_hint">Back acts as Quick Access. Hold it with another button for Steam shortcuts.</string>
|
||||
<string name="back_menu_then_qam">Menu → QAM</string>
|
||||
<string name="back_qam_then_menu">QAM → Menu</string>
|
||||
<string name="mode_session">Session</string>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.view.KeyEvent
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], manifest = Config.NONE)
|
||||
class BackQamInputTest {
|
||||
private val changes = mutableListOf<Boolean>()
|
||||
private val input = BackQamInput(changes::add)
|
||||
|
||||
@Test fun aHoldAndItsRepeatsProduceOnePressAndOneRelease() {
|
||||
assertTrue(send(KeyEvent.ACTION_DOWN))
|
||||
for (repeat in 1..10) assertTrue(send(KeyEvent.ACTION_DOWN, repeat = repeat))
|
||||
assertEquals(listOf(true), changes)
|
||||
assertTrue(send(KeyEvent.ACTION_UP))
|
||||
assertEquals(listOf(true, false), changes)
|
||||
}
|
||||
|
||||
@Test fun otherButtonsAndSelectPassThroughWhileBackIsHeld() {
|
||||
send(KeyEvent.ACTION_DOWN)
|
||||
for (code in listOf(KeyEvent.KEYCODE_BUTTON_X, KeyEvent.KEYCODE_BUTTON_SELECT)) {
|
||||
for (action in listOf(KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP)) {
|
||||
assertFalse(input.onKeyEvent(KeyEvent(action, code), true))
|
||||
}
|
||||
}
|
||||
assertEquals(listOf(true), changes)
|
||||
send(KeyEvent.ACTION_UP)
|
||||
assertEquals(listOf(true, false), changes)
|
||||
}
|
||||
|
||||
@Test fun disabledRoutingPreservesBackNavigation() {
|
||||
assertFalse(send(KeyEvent.ACTION_DOWN, enabled = false))
|
||||
assertFalse(send(KeyEvent.ACTION_UP, enabled = false))
|
||||
assertTrue(changes.isEmpty())
|
||||
}
|
||||
|
||||
@Test fun losingFocusReleasesQamAndConsumesTheLateRelease() {
|
||||
send(KeyEvent.ACTION_DOWN)
|
||||
input.cancel()
|
||||
input.cancel()
|
||||
assertTrue(send(KeyEvent.ACTION_DOWN, repeat = 1, enabled = false))
|
||||
assertTrue(send(KeyEvent.ACTION_UP, enabled = false))
|
||||
assertEquals(listOf(true, false), changes)
|
||||
assertFalse(send(KeyEvent.ACTION_UP, enabled = false))
|
||||
}
|
||||
|
||||
@Test fun openingAnOverlayReleasesQamBeforeRoutingItsNextKey() {
|
||||
send(KeyEvent.ACTION_DOWN)
|
||||
assertFalse(input.onKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BUTTON_B), false))
|
||||
assertTrue(send(KeyEvent.ACTION_UP, enabled = false))
|
||||
assertEquals(listOf(true, false), changes)
|
||||
}
|
||||
|
||||
@Test fun repeatsAfterCancellationCannotReassertQam() {
|
||||
send(KeyEvent.ACTION_DOWN)
|
||||
input.cancel()
|
||||
assertTrue(send(KeyEvent.ACTION_DOWN, repeat = 1))
|
||||
assertEquals(listOf(true, false), changes)
|
||||
assertTrue(send(KeyEvent.ACTION_DOWN))
|
||||
assertTrue(send(KeyEvent.ACTION_UP))
|
||||
assertEquals(listOf(true, false, true, false), changes)
|
||||
}
|
||||
|
||||
@Test fun cancelledReleaseStillReleasesQam() {
|
||||
send(KeyEvent.ACTION_DOWN)
|
||||
assertTrue(input.onKeyEvent(KeyEvent(0, 100, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK,
|
||||
0, 0, 4, 0, KeyEvent.FLAG_CANCELED), true))
|
||||
assertEquals(listOf(true, false), changes)
|
||||
}
|
||||
|
||||
@Test fun removingOneDeviceDoesNotReleaseAnotherDevicesHold() {
|
||||
send(KeyEvent.ACTION_DOWN, device = 4)
|
||||
send(KeyEvent.ACTION_DOWN, device = 5)
|
||||
input.onDeviceRemoved(4)
|
||||
assertTrue(changes.last())
|
||||
send(KeyEvent.ACTION_UP, device = 5)
|
||||
assertFalse(changes.last())
|
||||
send(KeyEvent.ACTION_DOWN, device = 4)
|
||||
input.onDeviceRemoved(4)
|
||||
assertFalse(changes.last())
|
||||
}
|
||||
|
||||
private fun send(action: Int, repeat: Int = 0, device: Int = 4, enabled: Boolean = true) =
|
||||
input.onKeyEvent(KeyEvent(0, 100, action, KeyEvent.KEYCODE_BACK, repeat, 0, device, 0), enabled)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.os.Handler
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import java.nio.file.Files
|
||||
import java.time.Duration
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.util.ReflectionHelpers
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], manifest = Config.NONE, instrumentedPackages = ["com.droiddeck.launcher.input.FakeInputWriter"],
|
||||
shadows = [JoyConPadTest.RecordingWriter::class])
|
||||
class PadBridgeQamTest {
|
||||
private val directory = Files.createTempDirectory("qam-pad").toFile()
|
||||
private lateinit var bridge: PadBridge
|
||||
|
||||
@Before fun start() {
|
||||
JoyConPadTest.RecordingWriter.pads.clear()
|
||||
ReflectionHelpers.setStaticField(FakeInputWriter::class.java, "padType", FakeInputWriter.PAD_TYPE_DECK)
|
||||
SessionState.deckPad = true
|
||||
bridge = PadBridge(directory)
|
||||
assertTrue(bridge.start())
|
||||
}
|
||||
|
||||
@After fun stop() {
|
||||
bridge.stop()
|
||||
SessionState.deckPad = false
|
||||
directory.deleteRecursively()
|
||||
}
|
||||
|
||||
@Test fun onlyNativeQamControllersAcceptAHeldBack() {
|
||||
for (type in listOf(-1, FakeInputWriter.PAD_TYPE_DECK, FakeInputWriter.PAD_TYPE_STEAM,
|
||||
FakeInputWriter.PAD_TYPE_XBOX, FakeInputWriter.PAD_TYPE_DS4, FakeInputWriter.PAD_TYPE_DUALSENSE)) {
|
||||
ReflectionHelpers.setStaticField(FakeInputWriter::class.java, "padType", type)
|
||||
val supported = type <= FakeInputWriter.PAD_TYPE_STEAM
|
||||
assertEquals(supported, bridge.canHoldQam())
|
||||
bridge.setBackQamPressed(true)
|
||||
bridge.applyExternal({}, false)
|
||||
assertEquals(supported, pad().isDown(PadState.QAM))
|
||||
bridge.setBackQamPressed(false)
|
||||
}
|
||||
SessionState.deckPad = false
|
||||
assertFalse(bridge.canHoldQam())
|
||||
}
|
||||
|
||||
@Test fun qamAndOtherButtonsShareAReportUntilBackIsReleased() {
|
||||
bridge.setBackQamPressed(true)
|
||||
advanceTime()
|
||||
bridge.applyExternal({ it.press(PadState.X, true); it.press(PadState.SELECT, true) }, true)
|
||||
assertTrue(pad().isDown(PadState.QAM))
|
||||
assertTrue(pad().isDown(PadState.X))
|
||||
assertTrue(pad().isDown(PadState.SELECT))
|
||||
bridge.setBackQamPressed(false)
|
||||
assertFalse(pad().isDown(PadState.QAM))
|
||||
assertTrue(pad().isDown(PadState.X))
|
||||
assertTrue(pad().isDown(PadState.SELECT))
|
||||
}
|
||||
|
||||
@Test fun touchQamAndBackHaveIndependentReleases() {
|
||||
bridge.setBackQamPressed(true)
|
||||
bridge.setSystemButtons(false, true)
|
||||
bridge.setBackQamPressed(false)
|
||||
assertTrue(pad().isDown(PadState.QAM))
|
||||
bridge.setBackQamPressed(true)
|
||||
bridge.setSystemButtons(false, false)
|
||||
assertTrue(pad().isDown(PadState.QAM))
|
||||
bridge.setBackQamPressed(false)
|
||||
assertFalse(pad().isDown(PadState.QAM))
|
||||
}
|
||||
|
||||
@Test fun aTimedQamTapCannotReleaseAHeldBack() {
|
||||
bridge.triggerQam()
|
||||
bridge.setBackQamPressed(true)
|
||||
advanceTime()
|
||||
assertTrue(pad().isDown(PadState.QAM))
|
||||
bridge.setBackQamPressed(false)
|
||||
assertFalse(pad().isDown(PadState.QAM))
|
||||
}
|
||||
|
||||
@Test fun releasingAllOrStoppingClearsTheHold() {
|
||||
bridge.setBackQamPressed(true)
|
||||
bridge.releaseAll()
|
||||
assertFalse(pad().isDown(PadState.QAM))
|
||||
bridge.setBackQamPressed(true)
|
||||
bridge.stop()
|
||||
bridge.start()
|
||||
bridge.applyExternal({}, false)
|
||||
assertFalse(pad().isDown(PadState.QAM))
|
||||
}
|
||||
|
||||
@Test fun disablingTheBuiltinPadClearsAndRejectsTheHold() {
|
||||
bridge.setBackQamPressed(true)
|
||||
bridge.setBuiltinPad(false)
|
||||
assertFalse(bridge.canHoldQam())
|
||||
assertFalse(pad().isDown(PadState.QAM))
|
||||
bridge.setBackQamPressed(true)
|
||||
bridge.setBuiltinPad(true)
|
||||
bridge.applyExternal({}, false)
|
||||
assertFalse(pad().isDown(PadState.QAM))
|
||||
}
|
||||
|
||||
private fun pad() = requireNotNull(JoyConPadTest.RecordingWriter.pads[0])
|
||||
|
||||
private fun advanceTime() {
|
||||
val handler = ReflectionHelpers.getStaticField<Handler>(PadBridge::class.java, "chordHandler")
|
||||
shadowOf(handler.looper).idleFor(Duration.ofSeconds(2))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user