Skip to content

Commit cde913a

Browse files
Remove PC mouse repeat first action delay (#2555)
- Send the initial repeatable PC mouse command from the ViewModel immediately - Move PcMouseRepeatManager to schedule only follow-up repeats after the first send succeeds - Keep repeat timing, switch-stop behavior, and non-repeatable PC controls unchanged - Add focused coverage for immediate first sends, repeat timing, and first-send failures 🤖 Auto-generated Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent 5ad8451 commit cde913a

5 files changed

Lines changed: 140 additions & 50 deletions

File tree

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,29 @@ class PcMouseRepeatManager internal constructor(
2929
settings = PreferencePcMouseRepeatSettings(context.applicationContext)
3030
}
3131

32-
fun start(
32+
fun canRepeat(command: PcControlCommand): Boolean {
33+
return isRepeatable(command) && currentSettings().isEnabled()
34+
}
35+
36+
fun startAfterInitialSend(
3337
command: PcControlCommand,
3438
scope: CoroutineScope,
35-
sendCommand: suspend (PcControlCommand) -> PcCommandResult
39+
sendRepeatedCommand: suspend (PcControlCommand) -> PcCommandResult
3640
): Boolean {
37-
if (!isRepeatable(command) || !currentSettings().isEnabled()) return false
41+
if (!canRepeat(command)) return false
3842

3943
stop(showMessage = false)
4044
repeatedCommand = command
45+
showMessage(R.string.pc_mouse_repeat_started, MessageSeverity.Success)
4146
repeatJob = scope.launch {
42-
if (!sendAndContinue(command, sendCommand, showStartedMessage = true)) return@launch
43-
4447
while (isActive) {
4548
delay(currentSettings().intervalMs())
4649
if (!isActive) return@launch
4750
if (!currentSettings().isEnabled()) {
4851
stop()
4952
return@launch
5053
}
51-
if (!sendAndContinue(command, sendCommand)) return@launch
54+
if (!sendAndContinue(command, sendRepeatedCommand)) return@launch
5255
}
5356
}
5457
return true
@@ -79,14 +82,10 @@ class PcMouseRepeatManager internal constructor(
7982

8083
private suspend fun sendAndContinue(
8184
command: PcControlCommand,
82-
sendCommand: suspend (PcControlCommand) -> PcCommandResult,
83-
showStartedMessage: Boolean = false
85+
sendCommand: suspend (PcControlCommand) -> PcCommandResult
8486
): Boolean {
8587
return when (sendCommand(command)) {
86-
PcCommandResult.Ack -> {
87-
if (showStartedMessage) showMessage(R.string.pc_mouse_repeat_started, MessageSeverity.Success)
88-
true
89-
}
88+
PcCommandResult.Ack -> true
9089
is PcCommandResult.AuthFailed,
9190
is PcCommandResult.Failed -> {
9291
clearRepeatState()

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.enaboapps.switchify.pc.pcTextStreamItemsFor
1818
import com.enaboapps.switchify.pc.supportsTextStreams
1919
import com.enaboapps.switchify.service.core.ServiceCore
2020
import java.util.UUID
21+
import kotlinx.coroutines.CoroutineStart
2122
import kotlinx.coroutines.delay
2223
import kotlinx.coroutines.flow.MutableStateFlow
2324
import kotlinx.coroutines.flow.StateFlow
@@ -123,12 +124,29 @@ class PcMouseControlViewModel(
123124
}
124125

125126
fun sendMouseCommand(command: PcControlCommand, repeatable: Boolean) {
126-
if (repeatable && mouseRepeatManager.start(command, viewModelScope, ::sendRepeatCommand)) {
127+
if (repeatable && mouseRepeatManager.canRepeat(command)) {
128+
sendRepeatableMouseCommand(command)
127129
return
128130
}
129131
send(command)
130132
}
131133

134+
private fun sendRepeatableMouseCommand(command: PcControlCommand) {
135+
viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
136+
when (sendRepeatCommand(command)) {
137+
PcCommandResult.Ack -> {
138+
mouseRepeatManager.startAfterInitialSend(
139+
command = command,
140+
scope = viewModelScope,
141+
sendRepeatedCommand = ::sendRepeatCommand
142+
)
143+
}
144+
is PcCommandResult.AuthFailed,
145+
is PcCommandResult.Failed -> Unit
146+
}
147+
}
148+
}
149+
132150
private suspend fun sendRepeatCommand(command: PcControlCommand): PcCommandResult {
133151
return sendNoAckCommandNow(command) {
134152
it.copy(

app/src/test/java/com/enaboapps/switchify/pc/PcMouseRepeatManagerTest.kt

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,35 +24,50 @@ class PcMouseRepeatManagerTest {
2424
}
2525

2626
@Test
27-
fun firstCommandSendsImmediatelyThenRepeatsAfterInterval() = runTest {
27+
fun canRepeatFalseWhenDisabled() {
28+
val repeatManager = repeatManager(enabled = false)
29+
30+
assertFalse(repeatManager.canRepeat(PcControlCommand.Move(5, 0)))
31+
}
32+
33+
@Test
34+
fun startAfterInitialSendDoesNotSendBeforeInterval() = runTest {
2835
val commands = mutableListOf<PcControlCommand>()
2936
val repeatManager = repeatManager(intervalMs = 250L)
3037

3138
assertTrue(
32-
repeatManager.start(PcControlCommand.Move(5, 0), this) {
39+
repeatManager.startAfterInitialSend(PcControlCommand.Move(5, 0), this) {
3340
commands += it
3441
PcCommandResult.Ack
3542
}
3643
)
3744
runCurrent()
3845

39-
assertEquals(listOf(PcControlCommand.Move(5, 0)), commands)
46+
assertEquals(emptyList<PcControlCommand>(), commands)
4047

4148
advanceTimeBy(249)
4249
runCurrent()
4350

44-
assertEquals(listOf(PcControlCommand.Move(5, 0)), commands)
51+
assertEquals(emptyList<PcControlCommand>(), commands)
52+
repeatManager.stop(showMessage = false)
53+
}
4554

46-
advanceTimeBy(1)
47-
runCurrent()
55+
@Test
56+
fun startAfterInitialSendSendsFirstRepeatAfterInterval() = runTest {
57+
val commands = mutableListOf<PcControlCommand>()
58+
val repeatManager = repeatManager(intervalMs = 250L)
4859

49-
assertEquals(
50-
listOf(
51-
PcControlCommand.Move(5, 0),
52-
PcControlCommand.Move(5, 0)
53-
),
54-
commands
60+
assertTrue(
61+
repeatManager.startAfterInitialSend(PcControlCommand.Move(5, 0), this) {
62+
commands += it
63+
PcCommandResult.Ack
64+
}
5565
)
66+
67+
advanceTimeBy(250)
68+
runCurrent()
69+
70+
assertEquals(listOf(PcControlCommand.Move(5, 0)), commands)
5671
repeatManager.stop(showMessage = false)
5772
}
5873

@@ -62,7 +77,7 @@ class PcMouseRepeatManagerTest {
6277
val repeatManager = repeatManager(enabled = false)
6378

6479
assertFalse(
65-
repeatManager.start(PcControlCommand.Scroll(0, 5), this) {
80+
repeatManager.startAfterInitialSend(PcControlCommand.Scroll(0, 5), this) {
6681
commands += it
6782
PcCommandResult.Ack
6883
}
@@ -79,18 +94,17 @@ class PcMouseRepeatManagerTest {
7994
val repeatManager = repeatManager(settings)
8095

8196
assertTrue(
82-
repeatManager.start(PcControlCommand.Scroll(0, 5), this) {
97+
repeatManager.startAfterInitialSend(PcControlCommand.Scroll(0, 5), this) {
8398
commands += it
8499
PcCommandResult.Ack
85100
}
86101
)
87-
runCurrent()
88102
settings.enabled = false
89103

90104
advanceTimeBy(100)
91105
runCurrent()
92106

93-
assertEquals(listOf(PcControlCommand.Scroll(0, 5)), commands)
107+
assertEquals(emptyList<PcControlCommand>(), commands)
94108
assertFalse(repeatManager.isRepeating())
95109
}
96110

@@ -100,7 +114,7 @@ class PcMouseRepeatManagerTest {
100114

101115
assertFalse(repeatManager.stopForSwitchPress())
102116
assertTrue(
103-
repeatManager.start(PcControlCommand.Scroll(0, 5), this) {
117+
repeatManager.startAfterInitialSend(PcControlCommand.Scroll(0, 5), this) {
104118
PcCommandResult.Ack
105119
}
106120
)
@@ -112,18 +126,13 @@ class PcMouseRepeatManagerTest {
112126

113127
@Test
114128
fun commandFailureStopsRepeat() = runTest {
115-
val results = ArrayDeque<PcCommandResult>().apply {
116-
add(PcCommandResult.Ack)
117-
add(PcCommandResult.Failed())
118-
}
119129
val repeatManager = repeatManager(intervalMs = 100L)
120130

121131
assertTrue(
122-
repeatManager.start(PcControlCommand.Move(5, 0), this) {
123-
results.removeFirst()
132+
repeatManager.startAfterInitialSend(PcControlCommand.Move(5, 0), this) {
133+
PcCommandResult.Failed()
124134
}
125135
)
126-
runCurrent()
127136
assertTrue(repeatManager.isRepeating())
128137

129138
advanceTimeBy(100)
@@ -134,18 +143,13 @@ class PcMouseRepeatManagerTest {
134143

135144
@Test
136145
fun authFailureStopsRepeat() = runTest {
137-
val results = ArrayDeque<PcCommandResult>().apply {
138-
add(PcCommandResult.Ack)
139-
add(PcCommandResult.AuthFailed())
140-
}
141146
val repeatManager = repeatManager(intervalMs = 100L)
142147

143148
assertTrue(
144-
repeatManager.start(PcControlCommand.Move(5, 0), this) {
145-
results.removeFirst()
149+
repeatManager.startAfterInitialSend(PcControlCommand.Move(5, 0), this) {
150+
PcCommandResult.AuthFailed()
146151
}
147152
)
148-
runCurrent()
149153

150154
advanceTimeBy(100)
151155
runCurrent()
@@ -154,16 +158,30 @@ class PcMouseRepeatManagerTest {
154158
}
155159

156160
@Test
157-
fun startAndStopMessagesAreRecordedForUserObservableStops() = runTest {
161+
fun startAfterInitialSendRecordsStartedMessage() = runTest {
158162
val messages = mutableListOf<Int>()
159163
val repeatManager = repeatManager(messages = messages)
160164

161165
assertTrue(
162-
repeatManager.start(PcControlCommand.Move(5, 0), this) {
166+
repeatManager.startAfterInitialSend(PcControlCommand.Move(5, 0), this) {
167+
PcCommandResult.Ack
168+
}
169+
)
170+
171+
assertEquals(listOf(R.string.pc_mouse_repeat_started), messages)
172+
repeatManager.stop(showMessage = false)
173+
}
174+
175+
@Test
176+
fun stopForSwitchPressRecordsStoppedMessage() = runTest {
177+
val messages = mutableListOf<Int>()
178+
val repeatManager = repeatManager(messages = messages)
179+
180+
assertTrue(
181+
repeatManager.startAfterInitialSend(PcControlCommand.Move(5, 0), this) {
163182
PcCommandResult.Ack
164183
}
165184
)
166-
runCurrent()
167185
repeatManager.stopForSwitchPress()
168186

169187
assertEquals(

app/src/test/java/com/enaboapps/switchify/screens/pc/PcMouseControlViewModelTest.kt

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,6 @@ class PcMouseControlViewModelTest {
240240
val viewModel = viewModel(controller)
241241

242242
viewModel.sendMouseCommand(PcControlCommand.Move(80, 0), repeatable = true)
243-
runCurrent()
244243

245244
assertEquals(listOf(PcControlCommand.Move(80, 0)), connector.realtimeCommands)
246245

@@ -262,6 +261,34 @@ class PcMouseControlViewModelTest {
262261
mouseRepeatManager.stop(showMessage = false)
263262
}
264263

264+
@Test
265+
fun repeatableScrollCommandSendsImmediatelyThenRepeats() = runTest(dispatcher) {
266+
val connector = FakeConnector()
267+
val controller = connectedController(connector = connector)
268+
val viewModel = viewModel(controller)
269+
270+
viewModel.sendMouseCommand(PcControlCommand.Scroll(0, 5), repeatable = true)
271+
272+
assertEquals(listOf(PcControlCommand.Scroll(0, 5)), connector.realtimeCommands)
273+
274+
advanceTimeBy(249)
275+
runCurrent()
276+
277+
assertEquals(listOf(PcControlCommand.Scroll(0, 5)), connector.realtimeCommands)
278+
279+
advanceTimeBy(1)
280+
runCurrent()
281+
282+
assertEquals(
283+
listOf(
284+
PcControlCommand.Scroll(0, 5),
285+
PcControlCommand.Scroll(0, 5)
286+
),
287+
connector.realtimeCommands
288+
)
289+
mouseRepeatManager.stop(showMessage = false)
290+
}
291+
265292
@Test
266293
fun disabledMouseRepeatSendsOnlyOnce() = runTest(dispatcher) {
267294
mouseRepeatSettings.enabled = false
@@ -291,6 +318,34 @@ class PcMouseControlViewModelTest {
291318
assertEquals(listOf(PcControlCommand.LeftClick), connector.realtimeCommands)
292319
}
293320

321+
@Test
322+
fun repeatableMouseCommandFailureDoesNotStartRepeat() = runTest(dispatcher) {
323+
val connector = FakeConnector(commandResult = PcCommandResult.Failed())
324+
val controller = connectedController(connector = connector)
325+
val viewModel = viewModel(controller)
326+
327+
viewModel.sendMouseCommand(PcControlCommand.Move(80, 0), repeatable = true)
328+
329+
assertEquals(listOf(PcControlCommand.Move(80, 0)), connector.realtimeCommands)
330+
assertFalse(mouseRepeatManager.isRepeating())
331+
}
332+
333+
@Test
334+
fun repeatableMouseCommandAuthFailureDoesNotStartRepeat() = runTest(dispatcher) {
335+
val connector = FakeConnector(commandResult = PcCommandResult.AuthFailed())
336+
val controller = connectedController(connector = connector)
337+
val viewModel = viewModel(controller)
338+
339+
viewModel.sendMouseCommand(PcControlCommand.Move(80, 0), repeatable = true)
340+
341+
assertTrue(connector.realtimeCommands.isNotEmpty())
342+
assertEquals(
343+
connector.realtimeCommands,
344+
List(connector.realtimeCommands.size) { PcControlCommand.Move(80, 0) }
345+
)
346+
assertFalse(mouseRepeatManager.isRepeating())
347+
}
348+
294349
@Test
295350
fun reconnectingStopsMouseRepeat() = runTest(dispatcher) {
296351
val connector = FakeConnector()

app/src/test/java/com/enaboapps/switchify/service/core/TasksTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class TasksTest {
8383
@Test
8484
fun hasActiveStoppableTaskTrueForMouseRepeat() = runTest {
8585
assertTrue(
86-
mouseRepeatManager.start(PcControlCommand.Move(10, 0), this) {
86+
mouseRepeatManager.startAfterInitialSend(PcControlCommand.Move(10, 0), this) {
8787
PcCommandResult.Ack
8888
}
8989
)
@@ -125,7 +125,7 @@ class TasksTest {
125125
repeatManager.setAutoRepeatEnabledForTesting(true)
126126
repeatManager.onGesturePerformed(testGesture())
127127
assertTrue(
128-
mouseRepeatManager.start(PcControlCommand.Scroll(0, 5), this) {
128+
mouseRepeatManager.startAfterInitialSend(PcControlCommand.Scroll(0, 5), this) {
129129
PcCommandResult.Ack
130130
}
131131
)

0 commit comments

Comments
 (0)