Skip to content

Commit 8a04f26

Browse files
authored
Add PC Start button
- Add Android keyboard shortcut command support for PC controls - Send the Start button as keyboard.shortcut with the Meta key to match Switchify PC protocol support - Add Start to the Window controls surface and cover protocol, BLE, UI ordering, and ViewModel routing 🤖 Auto-generated
1 parent 28ec0ac commit 8a04f26

10 files changed

Lines changed: 116 additions & 21 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ sealed class PcControlCommand {
2525
data class TextStreamKey(val streamId: String, val seq: Int, val key: PcKeyboardKey) : PcControlCommand()
2626
data class TextStreamClose(val streamId: String, val expectedCount: Int) : PcControlCommand()
2727
data class PressKey(val key: PcKeyboardKey) : PcControlCommand()
28+
data class KeyboardShortcut(val keys: List<PcKeyboardShortcutKey>) : PcControlCommand()
2829
data class WindowControl(val action: PcWindowControlAction) : PcControlCommand()
2930
data object LeftClick : PcControlCommand()
3031
data object DoubleClick : PcControlCommand()

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ enum class PcKeyboardKey(
3434
F11("F11", R.string.pc_key_f11),
3535
F12("F12", R.string.pc_key_f12)
3636
}
37+
38+
enum class PcKeyboardShortcutKey(val protocolValue: String) {
39+
Meta("Meta")
40+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,25 @@ object PcProtocol {
332332
)
333333
}
334334

335+
fun keyboardShortcut(
336+
id: String,
337+
deviceId: String,
338+
token: String,
339+
timestamp: Long,
340+
keys: List<PcKeyboardShortcutKey>,
341+
responseMode: PcCommandResponseMode = PcCommandResponseMode.Ack
342+
): String {
343+
return authenticatedCommand(
344+
id,
345+
deviceId,
346+
token,
347+
timestamp,
348+
"keyboard.shortcut",
349+
JSONObject().put("keys", JSONArray(keys.map { it.protocolValue })),
350+
responseMode
351+
)
352+
}
353+
335354
fun windowControl(
336355
id: String,
337356
deviceId: String,

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
@@ -292,6 +292,7 @@ class SwitchifyPcBleClient(
292292
is PcControlCommand.TextStreamKey -> PcProtocol.keyboardTextStreamKey(id, deviceId, token, timestamp, streamId, seq, key, responseMode)
293293
is PcControlCommand.TextStreamClose -> PcProtocol.keyboardTextStreamClose(id, deviceId, token, timestamp, streamId, expectedCount, responseMode)
294294
is PcControlCommand.PressKey -> PcProtocol.keyboardKey(id, deviceId, token, timestamp, key, responseMode)
295+
is PcControlCommand.KeyboardShortcut -> PcProtocol.keyboardShortcut(id, deviceId, token, timestamp, keys, responseMode)
295296
is PcControlCommand.WindowControl -> PcProtocol.windowControl(id, deviceId, token, timestamp, action, responseMode)
296297
}
297298
}
@@ -396,6 +397,7 @@ private fun PcControlCommand.protocolType(): String {
396397
is PcControlCommand.TextStreamKey -> "keyboard.textStream.key"
397398
is PcControlCommand.TextStreamClose -> "keyboard.textStream.close"
398399
is PcControlCommand.PressKey -> "keyboard.key"
400+
is PcControlCommand.KeyboardShortcut -> "keyboard.shortcut"
399401
is PcControlCommand.WindowControl -> "window.control"
400402
}
401403
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import androidx.compose.ui.res.stringResource
2020
import androidx.compose.ui.unit.dp
2121
import com.enaboapps.switchify.R
2222
import com.enaboapps.switchify.pc.PcControlCommand
23+
import com.enaboapps.switchify.pc.PcKeyboardShortcutKey
2324
import com.enaboapps.switchify.pc.PcWindowControlAction
2425

2526
data class PcWindowControlSpec(
2627
@param:StringRes val labelResId: Int,
27-
val command: PcControlCommand.WindowControl,
28+
val command: PcControlCommand,
2829
val icon: ImageVector? = null,
2930
val tone: PcCommandTone = PcCommandTone.Neutral
3031
)
@@ -69,11 +70,16 @@ fun PcWindowControlScreen(
6970
}
7071

7172
fun pcWindowCompactControlSpecs(): List<PcWindowControlSpec?> {
72-
return pcWindowControlSpecs() + listOf(null, null)
73+
return pcWindowControlSpecs() + listOf(null)
7374
}
7475

7576
fun pcWindowControlSpecs(): List<PcWindowControlSpec> {
7677
return listOf(
78+
PcWindowControlSpec(
79+
R.string.pc_key_start,
80+
PcControlCommand.KeyboardShortcut(listOf(PcKeyboardShortcutKey.Meta)),
81+
Icons.Rounded.Computer
82+
),
7783
PcWindowControlSpec(
7884
R.string.pc_window_switch_next,
7985
PcControlCommand.WindowControl(PcWindowControlAction.SwitchNext),

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@
390390
<string name="pc_window_minimize">Minimize</string>
391391
<string name="pc_window_maximize">Maximize</string>
392392
<string name="pc_window_close">Close app</string>
393+
<string name="pc_key_start">Start</string>
393394
<string name="pc_key_backspace">Backspace</string>
394395
<string name="pc_key_delete">Delete</string>
395396
<string name="pc_key_enter">Enter</string>

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.enaboapps.switchify.pc
22

33
import org.json.JSONObject
4+
import org.json.JSONArray
45
import org.junit.Assert.assertEquals
56
import org.junit.Assert.assertFalse
67
import org.junit.Assert.assertTrue
@@ -314,6 +315,7 @@ class PcProtocolTest {
314315
JSONObject(PcProtocol.keyboardTextStreamChar("stream-char-1", "device-1", "shared-token", 1000L, "android-1", 0, "H")),
315316
JSONObject(PcProtocol.keyboardTextStreamKey("stream-key-1", "device-1", "shared-token", 1000L, "android-1", 1, PcKeyboardKey.Enter)),
316317
JSONObject(PcProtocol.keyboardKey("key-1", "device-1", "shared-token", 1000L, PcKeyboardKey.Enter, PcCommandResponseMode.None)),
318+
JSONObject(PcProtocol.keyboardShortcut("shortcut-1", "device-1", "shared-token", 1000L, listOf(PcKeyboardShortcutKey.Meta), PcCommandResponseMode.None)),
317319
JSONObject(PcProtocol.windowControl("window-1", "device-1", "shared-token", 1000L, PcWindowControlAction.SwitchNext, PcCommandResponseMode.None))
318320
)
319321

@@ -477,6 +479,39 @@ class PcProtocolTest {
477479
assertFalse(json.toString().contains("shared-token"))
478480
}
479481

482+
@Test
483+
fun buildsMetaKeyboardShortcutCommand() {
484+
val json = JSONObject(
485+
PcProtocol.keyboardShortcut(
486+
id = "shortcut-meta",
487+
deviceId = "device-1",
488+
token = "shared-token",
489+
timestamp = 1000L,
490+
keys = listOf(PcKeyboardShortcutKey.Meta)
491+
)
492+
)
493+
val payload = json.getJSONObject("payload")
494+
495+
assertEquals(1, json.getInt("version"))
496+
assertEquals("shortcut-meta", json.getString("id"))
497+
assertEquals("device-1", json.getString("deviceId"))
498+
assertEquals(1000L, json.getLong("timestamp"))
499+
assertEquals("keyboard.shortcut", json.getString("type"))
500+
assertEquals("Meta", payload.getJSONArray("keys").getString(0))
501+
assertEquals(
502+
PcProtocol.authProof(
503+
id = "shortcut-meta",
504+
deviceId = "device-1",
505+
timestamp = 1000L,
506+
type = "keyboard.shortcut",
507+
payload = JSONObject().put("keys", JSONArray(listOf("Meta"))),
508+
token = "shared-token"
509+
),
510+
json.getString("auth")
511+
)
512+
assertFalse(json.toString().contains("shared-token"))
513+
}
514+
480515
@Test
481516
fun buildsTextStreamOpenCommandWithAuthProof() {
482517
val json = JSONObject(

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.enaboapps.switchify.pc.PcControlConnectionEvent
1111
import com.enaboapps.switchify.pc.PcDeviceIdentity
1212
import com.enaboapps.switchify.pc.PcErrorReason
1313
import com.enaboapps.switchify.pc.PcKeyboardKey
14+
import com.enaboapps.switchify.pc.PcKeyboardShortcutKey
1415
import com.enaboapps.switchify.pc.PcLiveControlResult
1516
import com.enaboapps.switchify.pc.PcPairingResult
1617
import com.enaboapps.switchify.pc.PcPairingTokenStore
@@ -233,6 +234,7 @@ class SwitchifyPcBleClientTest {
233234
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.Scroll(0, 10)))
234235
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.TypeText("hello")))
235236
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.PressKey(PcKeyboardKey.Enter)))
237+
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.KeyboardShortcut(listOf(PcKeyboardShortcutKey.Meta))))
236238
assertEquals(PcCommandResult.Ack, client.sendCommand(session, PcControlCommand.WindowControl(PcWindowControlAction.MinimizeFocused)))
237239

238240
assertEquals(
@@ -246,6 +248,7 @@ class SwitchifyPcBleClientTest {
246248
"mouse.scroll",
247249
"keyboard.typeText",
248250
"keyboard.key",
251+
"keyboard.shortcut",
249252
"window.control"
250253
),
251254
seenTypes
@@ -356,7 +359,7 @@ class SwitchifyPcBleClientTest {
356359
"connection.ping" -> ack(json.getString("id"))
357360
"pointer.profile" -> pointerProfile(
358361
json.getString("id"),
359-
noAckCommands = listOf("mouse.click", "keyboard.typeText", "window.control")
362+
noAckCommands = listOf("mouse.click", "keyboard.typeText", "keyboard.shortcut", "window.control")
360363
)
361364
else -> ack(json.getString("id"))
362365
}
@@ -366,12 +369,14 @@ class SwitchifyPcBleClientTest {
366369

367370
assertEquals(PcCommandResult.Ack, result.connection.sendRealtimeCommand(PcControlCommand.LeftClick))
368371
assertEquals(PcCommandResult.Ack, result.connection.sendRealtimeCommand(PcControlCommand.TypeText("Hello")))
372+
assertEquals(PcCommandResult.Ack, result.connection.sendRealtimeCommand(PcControlCommand.KeyboardShortcut(listOf(PcKeyboardShortcutKey.Meta))))
369373
assertEquals(PcCommandResult.Ack, result.connection.sendRealtimeCommand(PcControlCommand.WindowControl(PcWindowControlAction.SwitchNext)))
370374

371375
assertEquals(listOf("connection.ping", "pointer.profile"), seenTypes)
372376
val messages = fakeConnection.sentMessages.map(::JSONObject)
373-
assertEquals(listOf("mouse.click", "keyboard.typeText", "window.control"), messages.map { it.getString("type") })
377+
assertEquals(listOf("mouse.click", "keyboard.typeText", "keyboard.shortcut", "window.control"), messages.map { it.getString("type") })
374378
assertTrue(messages.all { it.getString("responseMode") == "none" })
379+
assertEquals("Meta", messages[2].getJSONObject("payload").getJSONArray("keys").getString(0))
375380
}
376381

377382
@Test

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.enaboapps.switchify.pc.PcDiscovery
1616
import com.enaboapps.switchify.pc.PcDiscoveryStatus
1717
import com.enaboapps.switchify.pc.PcErrorReason
1818
import com.enaboapps.switchify.pc.PcKeyboardKey
19+
import com.enaboapps.switchify.pc.PcKeyboardShortcutKey
1920
import com.enaboapps.switchify.pc.PcLiveControlResult
2021
import com.enaboapps.switchify.pc.PcMouseRepeatManager
2122
import com.enaboapps.switchify.pc.PcMouseRepeatSettings
@@ -1005,6 +1006,24 @@ class PcMouseControlViewModelTest {
10051006
assertEquals(PcControlSurface.Window, viewModel.uiState.value.activeSurface)
10061007
}
10071008

1009+
@Test
1010+
fun windowSurfaceCanSendStartShortcutCommand() = runTest(dispatcher) {
1011+
val connector = FakeConnector()
1012+
val controller = connectedController(connector = connector)
1013+
val viewModel = viewModel(controller)
1014+
val command = PcControlCommand.KeyboardShortcut(listOf(PcKeyboardShortcutKey.Meta))
1015+
1016+
viewModel.selectControlSurface(PcControlSurface.Window)
1017+
viewModel.send(command)
1018+
advanceUntilIdle()
1019+
1020+
assertEquals(listOf(command), connector.realtimeCommands)
1021+
assertTrue(connector.commands.isEmpty())
1022+
assertEquals(PcControlSurface.Window, viewModel.uiState.value.activeSurface)
1023+
assertFalse(viewModel.uiState.value.isBusy)
1024+
assertNull(viewModel.uiState.value.message)
1025+
}
1026+
10081027
@Test
10091028
fun controlCommandDoesNotSetBusy() = runTest(dispatcher) {
10101029
val connector = FakeConnector()

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

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.enaboapps.switchify.screens.pc
33
import com.enaboapps.switchify.R
44
import com.enaboapps.switchify.pc.PcControlCommand
55
import com.enaboapps.switchify.pc.PcKeyboardKey
6+
import com.enaboapps.switchify.pc.PcKeyboardShortcutKey
67
import com.enaboapps.switchify.pc.PcWindowControlAction
78
import org.junit.Assert.assertEquals
89
import org.junit.Test
@@ -14,6 +15,7 @@ class PcWindowControlScreenTest {
1415

1516
assertEquals(
1617
listOf(
18+
PcControlCommand.KeyboardShortcut(listOf(PcKeyboardShortcutKey.Meta)),
1719
PcControlCommand.WindowControl(PcWindowControlAction.SwitchNext),
1820
PcControlCommand.WindowControl(PcWindowControlAction.SwitchPrevious),
1921
PcControlCommand.WindowControl(PcWindowControlAction.TaskView),
@@ -30,20 +32,22 @@ class PcWindowControlScreenTest {
3032
fun windowCommandLabelsMatchActions() {
3133
val specs = pcWindowControlSpecs()
3234

33-
assertEquals(R.string.pc_window_switch_next, specs[0].labelResId)
34-
assertEquals(PcWindowControlAction.SwitchNext, specs[0].command.action)
35-
assertEquals(R.string.pc_window_switch_previous, specs[1].labelResId)
36-
assertEquals(PcWindowControlAction.SwitchPrevious, specs[1].command.action)
37-
assertEquals(R.string.pc_window_task_view, specs[2].labelResId)
38-
assertEquals(PcWindowControlAction.TaskView, specs[2].command.action)
39-
assertEquals(R.string.pc_window_show_desktop, specs[3].labelResId)
40-
assertEquals(PcWindowControlAction.ShowDesktop, specs[3].command.action)
41-
assertEquals(R.string.pc_window_minimize, specs[4].labelResId)
42-
assertEquals(PcWindowControlAction.MinimizeFocused, specs[4].command.action)
43-
assertEquals(R.string.pc_window_maximize, specs[5].labelResId)
44-
assertEquals(PcWindowControlAction.MaximizeFocused, specs[5].command.action)
45-
assertEquals(R.string.pc_window_close, specs[6].labelResId)
46-
assertEquals(PcWindowControlAction.CloseFocused, specs[6].command.action)
35+
assertEquals(R.string.pc_key_start, specs[0].labelResId)
36+
assertEquals(PcControlCommand.KeyboardShortcut(listOf(PcKeyboardShortcutKey.Meta)), specs[0].command)
37+
assertEquals(R.string.pc_window_switch_next, specs[1].labelResId)
38+
assertEquals(PcControlCommand.WindowControl(PcWindowControlAction.SwitchNext), specs[1].command)
39+
assertEquals(R.string.pc_window_switch_previous, specs[2].labelResId)
40+
assertEquals(PcControlCommand.WindowControl(PcWindowControlAction.SwitchPrevious), specs[2].command)
41+
assertEquals(R.string.pc_window_task_view, specs[3].labelResId)
42+
assertEquals(PcControlCommand.WindowControl(PcWindowControlAction.TaskView), specs[3].command)
43+
assertEquals(R.string.pc_window_show_desktop, specs[4].labelResId)
44+
assertEquals(PcControlCommand.WindowControl(PcWindowControlAction.ShowDesktop), specs[4].command)
45+
assertEquals(R.string.pc_window_minimize, specs[5].labelResId)
46+
assertEquals(PcControlCommand.WindowControl(PcWindowControlAction.MinimizeFocused), specs[5].command)
47+
assertEquals(R.string.pc_window_maximize, specs[6].labelResId)
48+
assertEquals(PcControlCommand.WindowControl(PcWindowControlAction.MaximizeFocused), specs[6].command)
49+
assertEquals(R.string.pc_window_close, specs[7].labelResId)
50+
assertEquals(PcControlCommand.WindowControl(PcWindowControlAction.CloseFocused), specs[7].command)
4751
}
4852

4953
@Test
@@ -52,17 +56,16 @@ class PcWindowControlScreenTest {
5256
val commands = specs.mapNotNull { it?.command }
5357

5458
assertEquals(9, specs.size)
55-
assertEquals(7, specs.filterNotNull().size)
59+
assertEquals(8, specs.filterNotNull().size)
5660
assertEquals(pcWindowControlSpecs().map { it.command }, commands)
57-
assertEquals(null, specs[7])
5861
assertEquals(null, specs[8])
5962
}
6063

6164
@Test
6265
fun closeWindowCommandUsesDestructiveTone() {
6366
val specs = pcWindowControlSpecs()
6467

65-
assertEquals(PcCommandTone.Destructive, specs[6].tone)
68+
assertEquals(PcCommandTone.Destructive, specs[7].tone)
6669
}
6770

6871
@Test

0 commit comments

Comments
 (0)