Skip to content

Commit 05c81ae

Browse files
committed
Migrate PC mouse repeat to PC protocol
- Parse PC repeat capabilities and send repeat start/stop commands - Mirror PC-side repeat state for switch-stop behavior - Move PC repeat settings source of truth out of Android 🤖 Auto-generated
1 parent 5a638d8 commit 05c81ae

8 files changed

Lines changed: 215 additions & 63 deletions

File tree

app/src/main/java/com/enaboapps/switchify/pc/PcConnector.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ sealed class PcPingResult {
1616
sealed class PcControlCommand {
1717
data class Move(val dx: Int, val dy: Int) : PcControlCommand()
1818
data class Scroll(val dx: Int, val dy: Int) : PcControlCommand()
19+
data class RepeatStart(val command: PcControlCommand) : PcControlCommand()
20+
data object RepeatStop : PcControlCommand()
1921
data class DragStart(val button: String = "left") : PcControlCommand()
2022
data class DragEnd(val button: String = "left") : PcControlCommand()
2123
data class TypeText(val text: String) : PcControlCommand()

app/src/main/java/com/enaboapps/switchify/pc/PcMouseRepeatManager.kt

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class PcMouseRepeatManager internal constructor(
1919
private var repeatedCommand: PcControlCommand? = null
2020
private var repeatArmed = false
2121
private var pausedForReconnect = false
22+
private var pcSideRepeatActive = false
23+
private var pcSideRepeatPending = false
24+
private var stopPcSideRepeat: (suspend () -> PcCommandResult)? = null
25+
private var pcSideScope: CoroutineScope? = null
2226

2327
companion object {
2428
internal const val RECONNECT_GRACE_MS = 5_000L
@@ -30,7 +34,7 @@ class PcMouseRepeatManager internal constructor(
3034
}
3135

3236
fun init(context: Context) {
33-
settings = PreferencePcMouseRepeatSettings(context.applicationContext)
37+
settings = DefaultPcMouseRepeatSettings
3438
}
3539

3640
fun canRepeat(command: PcControlCommand): Boolean {
@@ -47,6 +51,43 @@ class PcMouseRepeatManager internal constructor(
4751
return true
4852
}
4953

54+
fun armPcSideRepeat(
55+
command: PcControlCommand,
56+
scope: CoroutineScope,
57+
stopRepeatedCommand: suspend () -> PcCommandResult
58+
): Boolean {
59+
if (!isRepeatable(command)) return false
60+
61+
stop(showMessage = false)
62+
pcSideRepeatPending = true
63+
pcSideRepeatActive = false
64+
pcSideScope = scope
65+
stopPcSideRepeat = stopRepeatedCommand
66+
repeatedCommand = command
67+
repeatArmed = true
68+
showMessage(R.string.pc_mouse_repeat_started, MessageSeverity.Success)
69+
return true
70+
}
71+
72+
fun confirmPcSideStarted(command: PcControlCommand): Boolean {
73+
if (!pcSideRepeatPending || repeatedCommand != command) return false
74+
75+
pcSideRepeatPending = false
76+
pcSideRepeatActive = true
77+
repeatArmed = true
78+
return true
79+
}
80+
81+
fun cancelPcSidePending(showMessage: Boolean = false): Boolean {
82+
if (!pcSideRepeatPending && !pcSideRepeatActive) return false
83+
84+
clearPcSideRepeatState()
85+
if (showMessage) {
86+
showMessage(R.string.pc_mouse_repeat_stopped, MessageSeverity.Info)
87+
}
88+
return true
89+
}
90+
5091
fun startAfterInitialSend(
5192
command: PcControlCommand,
5293
scope: CoroutineScope,
@@ -159,10 +200,28 @@ class PcMouseRepeatManager internal constructor(
159200
return true
160201
}
161202

162-
fun stopForSwitchPress(): Boolean = stop()
203+
fun stopForSwitchPress(): Boolean {
204+
if (stopPcSideForSwitchPress()) return true
205+
return stop()
206+
}
207+
208+
private fun stopPcSideForSwitchPress(): Boolean {
209+
if (!pcSideRepeatPending && !pcSideRepeatActive) return false
210+
211+
val stop = stopPcSideRepeat
212+
val scope = pcSideScope
213+
clearPcSideRepeatState()
214+
showMessage(R.string.pc_mouse_repeat_stopped, MessageSeverity.Info)
215+
if (stop != null && scope != null) {
216+
scope.launch {
217+
stop()
218+
}
219+
}
220+
return true
221+
}
163222

164223
fun isRepeating(): Boolean {
165-
return repeatArmed && repeatedCommand != null
224+
return (repeatArmed && repeatedCommand != null) || pcSideRepeatPending || pcSideRepeatActive
166225
}
167226

168227
fun clearServiceState(showMessage: Boolean = false) {
@@ -207,6 +266,20 @@ class PcMouseRepeatManager internal constructor(
207266
repeatedCommand = null
208267
repeatArmed = false
209268
pausedForReconnect = false
269+
clearPcSideState()
270+
}
271+
272+
private fun clearPcSideRepeatState() {
273+
repeatedCommand = null
274+
repeatArmed = false
275+
clearPcSideState()
276+
}
277+
278+
private fun clearPcSideState() {
279+
pcSideRepeatActive = false
280+
pcSideRepeatPending = false
281+
stopPcSideRepeat = null
282+
pcSideScope = null
210283
}
211284

212285
private fun showMessage(messageResId: Int, severity: MessageSeverity) {
@@ -221,6 +294,7 @@ class PcMouseRepeatManager internal constructor(
221294
repeatedCommand = null
222295
repeatArmed = false
223296
pausedForReconnect = false
297+
clearPcSideState()
224298
settings = null
225299
showHudMessage = defaultHudMessageHandler()
226300
}
@@ -234,6 +308,11 @@ class PcMouseRepeatManager internal constructor(
234308
}
235309
}
236310

311+
private object DefaultPcMouseRepeatSettings : PcMouseRepeatSettings {
312+
override fun isEnabled(): Boolean = true
313+
override fun intervalMs(): Long = PcMouseRepeatDefaults.DEFAULT_INTERVAL_MS
314+
}
315+
237316
private fun defaultHudMessageHandler(): (Int, MessageSeverity) -> Unit = { messageResId, severity ->
238317
ServiceMessageHUD.instance.showMessage(
239318
messageResId,

app/src/main/java/com/enaboapps/switchify/pc/PcPointerProfile.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ data class PcPointerMovementProfile(
1212
data class PcPointerCapabilities(
1313
val noAckMouseMove: Boolean = false,
1414
val noAckCommands: Set<String> = emptySet(),
15-
val supportedCommands: Set<String> = emptySet()
15+
val supportedCommands: Set<String> = emptySet(),
16+
val mouseRepeat: PcMouseRepeatCapabilities = PcMouseRepeatCapabilities()
17+
)
18+
19+
data class PcMouseRepeatCapabilities(
20+
val supported: Boolean = false,
21+
val enabled: Boolean = false,
22+
val intervalMs: Long = 250L,
23+
val minIntervalMs: Long = 100L,
24+
val maxIntervalMs: Long = 2000L
1625
)
1726

1827
data class PcPointerBounds(

app/src/main/java/com/enaboapps/switchify/pc/PcProtocol.kt

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,41 @@ object PcProtocol {
178178
return authenticatedCommand(id, deviceId, token, timestamp, "mouse.scroll", JSONObject().put("dx", dx).put("dy", dy), responseMode)
179179
}
180180

181+
fun mouseRepeatStart(
182+
id: String,
183+
deviceId: String,
184+
token: String,
185+
timestamp: Long,
186+
command: PcControlCommand
187+
): String {
188+
val nested = when (command) {
189+
is PcControlCommand.Move -> JSONObject()
190+
.put("type", "mouse.move")
191+
.put("payload", JSONObject().put("dx", command.dx).put("dy", command.dy))
192+
is PcControlCommand.Scroll -> JSONObject()
193+
.put("type", "mouse.scroll")
194+
.put("payload", JSONObject().put("dx", command.dx).put("dy", command.dy))
195+
else -> throw IllegalArgumentException("Unsupported repeat command.")
196+
}
197+
return authenticatedCommand(
198+
id,
199+
deviceId,
200+
token,
201+
timestamp,
202+
"mouse.repeat.start",
203+
JSONObject().put("command", nested)
204+
)
205+
}
206+
207+
fun mouseRepeatStop(
208+
id: String,
209+
deviceId: String,
210+
token: String,
211+
timestamp: Long
212+
): String {
213+
return authenticatedCommand(id, deviceId, token, timestamp, "mouse.repeat.stop", JSONObject())
214+
}
215+
181216
fun mouseDragStart(
182217
id: String,
183218
deviceId: String,
@@ -494,7 +529,8 @@ object PcProtocol {
494529
val capabilities = PcPointerCapabilities(
495530
noAckMouseMove = capabilitiesJson?.optBoolean("noAckMouseMove", false) ?: false,
496531
noAckCommands = noAckCommands,
497-
supportedCommands = supportedCommands
532+
supportedCommands = supportedCommands,
533+
mouseRepeat = parseMouseRepeatCapabilities(capabilitiesJson) ?: return PcProtocolResponse.Invalid
498534
)
499535
val bounds = PcPointerBounds(
500536
x = boundsJson.optInt("x"),
@@ -532,6 +568,26 @@ object PcProtocol {
532568
)
533569
}
534570

571+
private fun parseMouseRepeatCapabilities(capabilitiesJson: JSONObject?): PcMouseRepeatCapabilities? {
572+
val repeatJson = capabilitiesJson?.opt("mouseRepeat") ?: return PcMouseRepeatCapabilities()
573+
if (repeatJson !is JSONObject) return null
574+
575+
val intervalMs = repeatJson.optLong("intervalMs", 250L)
576+
val minIntervalMs = repeatJson.optLong("minIntervalMs", 100L)
577+
val maxIntervalMs = repeatJson.optLong("maxIntervalMs", 2000L)
578+
if (intervalMs <= 0L || minIntervalMs <= 0L || maxIntervalMs < minIntervalMs) return null
579+
if (repeatJson.has("supported") && repeatJson.opt("supported") !is Boolean) return null
580+
if (repeatJson.has("enabled") && repeatJson.opt("enabled") !is Boolean) return null
581+
582+
return PcMouseRepeatCapabilities(
583+
supported = repeatJson.optBoolean("supported", false),
584+
enabled = repeatJson.optBoolean("enabled", false),
585+
intervalMs = intervalMs,
586+
minIntervalMs = minIntervalMs,
587+
maxIntervalMs = maxIntervalMs
588+
)
589+
}
590+
535591
private fun parseNoAckCommands(capabilitiesJson: JSONObject?): Set<String>? {
536592
if (capabilitiesJson == null || !capabilitiesJson.has("noAckCommands")) return emptySet()
537593
val commandsJson = capabilitiesJson.opt("noAckCommands")
@@ -585,6 +641,8 @@ object PcProtocol {
585641
"connection.ping",
586642
"connection.disconnecting",
587643
"pointer.profile",
644+
"mouse.repeat.start",
645+
"mouse.repeat.stop",
588646
"keyboard.textStream.open",
589647
"keyboard.textStream.chunk",
590648
"keyboard.textStream.close"

app/src/main/java/com/enaboapps/switchify/pc/bluetooth/SwitchifyPcBleClient.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ class SwitchifyPcBleClient(
280280
return when (this) {
281281
is PcControlCommand.Move -> PcProtocol.mouseMove(id, deviceId, token, timestamp, dx, dy, responseMode)
282282
is PcControlCommand.Scroll -> PcProtocol.mouseScroll(id, deviceId, token, timestamp, dx, dy, responseMode)
283+
is PcControlCommand.RepeatStart -> PcProtocol.mouseRepeatStart(id, deviceId, token, timestamp, command)
284+
PcControlCommand.RepeatStop -> PcProtocol.mouseRepeatStop(id, deviceId, token, timestamp)
283285
is PcControlCommand.DragStart -> PcProtocol.mouseDragStart(id, deviceId, token, timestamp, button, responseMode)
284286
is PcControlCommand.DragEnd -> PcProtocol.mouseDragEnd(id, deviceId, token, timestamp, button, responseMode)
285287
PcControlCommand.LeftClick -> PcProtocol.mouseClick(id, deviceId, token, timestamp, responseMode = responseMode)
@@ -385,6 +387,8 @@ private fun PcControlCommand.protocolType(): String {
385387
return when (this) {
386388
is PcControlCommand.Move -> "mouse.move"
387389
is PcControlCommand.Scroll -> "mouse.scroll"
390+
is PcControlCommand.RepeatStart -> "mouse.repeat.start"
391+
PcControlCommand.RepeatStop -> "mouse.repeat.stop"
388392
is PcControlCommand.DragStart -> "mouse.dragStart"
389393
is PcControlCommand.DragEnd -> "mouse.dragEnd"
390394
PcControlCommand.LeftClick -> "mouse.click"

app/src/main/java/com/enaboapps/switchify/screens/pc/PcMouseControlViewModel.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,53 @@ class PcMouseControlViewModel(
143143
}
144144

145145
fun sendMouseCommand(command: PcControlCommand, repeatable: Boolean) {
146+
val mouseRepeat = currentMouseRepeatCapabilities()
147+
if (repeatable && mouseRepeat?.supported == true) {
148+
if (mouseRepeat.enabled) {
149+
sendPcSideRepeatCommand(command)
150+
} else {
151+
send(command)
152+
}
153+
return
154+
}
155+
146156
if (repeatable && mouseRepeatManager.armForInitialSend(command)) {
147157
sendRepeatableMouseCommand(command)
148158
return
149159
}
150160
send(command)
151161
}
152162

163+
private fun sendPcSideRepeatCommand(command: PcControlCommand) {
164+
val controller = serviceControllerProvider()
165+
if (controller == null || !mouseRepeatManager.armPcSideRepeat(
166+
command = command,
167+
scope = viewModelScope,
168+
stopRepeatedCommand = { controller.sendControlCommand(PcControlCommand.RepeatStop) }
169+
)) {
170+
send(command)
171+
return
172+
}
173+
174+
viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
175+
when (sendNoAckCommandNow(PcControlCommand.RepeatStart(command)) {
176+
it.copy(
177+
isBusy = false,
178+
busyCommand = null,
179+
message = null
180+
)
181+
}) {
182+
PcCommandResult.Ack -> {
183+
mouseRepeatManager.confirmPcSideStarted(command)
184+
}
185+
is PcCommandResult.AuthFailed,
186+
is PcCommandResult.Failed -> {
187+
mouseRepeatManager.cancelPcSidePending(showMessage = false)
188+
}
189+
}
190+
}
191+
}
192+
153193
private fun sendRepeatableMouseCommand(command: PcControlCommand) {
154194
viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
155195
when (sendRepeatCommand(command)) {
@@ -567,6 +607,12 @@ class PcMouseControlViewModel(
567607
?: false
568608
}
569609

610+
private fun currentMouseRepeatCapabilities() =
611+
((serviceControllerProvider()?.state?.value as? PcServiceConnectionState.Connected)?.pointerProfile
612+
?: serviceControllerProvider()?.currentPointerProfile())
613+
?.capabilities
614+
?.mouseRepeat
615+
570616
private suspend fun sendEnterAfterTypedText(controller: PcServiceConnectionController) {
571617
when (val keyResult = controller.sendRealtimeControlCommand(PcControlCommand.PressKey(PcKeyboardKey.Enter))) {
572618
PcCommandResult.Ack -> clearTypingSendAfterEnter()

0 commit comments

Comments
 (0)