Recently Written · git

assistkey

Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords

git clone https://github.com/equwal/assistkey

Log | Files | Refs


commit 328b1248fba2a5aabc0071101d2803ac9fc686be
truex <truex@equwal.com>
2026-09-21 04:14:25 -0700

Add an action that shows or hides the button bar

Android 16 has no auto-hide for the three-button bar. ActionKind.NAV
with the payload toggle_bar switches the bar through shell access, with
the commands that the Navigation screen uses, and keeps the gestures as
the user set them. The allow step asks for shell access when an action
works through the shell: the button bar and the light.

 CHANGELOG.md                                             |  5 +++++
 .../main/java/dev/equwal/assistkey/model/ActionSpec.kt   |  6 ++++++
 .../main/java/dev/equwal/assistkey/native/NavNative.kt   | 16 ++++++++++++++++
 .../main/java/dev/equwal/assistkey/route/ActionRouter.kt |  1 +
 .../dev/equwal/assistkey/setup/GuidedSetupActivity.kt    | 15 +++++++++++++--
 .../java/dev/equwal/assistkey/ui/ActionPickerActivity.kt |  3 +++
 .../dev/equwal/assistkey/model/ActionSpecNameTest.kt     |  6 ++++++
 7 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f30fae8..e9f68c5 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`.
+- **Show or hide the button bar** is an action (build for direct install).
+  Android 16 has no auto-hide for the three-button bar, so a button does it.
+  It uses the shell commands of the Navigation screen and keeps the gestures
+  as they are. The allow step asks for shell access for this action and for
+  the light actions.
 - **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.
diff --git a/app/src/main/java/dev/equwal/assistkey/model/ActionSpec.kt b/app/src/main/java/dev/equwal/assistkey/model/ActionSpec.kt
index 23e5d1d..a904292 100644
--- a/app/src/main/java/dev/equwal/assistkey/model/ActionSpec.kt
+++ b/app/src/main/java/dev/equwal/assistkey/model/ActionSpec.kt
@@ -26,6 +26,12 @@ enum class ActionKind {
     /** payload = package name; launches its main activity */
     LAUNCH_APP,
 
+    /**
+     * payload = toggle_bar. Shows or hides the three-button bar through shell
+     * access. Android 16 has no auto-hide for that bar, so a button does it.
+     */
+    NAV,
+
     /** payload = "pkg/class"; explicit component, no intent-filter needed */
     LAUNCH_COMPONENT,
 
diff --git a/app/src/main/java/dev/equwal/assistkey/native/NavNative.kt b/app/src/main/java/dev/equwal/assistkey/native/NavNative.kt
index 9dd71f1..18f652d 100644
--- a/app/src/main/java/dev/equwal/assistkey/native/NavNative.kt
+++ b/app/src/main/java/dev/equwal/assistkey/native/NavNative.kt
@@ -77,6 +77,22 @@ object NavNative {
         return out
     }
 
+    const val PREFS = "assistkey_nav"
+
+    /**
+     * Shows the button bar when it is hidden, and hides it when it shows. The
+     * gestures stay as the user set them. It needs shell access. False where
+     * there is none.
+     */
+    fun toggleBar(c: Context): Boolean {
+        if (!dev.equwal.assistkey.shell.Shell.ready) return false
+        val p = c.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+        val buttons = !p.getBoolean("buttons", true)
+        p.edit().putBoolean("buttons", buttons).apply()
+        dev.equwal.assistkey.shell.Shell.runAll(shellCommands(buttons, p.getBoolean("gestures", true)))
+        return true
+    }
+
     /** The same, for someone typing them at a computer. */
     fun commands(buttons: Boolean, gestures: Boolean): List<String> =
         shellCommands(buttons, gestures).filterNot { it.startsWith("sleep") }.distinct().map { "adb shell $it" }
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 0a4324c..47a7bb1 100644
--- a/app/src/main/java/dev/equwal/assistkey/route/ActionRouter.kt
+++ b/app/src/main/java/dev/equwal/assistkey/route/ActionRouter.kt
@@ -37,6 +37,7 @@ object ActionRouter {
                     dev.equwal.assistkey.display.ExtraDim.act(ctx.applicationContext, spec.payload)
                     true
                 }
+                ActionKind.NAV -> dev.equwal.assistkey.native.NavNative.toggleBar(ctx.applicationContext)
                 ActionKind.VOICE -> ServiceHolder.service
                     ?.let { dev.equwal.assistkey.voice.Dictation.toggle(it) } ?: false
             }
diff --git a/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt b/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt
index 6c5523d..bf059c1 100644
--- a/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt
+++ b/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt
@@ -295,7 +295,8 @@ class GuidedSetupActivity : Activity() {
 
         val missing = plan.needs.filterNot(::satisfied)
         val microphone = spec.kind == ActionKind.VOICE && !Dictation.hasMicrophone(this)
-        if (plan.possible && missing.isEmpty() && !microphone) return if (askOnly) finish() else go(Step.DONE)
+        val shell = needsShell(spec) && !Shell.ready
+        if (plan.possible && missing.isEmpty() && !microphone && !shell) return if (askOnly) finish() else go(Step.DONE)
 
         plan.blocked?.let {
             col.note(it)
@@ -309,6 +310,11 @@ class GuidedSetupActivity : Activity() {
                 requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 1)
             }
         }
+        if (shell) {
+            col.row("Turn on shell access", "This action works through Shizuku") {
+                startActivity(Intent(this, ShellActivity::class.java))
+            }
+        }
         if (Need.KEY_FILTER in missing) {
             col.note("If the switch turns itself off, allow restricted settings in App info.")
             col.button("Open App info") { Channels.safeStart(this, Channels.appInfoIntent(this)) }
@@ -374,9 +380,14 @@ class GuidedSetupActivity : Activity() {
             val needsFilter = ActionRouter.requiresAccessibility(spec) || spec.kind == ActionKind.MENU
             val plan = Route.plan(t, needsFilter, env(c))
             val microphone = spec.kind == ActionKind.VOICE && !Dictation.hasMicrophone(c)
-            return !(plan.possible && plan.needs.all { satisfied(c, it) } && !microphone)
+            val shell = needsShell(spec) && !Shell.ready
+            return !(plan.possible && plan.needs.all { satisfied(c, it) } && !microphone && !shell)
         }
 
+        /** The light and the button bar are set by shell commands. */
+        private fun needsShell(spec: ActionSpec): Boolean =
+            Shell.SUPPORTED && (spec.kind == ActionKind.NAV || spec.kind == ActionKind.DIM)
+
         private fun env(c: android.content.Context): Route.Env =
             Route.Env(powerIsDirect = Shell.ready && PowerControl.wanted(c), shellSupported = Shell.SUPPORTED)
 
diff --git a/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt
index a404c0f..ef8f939 100644
--- a/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt
+++ b/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt
@@ -65,6 +65,8 @@ class ActionPickerActivity : Activity() {
         viwoods(col)
         navigation(col)
         if (dev.equwal.assistkey.shell.Shell.SUPPORTED) {
+            col.header("Button bar")
+            col.row(NAV_TOGGLE.label, "Needs shell access") { choose(NAV_TOGGLE) }
             col.header("Light")
             listOf("toggle", "darker", "brighter", "system_up", "system_down").forEach { what ->
                 val spec = ActionSpec(ActionKind.DIM, what, "")
@@ -231,6 +233,7 @@ class ActionPickerActivity : Activity() {
     companion object {
         private const val EXTRA_TRIGGER = "trigger"
         private const val EXTRA_PICK_ONLY = "pick_only"
+        private val NAV_TOGGLE = ActionSpec(ActionKind.NAV, "toggle_bar", "Show or hide the button bar")
 
         /** Set by the guided setup, which asks for what a binding needs by itself. */
         const val EXTRA_NO_ASK = "no_ask"
diff --git a/app/src/test/java/dev/equwal/assistkey/model/ActionSpecNameTest.kt b/app/src/test/java/dev/equwal/assistkey/model/ActionSpecNameTest.kt
index de5fcd9..0456411 100644
--- a/app/src/test/java/dev/equwal/assistkey/model/ActionSpecNameTest.kt
+++ b/app/src/test/java/dev/equwal/assistkey/model/ActionSpecNameTest.kt
@@ -21,4 +21,10 @@ class ActionSpecNameTest {
     @Test fun `other actions keep their stored label`() {
         assertEquals("Back", ActionSpec(ActionKind.GLOBAL, "BACK", "Back").describe())
     }
+
+    @Test fun `the button bar action has a name a person can read, and survives a save`() {
+        val spec = ActionSpec(ActionKind.NAV, "toggle_bar", "Show or hide the button bar")
+        assertEquals("Show or hide the button bar", spec.describe())
+        assertEquals(ActionKind.NAV, ActionKind.fromName("NAV"))
+    }
 }