Skip to content

Commit a2b88c9

Browse files
Route PC text send through streams (#2533)
- Add PC text stream protocol commands and capability parsing. - Send existing typing actions through streams when supported. - Keep bulk text fallback for older PC builds and cover stream pacing. 🤖 Auto-generated Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent 15f4916 commit a2b88c9

9 files changed

Lines changed: 1048 additions & 59 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ sealed class PcControlCommand {
1919
data class DragStart(val button: String = "left") : PcControlCommand()
2020
data class DragEnd(val button: String = "left") : PcControlCommand()
2121
data class TypeText(val text: String) : PcControlCommand()
22+
data class TextStreamOpen(val streamId: String) : PcControlCommand()
23+
data class TextStreamChar(val streamId: String, val seq: Int, val text: String) : PcControlCommand()
24+
data class TextStreamChunk(val streamId: String, val seq: Int, val text: String) : PcControlCommand()
25+
data class TextStreamKey(val streamId: String, val seq: Int, val key: PcKeyboardKey) : PcControlCommand()
26+
data class TextStreamClose(val streamId: String, val expectedCount: Int) : PcControlCommand()
2227
data class PressKey(val key: PcKeyboardKey) : PcControlCommand()
2328
data class WindowControl(val action: PcWindowControlAction) : PcControlCommand()
2429
data object LeftClick : PcControlCommand()

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

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

33
const val PC_KEYBOARD_TYPE_TEXT_MAX_LENGTH = 2_000
4+
const val PC_TEXT_STREAM_CHUNK_CODE_POINTS = 4
5+
6+
sealed class PcTextStreamItem {
7+
data class Chunk(val text: String) : PcTextStreamItem()
8+
data class Key(val key: PcKeyboardKey) : PcTextStreamItem()
9+
}
410

511
fun isSafePcTypedText(text: String): Boolean {
612
if (text.length > PC_KEYBOARD_TYPE_TEXT_MAX_LENGTH) return false
@@ -10,3 +16,43 @@ fun isSafePcTypedText(text: String): Boolean {
1016
code in 0x7f..0x9f
1117
}
1218
}
19+
20+
fun pcTextStreamItemsFor(text: String): List<PcTextStreamItem> {
21+
val items = mutableListOf<PcTextStreamItem>()
22+
val chunk = StringBuilder()
23+
var chunkCodePoints = 0
24+
25+
fun flushChunk() {
26+
if (chunk.isNotEmpty()) {
27+
items += PcTextStreamItem.Chunk(chunk.toString())
28+
chunk.clear()
29+
chunkCodePoints = 0
30+
}
31+
}
32+
33+
var index = 0
34+
while (index < text.length) {
35+
val codePoint = text.codePointAt(index)
36+
val itemText = String(Character.toChars(codePoint))
37+
when (itemText) {
38+
"\n", "\r" -> {
39+
flushChunk()
40+
items += PcTextStreamItem.Key(PcKeyboardKey.Enter)
41+
}
42+
"\t" -> {
43+
flushChunk()
44+
items += PcTextStreamItem.Key(PcKeyboardKey.Tab)
45+
}
46+
else -> {
47+
chunk.append(itemText)
48+
chunkCodePoints += 1
49+
if (chunkCodePoints >= PC_TEXT_STREAM_CHUNK_CODE_POINTS) {
50+
flushChunk()
51+
}
52+
}
53+
}
54+
index += Character.charCount(codePoint)
55+
}
56+
flushChunk()
57+
return items
58+
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ data class PcPointerMovementProfile(
1111

1212
data class PcPointerCapabilities(
1313
val noAckMouseMove: Boolean = false,
14-
val noAckCommands: Set<String> = emptySet()
14+
val noAckCommands: Set<String> = emptySet(),
15+
val supportedCommands: Set<String> = emptySet()
1516
)
1617

1718
data class PcPointerBounds(
@@ -26,3 +27,14 @@ data class PcPointerDeltas(
2627
val medium: Int,
2728
val large: Int
2829
)
30+
31+
fun PcPointerMovementProfile.supportsTextStreams(): Boolean {
32+
return capabilities.supportedCommands.containsAll(
33+
setOf(
34+
"keyboard.textStream.open",
35+
"keyboard.textStream.chunk",
36+
"keyboard.textStream.key",
37+
"keyboard.textStream.close"
38+
)
39+
)
40+
}

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

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,108 @@ object PcProtocol {
211211
return authenticatedCommand(id, deviceId, token, timestamp, "keyboard.typeText", JSONObject().put("text", text), responseMode)
212212
}
213213

214+
fun keyboardTextStreamOpen(
215+
id: String,
216+
deviceId: String,
217+
token: String,
218+
timestamp: Long,
219+
streamId: String,
220+
responseMode: PcCommandResponseMode = PcCommandResponseMode.Ack
221+
): String {
222+
return authenticatedCommand(
223+
id,
224+
deviceId,
225+
token,
226+
timestamp,
227+
"keyboard.textStream.open",
228+
JSONObject().put("streamId", streamId),
229+
responseMode
230+
)
231+
}
232+
233+
fun keyboardTextStreamChar(
234+
id: String,
235+
deviceId: String,
236+
token: String,
237+
timestamp: Long,
238+
streamId: String,
239+
seq: Int,
240+
text: String,
241+
responseMode: PcCommandResponseMode = PcCommandResponseMode.None
242+
): String {
243+
return authenticatedCommand(
244+
id,
245+
deviceId,
246+
token,
247+
timestamp,
248+
"keyboard.textStream.char",
249+
JSONObject().put("streamId", streamId).put("seq", seq).put("text", text),
250+
responseMode
251+
)
252+
}
253+
254+
fun keyboardTextStreamChunk(
255+
id: String,
256+
deviceId: String,
257+
token: String,
258+
timestamp: Long,
259+
streamId: String,
260+
seq: Int,
261+
text: String,
262+
responseMode: PcCommandResponseMode = PcCommandResponseMode.Ack
263+
): String {
264+
return authenticatedCommand(
265+
id,
266+
deviceId,
267+
token,
268+
timestamp,
269+
"keyboard.textStream.chunk",
270+
JSONObject().put("streamId", streamId).put("seq", seq).put("text", text),
271+
responseMode
272+
)
273+
}
274+
275+
fun keyboardTextStreamKey(
276+
id: String,
277+
deviceId: String,
278+
token: String,
279+
timestamp: Long,
280+
streamId: String,
281+
seq: Int,
282+
key: PcKeyboardKey,
283+
responseMode: PcCommandResponseMode = PcCommandResponseMode.None
284+
): String {
285+
return authenticatedCommand(
286+
id,
287+
deviceId,
288+
token,
289+
timestamp,
290+
"keyboard.textStream.key",
291+
JSONObject().put("streamId", streamId).put("seq", seq).put("key", key.protocolValue),
292+
responseMode
293+
)
294+
}
295+
296+
fun keyboardTextStreamClose(
297+
id: String,
298+
deviceId: String,
299+
token: String,
300+
timestamp: Long,
301+
streamId: String,
302+
expectedCount: Int,
303+
responseMode: PcCommandResponseMode = PcCommandResponseMode.Ack
304+
): String {
305+
return authenticatedCommand(
306+
id,
307+
deviceId,
308+
token,
309+
timestamp,
310+
"keyboard.textStream.close",
311+
JSONObject().put("streamId", streamId).put("expectedCount", expectedCount),
312+
responseMode
313+
)
314+
}
315+
214316
fun keyboardKey(
215317
id: String,
216318
deviceId: String,
@@ -369,9 +471,11 @@ object PcProtocol {
369471
return PcProtocolResponse.Invalid
370472
}
371473
val noAckCommands = parseNoAckCommands(capabilitiesJson) ?: return PcProtocolResponse.Invalid
474+
val supportedCommands = parseSupportedCommands(capabilitiesJson) ?: return PcProtocolResponse.Invalid
372475
val capabilities = PcPointerCapabilities(
373476
noAckMouseMove = capabilitiesJson?.optBoolean("noAckMouseMove", false) ?: false,
374-
noAckCommands = noAckCommands
477+
noAckCommands = noAckCommands,
478+
supportedCommands = supportedCommands
375479
)
376480
val bounds = PcPointerBounds(
377481
x = boundsJson.optInt("x"),
@@ -425,6 +529,22 @@ object PcProtocol {
425529
return commands
426530
}
427531

532+
private fun parseSupportedCommands(capabilitiesJson: JSONObject?): Set<String>? {
533+
if (capabilitiesJson == null || !capabilitiesJson.has("supportedCommands")) return emptySet()
534+
val commandsJson = capabilitiesJson.opt("supportedCommands")
535+
if (commandsJson !is JSONArray) return null
536+
537+
val commands = mutableSetOf<String>()
538+
for (index in 0 until commandsJson.length()) {
539+
val command = commandsJson.opt(index)
540+
if (command !is String) return null
541+
if (command in CONTROL_COMMAND_TYPES) {
542+
commands += command
543+
}
544+
}
545+
return commands
546+
}
547+
428548
private val NO_ACK_CONTROL_COMMAND_TYPES = setOf(
429549
"mouse.move",
430550
"mouse.click",
@@ -436,7 +556,18 @@ object PcProtocol {
436556
"keyboard.key",
437557
"keyboard.shortcut",
438558
"keyboard.typeText",
559+
"keyboard.textStream.char",
560+
"keyboard.textStream.key",
439561
"media.control",
440562
"window.control"
441563
)
564+
565+
private val CONTROL_COMMAND_TYPES = NO_ACK_CONTROL_COMMAND_TYPES + setOf(
566+
"connection.ping",
567+
"connection.disconnecting",
568+
"pointer.profile",
569+
"keyboard.textStream.open",
570+
"keyboard.textStream.chunk",
571+
"keyboard.textStream.close"
572+
)
442573
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ class SwitchifyPcBleClient(
286286
PcControlCommand.DoubleClick -> PcProtocol.mouseDoubleClick(id, deviceId, token, timestamp, responseMode = responseMode)
287287
PcControlCommand.RightClick -> PcProtocol.mouseRightClick(id, deviceId, token, timestamp, responseMode)
288288
is PcControlCommand.TypeText -> PcProtocol.keyboardTypeText(id, deviceId, token, timestamp, text, responseMode)
289+
is PcControlCommand.TextStreamOpen -> PcProtocol.keyboardTextStreamOpen(id, deviceId, token, timestamp, streamId, responseMode)
290+
is PcControlCommand.TextStreamChar -> PcProtocol.keyboardTextStreamChar(id, deviceId, token, timestamp, streamId, seq, text, responseMode)
291+
is PcControlCommand.TextStreamChunk -> PcProtocol.keyboardTextStreamChunk(id, deviceId, token, timestamp, streamId, seq, text, responseMode)
292+
is PcControlCommand.TextStreamKey -> PcProtocol.keyboardTextStreamKey(id, deviceId, token, timestamp, streamId, seq, key, responseMode)
293+
is PcControlCommand.TextStreamClose -> PcProtocol.keyboardTextStreamClose(id, deviceId, token, timestamp, streamId, expectedCount, responseMode)
289294
is PcControlCommand.PressKey -> PcProtocol.keyboardKey(id, deviceId, token, timestamp, key, responseMode)
290295
is PcControlCommand.WindowControl -> PcProtocol.windowControl(id, deviceId, token, timestamp, action, responseMode)
291296
}
@@ -385,6 +390,11 @@ private fun PcControlCommand.protocolType(): String {
385390
PcControlCommand.DoubleClick -> "mouse.doubleClick"
386391
PcControlCommand.RightClick -> "mouse.rightClick"
387392
is PcControlCommand.TypeText -> "keyboard.typeText"
393+
is PcControlCommand.TextStreamOpen -> "keyboard.textStream.open"
394+
is PcControlCommand.TextStreamChar -> "keyboard.textStream.char"
395+
is PcControlCommand.TextStreamChunk -> "keyboard.textStream.chunk"
396+
is PcControlCommand.TextStreamKey -> "keyboard.textStream.key"
397+
is PcControlCommand.TextStreamClose -> "keyboard.textStream.close"
388398
is PcControlCommand.PressKey -> "keyboard.key"
389399
is PcControlCommand.WindowControl -> "window.control"
390400
}

0 commit comments

Comments
 (0)