Skip to content

Commit 22a2515

Browse files
authored
Add PC mouse drag control
- Add drag start and drag end commands to the Android PC control protocol - Serialize drag commands over the Bluetooth PC connector - Add a Drag tile with ACK-based toggle state in the mouse control UI - Reset drag mode on reconnect, disconnect, and failed connection states - Cover protocol, BLE serialization, grid order, and ViewModel toggle behavior 🤖 Auto-generated
1 parent cc247ed commit 22a2515

9 files changed

Lines changed: 212 additions & 6 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 DragStart(val button: String = "left") : PcControlCommand()
20+
data class DragEnd(val button: String = "left") : PcControlCommand()
1921
data class TypeText(val text: String) : PcControlCommand()
2022
data class PressKey(val key: PcKeyboardKey) : PcControlCommand()
2123
data class WindowControl(val action: PcWindowControlAction) : PcControlCommand()

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ object PcProtocol {
125125
return authenticatedCommand(id, deviceId, token, timestamp, "mouse.scroll", JSONObject().put("dx", dx).put("dy", dy))
126126
}
127127

128+
fun mouseDragStart(id: String, deviceId: String, token: String, timestamp: Long, button: String = "left"): String {
129+
return authenticatedCommand(id, deviceId, token, timestamp, "mouse.dragStart", JSONObject().put("button", button))
130+
}
131+
132+
fun mouseDragEnd(id: String, deviceId: String, token: String, timestamp: Long, button: String = "left"): String {
133+
return authenticatedCommand(id, deviceId, token, timestamp, "mouse.dragEnd", JSONObject().put("button", button))
134+
}
135+
128136
fun keyboardTypeText(id: String, deviceId: String, token: String, timestamp: Long, text: String): String {
129137
return authenticatedCommand(id, deviceId, token, timestamp, "keyboard.typeText", JSONObject().put("text", text))
130138
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ class SwitchifyPcBleClient(
265265
return when (this) {
266266
is PcControlCommand.Move -> PcProtocol.mouseMove(id, deviceId, token, timestamp, dx, dy)
267267
is PcControlCommand.Scroll -> PcProtocol.mouseScroll(id, deviceId, token, timestamp, dx, dy)
268+
is PcControlCommand.DragStart -> PcProtocol.mouseDragStart(id, deviceId, token, timestamp, button)
269+
is PcControlCommand.DragEnd -> PcProtocol.mouseDragEnd(id, deviceId, token, timestamp, button)
268270
PcControlCommand.LeftClick -> PcProtocol.mouseClick(id, deviceId, token, timestamp)
269271
PcControlCommand.DoubleClick -> PcProtocol.mouseDoubleClick(id, deviceId, token, timestamp)
270272
PcControlCommand.RightClick -> PcProtocol.mouseRightClick(id, deviceId, token, timestamp)

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,9 @@ fun pcMouseCompactControlSpecs(moveStep: Int): List<PcMouseControlSpec?> {
336336
movement[7],
337337
clicks[1],
338338
clicks[2],
339+
clicks[3],
339340
scroll[0],
340341
scroll[1],
341-
null,
342342
null
343343
)
344344
}
@@ -361,7 +361,8 @@ fun pcClickControlSpecs(): List<PcMouseControlSpec> {
361361
return listOf(
362362
PcMouseControlSpec(R.string.pc_mouse_click, PcControlCommand.LeftClick),
363363
PcMouseControlSpec(R.string.pc_mouse_double_click, PcControlCommand.DoubleClick),
364-
PcMouseControlSpec(R.string.pc_mouse_right_click, PcControlCommand.RightClick)
364+
PcMouseControlSpec(R.string.pc_mouse_right_click, PcControlCommand.RightClick),
365+
PcMouseControlSpec(R.string.menu_item_drag, PcControlCommand.DragStart())
365366
)
366367
}
367368

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ data class PcMouseControlUiState(
2424
val activeSurface: PcControlSurface = PcControlSurface.Mouse,
2525
val selectedMovementSize: PcMouseMovementSize = PcMouseMovementSize.Small,
2626
val movementStep: Int = PcMouseControlViewModel.FALLBACK_MOVEMENT_STEPS.small,
27+
val isDragging: Boolean = false,
2728
val isBusy: Boolean = false,
2829
val busyCommand: PcControlCommand? = null,
2930
val message: String? = null,
@@ -87,8 +88,26 @@ class PcMouseControlViewModel(
8788
}
8889

8990
fun send(command: PcControlCommand) {
90-
sendCommand(command) {
91-
it.copy(isBusy = false, busyCommand = null, message = null)
91+
val commandToSend = when (command) {
92+
is PcControlCommand.DragStart,
93+
is PcControlCommand.DragEnd -> if (_uiState.value.isDragging) {
94+
PcControlCommand.DragEnd()
95+
} else {
96+
PcControlCommand.DragStart()
97+
}
98+
else -> command
99+
}
100+
sendCommand(commandToSend) {
101+
it.copy(
102+
isDragging = when (commandToSend) {
103+
is PcControlCommand.DragStart -> true
104+
is PcControlCommand.DragEnd -> false
105+
else -> it.isDragging
106+
},
107+
isBusy = false,
108+
busyCommand = null,
109+
message = null
110+
)
92111
}
93112
}
94113

@@ -250,6 +269,7 @@ class PcMouseControlViewModel(
250269
_uiState.update {
251270
it.copy(
252271
connectedDisplayName = state.displayName,
272+
isDragging = false,
253273
isBusy = false,
254274
busyCommand = null,
255275
message = RECONNECTING_MESSAGE,
@@ -263,6 +283,7 @@ class PcMouseControlViewModel(
263283
it.copy(
264284
connectedDisplayName = null,
265285
movementStep = movementSteps.stepFor(it.selectedMovementSize),
286+
isDragging = false,
266287
isBusy = false,
267288
busyCommand = null,
268289
message = CONNECT_FIRST_MESSAGE,
@@ -276,6 +297,7 @@ class PcMouseControlViewModel(
276297
it.copy(
277298
connectedDisplayName = null,
278299
movementStep = movementSteps.stepFor(it.selectedMovementSize),
300+
isDragging = false,
279301
isBusy = false,
280302
busyCommand = null,
281303
message = state.message,
@@ -295,6 +317,7 @@ class PcMouseControlViewModel(
295317
_uiState.update {
296318
it.copy(
297319
connectedDisplayName = state.displayName,
320+
isDragging = false,
298321
isBusy = false,
299322
busyCommand = null,
300323
message = RECONNECTING_MESSAGE,
@@ -306,6 +329,7 @@ class PcMouseControlViewModel(
306329
_uiState.update {
307330
it.copy(
308331
connectedDisplayName = null,
332+
isDragging = false,
309333
isBusy = false,
310334
busyCommand = null,
311335
message = state.message,
@@ -337,6 +361,7 @@ class PcMouseControlViewModel(
337361
it.copy(
338362
connectedDisplayName = null,
339363
movementStep = movementSteps.stepFor(it.selectedMovementSize),
364+
isDragging = false,
340365
isBusy = false,
341366
busyCommand = null,
342367
message = CONNECT_FIRST_MESSAGE,

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,68 @@ class PcProtocolTest {
209209
assertEquals(5, scroll.getJSONObject("payload").getInt("dy"))
210210
}
211211

212+
@Test
213+
fun buildsMouseDragStartCommandWithAuthProof() {
214+
val json = JSONObject(
215+
PcProtocol.mouseDragStart(
216+
id = "drag-start-1",
217+
deviceId = "device-1",
218+
token = "shared-token",
219+
timestamp = 1000L
220+
)
221+
)
222+
223+
assertEquals(1, json.getInt("version"))
224+
assertEquals("drag-start-1", json.getString("id"))
225+
assertEquals("device-1", json.getString("deviceId"))
226+
assertEquals(1000L, json.getLong("timestamp"))
227+
assertEquals("mouse.dragStart", json.getString("type"))
228+
assertEquals("left", json.getJSONObject("payload").getString("button"))
229+
assertEquals(
230+
PcProtocol.authProof(
231+
id = "drag-start-1",
232+
deviceId = "device-1",
233+
timestamp = 1000L,
234+
type = "mouse.dragStart",
235+
payload = JSONObject().put("button", "left"),
236+
token = "shared-token"
237+
),
238+
json.getString("auth")
239+
)
240+
assertFalse(json.toString().contains("shared-token"))
241+
}
242+
243+
@Test
244+
fun buildsMouseDragEndCommandWithAuthProof() {
245+
val json = JSONObject(
246+
PcProtocol.mouseDragEnd(
247+
id = "drag-end-1",
248+
deviceId = "device-1",
249+
token = "shared-token",
250+
timestamp = 1000L
251+
)
252+
)
253+
254+
assertEquals(1, json.getInt("version"))
255+
assertEquals("drag-end-1", json.getString("id"))
256+
assertEquals("device-1", json.getString("deviceId"))
257+
assertEquals(1000L, json.getLong("timestamp"))
258+
assertEquals("mouse.dragEnd", json.getString("type"))
259+
assertEquals("left", json.getJSONObject("payload").getString("button"))
260+
assertEquals(
261+
PcProtocol.authProof(
262+
id = "drag-end-1",
263+
deviceId = "device-1",
264+
timestamp = 1000L,
265+
type = "mouse.dragEnd",
266+
payload = JSONObject().put("button", "left"),
267+
token = "shared-token"
268+
),
269+
json.getString("auth")
270+
)
271+
assertFalse(json.toString().contains("shared-token"))
272+
}
273+
212274
@Test
213275
fun buildsKeyboardTypeTextCommandWithAuthProof() {
214276
val json = JSONObject(

app/src/test/java/com/enaboapps/switchify/pc/bluetooth/SwitchifyPcBleClientTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ class SwitchifyPcBleClientTest {
205205
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.LeftClick))
206206
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.DoubleClick))
207207
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.RightClick))
208+
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.DragStart()))
209+
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.DragEnd()))
208210
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.Move(1, 2)))
209211
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.Scroll(0, 10)))
210212
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.TypeText("hello")))
@@ -216,6 +218,8 @@ class SwitchifyPcBleClientTest {
216218
"mouse.click",
217219
"mouse.doubleClick",
218220
"mouse.rightClick",
221+
"mouse.dragStart",
222+
"mouse.dragEnd",
219223
"mouse.move",
220224
"mouse.scroll",
221225
"keyboard.typeText",

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class PcControlCommandGridTest {
4040
assertEquals(PcControlCommand.LeftClick, commands[0])
4141
assertEquals(PcControlCommand.DoubleClick, commands[1])
4242
assertEquals(PcControlCommand.RightClick, commands[2])
43+
assertEquals(PcControlCommand.DragStart(), commands[3])
4344
}
4445

4546
@Test
@@ -67,6 +68,7 @@ class PcControlCommandGridTest {
6768
PcControlCommand.LeftClick,
6869
PcControlCommand.DoubleClick,
6970
PcControlCommand.RightClick,
71+
PcControlCommand.DragStart(),
7072
PcControlCommand.Scroll(0, 5),
7173
PcControlCommand.Scroll(0, -5)
7274
),
@@ -98,6 +100,7 @@ class PcControlCommandGridTest {
98100
PcControlCommand.Move(40, 40),
99101
PcControlCommand.DoubleClick,
100102
PcControlCommand.RightClick,
103+
PcControlCommand.DragStart(),
101104
PcControlCommand.Scroll(0, 5),
102105
PcControlCommand.Scroll(0, -5)
103106
),
@@ -110,8 +113,14 @@ class PcControlCommandGridTest {
110113
val specs = pcMouseCompactControlSpecs(40)
111114

112115
assertEquals(15, specs.size)
113-
assertEquals(13, specs.filterNotNull().size)
114-
assertEquals(null, specs[13])
116+
assertEquals(14, specs.filterNotNull().size)
115117
assertEquals(null, specs[14])
116118
}
119+
120+
@Test
121+
fun compactCommandsIncludeDragInStableScanOrder() {
122+
val commands = pcMouseCompactControlSpecs(40).mapNotNull { it?.command }
123+
124+
assertEquals(PcControlCommand.DragStart(), commands[11])
125+
}
117126
}

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,99 @@ class PcMouseControlViewModelTest {
280280
assertEquals("Connection expired. Connect to PC from Switchify first.", viewModel.uiState.value.message)
281281
}
282282

283+
@Test
284+
fun dragSendsDragStartWhenNotDragging() = runTest(dispatcher) {
285+
val connector = FakeConnector()
286+
val controller = connectedController(connector = connector)
287+
val viewModel = viewModel(controller)
288+
289+
viewModel.send(PcControlCommand.DragStart())
290+
advanceUntilIdle()
291+
292+
assertEquals(listOf(PcControlCommand.DragStart()), connector.commands)
293+
}
294+
295+
@Test
296+
fun dragAckSetsDraggingTrue() = runTest(dispatcher) {
297+
val controller = connectedController()
298+
val viewModel = viewModel(controller)
299+
300+
viewModel.send(PcControlCommand.DragStart())
301+
advanceUntilIdle()
302+
303+
assertEquals(true, viewModel.uiState.value.isDragging)
304+
}
305+
306+
@Test
307+
fun dragSendsDragEndWhenDragging() = runTest(dispatcher) {
308+
val connector = FakeConnector()
309+
val controller = connectedController(connector = connector)
310+
val viewModel = viewModel(controller)
311+
312+
viewModel.send(PcControlCommand.DragStart())
313+
advanceUntilIdle()
314+
viewModel.send(PcControlCommand.DragStart())
315+
advanceUntilIdle()
316+
317+
assertEquals(
318+
listOf(PcControlCommand.DragStart(), PcControlCommand.DragEnd()),
319+
connector.commands
320+
)
321+
}
322+
323+
@Test
324+
fun dragEndAckSetsDraggingFalse() = runTest(dispatcher) {
325+
val controller = connectedController()
326+
val viewModel = viewModel(controller)
327+
328+
viewModel.send(PcControlCommand.DragStart())
329+
advanceUntilIdle()
330+
viewModel.send(PcControlCommand.DragStart())
331+
advanceUntilIdle()
332+
333+
assertEquals(false, viewModel.uiState.value.isDragging)
334+
}
335+
336+
@Test
337+
fun dragFailureDoesNotFlipDragState() = runTest(dispatcher) {
338+
val connector = FakeConnector(commandResult = PcCommandResult.Failed())
339+
val controller = connectedController(connector = connector)
340+
val viewModel = viewModel(controller)
341+
342+
viewModel.send(PcControlCommand.DragStart())
343+
advanceUntilIdle()
344+
345+
assertEquals(false, viewModel.uiState.value.isDragging)
346+
assertEquals(PcMouseControlViewModel.COMMAND_FAILED_MESSAGE, viewModel.uiState.value.message)
347+
}
348+
349+
@Test
350+
fun reconnectResetsDraggingFalse() = runTest(dispatcher) {
351+
val controller = connectedController()
352+
val viewModel = viewModel(controller)
353+
val session = PcAuthenticatedSession("desktop-1", "device-1", "AA:BB:CC:DD:EE:FF")
354+
355+
viewModel.send(PcControlCommand.DragStart())
356+
advanceUntilIdle()
357+
PcConnectionStateHolder.setReconnecting(session, "Switchify PC")
358+
advanceUntilIdle()
359+
360+
assertEquals(false, viewModel.uiState.value.isDragging)
361+
}
362+
363+
@Test
364+
fun disconnectResetsDraggingFalse() = runTest(dispatcher) {
365+
val controller = connectedController()
366+
val viewModel = viewModel(controller)
367+
368+
viewModel.send(PcControlCommand.DragStart())
369+
advanceUntilIdle()
370+
controller.disconnect()
371+
advanceUntilIdle()
372+
373+
assertEquals(false, viewModel.uiState.value.isDragging)
374+
}
375+
283376
@Test
284377
fun reconnectingServiceStateShowsReconnecting() = runTest(dispatcher) {
285378
val session = PcAuthenticatedSession("desktop-1", "device-1", "AA:BB:CC:DD:EE:FF")

0 commit comments

Comments
 (0)