fix(sidecar): find the erased field by class, past the keyboard's own focus

The check that decides whether the select-all worked looked for an
"editable" attribute maestro's tree does not carry, so it answered
"cannot tell" every time and every erase paid the per-character
fallback. Worse, an open keyboard puts a second focused node in the
tree, one of the IME's own keys, carrying no text: taking the first
focused node would read a field still holding 4096 characters as empty,
which is the one answer that stops the erase early.

Match the text field by class instead. Measured against the real
backend, 4096 characters now clear in 385ms on API 34, 409ms on API 35
and 870ms on API 36, verified empty, where the fallback took ~4s.
This commit is contained in:
pj committed 2026-08-15 22:19:49 +05:30
1 parent b3a565e9e9
commit 3df8a3b3ba
1 file changed
+14 -7
@@ -957,31 +957,38 @@ internal fun deleteKeyCommands(count: Int, batch: Int): List<String> {
} }
} }
// focusedEditableTextLength reports how much text the focused editable node // focusedEditableTextLength reports how much text the focused text field
// holds, or null when the tree names no focused editable node. Null is // holds, or null when the tree names no focused text field. Null is "cannot
// "cannot tell", which is not the same as empty and must not be read as it. // tell", which is not the same as empty and must not be read as it.
//
// The field is found by class, not by an "editable" attribute: maestro's tree
// carries no such attribute. Class also settles the trap an open keyboard
// sets, which is that the IME contributes a focused node of its own. That node
// holds no text, so taking the first focused node would read a field still
// holding 4096 characters as empty, and empty is the answer that stops the
// erase.
internal fun focusedEditableTextLength(treeJson: String): Int? { internal fun focusedEditableTextLength(treeJson: String): Int? {
if (treeJson.isBlank()) return null if (treeJson.isBlank()) return null
return try { return try {
focusedEditableLength(jsonMapper.readTree(treeJson)) focusedFieldLength(jsonMapper.readTree(treeJson))
} catch (_: Exception) { } catch (_: Exception) {
null null
} }
} }
private fun focusedEditableLength( private fun focusedFieldLength(
node: com.fasterxml.jackson.databind.JsonNode, node: com.fasterxml.jackson.databind.JsonNode,
): Int? { ): Int? {
val attributes = node.get("attributes") val attributes = node.get("attributes")
if (attributes != null && attributes.isObject && if (attributes != null && attributes.isObject &&
attributes.get("focused")?.asText() == "true" && attributes.get("focused")?.asText() == "true" &&
attributes.get("editable")?.asText() == "true" attributes.get("class")?.asText().orEmpty().endsWith("EditText")
) { ) {
return attributes.get("text")?.asText().orEmpty().length return attributes.get("text")?.asText().orEmpty().length
} }
val children = node.get("children") ?: return null val children = node.get("children") ?: return null
if (!children.isArray) return null if (!children.isArray) return null
for (child in children) focusedEditableLength(child)?.let { return it } for (child in children) focusedFieldLength(child)?.let { return it }
return null return null
} }