commit 189bad36a4deca54fb94fdddba9c7f460c53f396 truex <truex@equwal.com> 2026-09-21 04:05:05 -0700 Add the Viwoods AI voice prompt as an action On a hold of the AI key, the firmware starts WebViewAiActivity of the Viwoods AI app with the text extra recodeKey=recode_key_start. The action AI voice prompt does the same, so that any button can have it. The payload of LAUNCH_COMPONENT can carry text extras after a question mark. ComponentPayload parses it with no Android types. A payload from before reads as it did.
CHANGELOG.md | 5 +++ .../dev/equwal/assistkey/model/ComponentPayload.kt | 30 ++++++++++++++++++ .../java/dev/equwal/assistkey/model/Presets.kt | 2 ++ .../dev/equwal/assistkey/route/ActionRouter.kt | 4 ++- .../equwal/assistkey/model/ComponentPayloadTest.kt | 37 ++++++++++++++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3831071..f30fae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ - **Change a button from the Done step.** "Change what it does" and "Change how you press it" go back to those steps with the button kept. Back on the Done step goes to the actions too. Tests: `Route.back`. +- **AI voice prompt** is an action of its own (Viwoods). It is what the + firmware does on a hold of the AI key: the Viwoods AI screen, told to start + its voice prompt. It needs the Viwoods AI account, as the stock hold does. + A component launch can now carry text extras (`pkg/class?key=value`). + Tests: `ComponentPayloadTest`. - **Ask when the user tries it.** The first run asks for every grant once. After that the app asks at the moment something is missing: after a menu is saved, after the Power switch on the Navigation screen, when a Power press diff --git a/app/src/main/java/dev/equwal/assistkey/model/ComponentPayload.kt b/app/src/main/java/dev/equwal/assistkey/model/ComponentPayload.kt new file mode 100644 index 0000000..4aea663 --- /dev/null +++ b/app/src/main/java/dev/equwal/assistkey/model/ComponentPayload.kt @@ -0,0 +1,30 @@ +package dev.equwal.assistkey.model + +/** + * The payload of [ActionKind.LAUNCH_COMPONENT]: "pkg/class", and after a "?" + * the text extras of the intent as "key=value" with "&" between them. + * + * The extras exist for a screen that does something else when it gets one. + * The Viwoods AI screen starts its voice prompt with recodeKey=recode_key_start. + * A payload from before has no "?" and reads as it always did. + */ +object ComponentPayload { + + data class Parts(val component: String, val extras: Map<String, String>) + + fun parse(payload: String): Parts { + val component = payload.substringBefore('?') + val extras = LinkedHashMap<String, String>() + if ('?' in payload) { + payload.substringAfter('?').split('&').forEach { pair -> + val key = pair.substringBefore('=') + if (key.isNotEmpty() && '=' in pair) extras[key] = pair.substringAfter('=') + } + } + return Parts(component, extras) + } + + fun format(component: String, extras: Map<String, String>): String = + if (extras.isEmpty()) component + else component + "?" + extras.entries.joinToString("&") { it.key + "=" + it.value } +} diff --git a/app/src/main/java/dev/equwal/assistkey/model/Presets.kt b/app/src/main/java/dev/equwal/assistkey/model/Presets.kt index e5f1388..69bf482 100644 --- a/app/src/main/java/dev/equwal/assistkey/model/Presets.kt +++ b/app/src/main/java/dev/equwal/assistkey/model/Presets.kt @@ -23,6 +23,8 @@ object Presets { Preset("AI quick prompt (transparent)", AI + "/com.wisky.wiskyai.AiTransparentActivity"), Preset("AI lookup", AI + "/com.wisky.wiskyai.AIActivity"), Preset("AI assistant (full UI)", AI + "/com.wisky.wiskyai.WebViewAiActivity"), + // What the firmware does on a hold of the AI key: the same screen, told to start the voice prompt. + Preset("AI voice prompt", AI + "/com.wisky.wiskyai.WebViewAiActivity?recodeKey=recode_key_start"), Preset("AI history", AI + "/com.wisky.wiskyai.HistroyActivity"), Preset("AI repository", AI + "/com.wisky.wiskyai.RepositoryActivity"), Preset("Viwoods home", LAUNCHER + "/com.viwoods.launcher.main.LauncherMainActivity") diff --git a/app/src/main/java/dev/equwal/assistkey/route/ActionRouter.kt b/app/src/main/java/dev/equwal/assistkey/route/ActionRouter.kt index e5fd816..0a4324c 100644 --- a/app/src/main/java/dev/equwal/assistkey/route/ActionRouter.kt +++ b/app/src/main/java/dev/equwal/assistkey/route/ActionRouter.kt @@ -115,9 +115,11 @@ object ActionRouter { * the internal Viwoods AI activities get reached. */ private fun launchComponent(ctx: Context, flat: String): Boolean { - val cn = ComponentName.unflattenFromString(flat) ?: return false + val parts = dev.equwal.assistkey.model.ComponentPayload.parse(flat) + val cn = ComponentName.unflattenFromString(parts.component) ?: return false val i = Intent() i.component = cn + parts.extras.forEach { (key, value) -> i.putExtra(key, value) } i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ctx.startActivity(i) return true diff --git a/app/src/test/java/dev/equwal/assistkey/model/ComponentPayloadTest.kt b/app/src/test/java/dev/equwal/assistkey/model/ComponentPayloadTest.kt new file mode 100644 index 0000000..495a462 --- /dev/null +++ b/app/src/test/java/dev/equwal/assistkey/model/ComponentPayloadTest.kt @@ -0,0 +1,37 @@ +package dev.equwal.assistkey.model + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.Random + +class ComponentPayloadTest { + + @Test fun `a payload from before has no extras and reads as it did`() { + val old = "com.viwoods.viwoodsai/com.wisky.wiskyai.AIActivity" + assertEquals(ComponentPayload.Parts(old, emptyMap()), ComponentPayload.parse(old)) + } + + @Test fun `the Viwoods voice prompt carries its one extra`() { + val parts = ComponentPayload.parse( + "com.viwoods.viwoodsai/com.wisky.wiskyai.WebViewAiActivity?recodeKey=recode_key_start" + ) + assertEquals("com.viwoods.viwoodsai/com.wisky.wiskyai.WebViewAiActivity", parts.component) + assertEquals(mapOf("recodeKey" to "recode_key_start"), parts.extras) + } + + @Test fun `junk after the question mark is skipped, not thrown`() { + assertEquals(mapOf("a" to "1"), ComponentPayload.parse("p/c?&=x&novalue&a=1").extras) + assertEquals(mapOf("a" to "b=c"), ComponentPayload.parse("p/c?a=b=c").extras) + } + + @Test fun `parse gives back what format was given`() { + val r = Random(21) + val letters = "abcXYZ019_." + fun word() = (0..r.nextInt(6)).map { letters[r.nextInt(letters.length)] }.joinToString("") + repeat(500) { + val extras = (0 until r.nextInt(4)).associate { word() to word() } + val parts = ComponentPayload.parse(ComponentPayload.format("pkg/" + word(), extras)) + assertEquals(extras, parts.extras) + } + } +}