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 5ff0c95e5e678b5eb3e639443da57dc84ef0536d
truex <truex@equwal.com>
2026-09-21 03:48:48 -0700

Let the user change a button from the Done step

The Done step has two buttons: Change what it does, and Change how you
press it. They go back to those steps with the button and the gesture
kept, so that the user can adjust a binding while he tests it. Back on
the Done step goes to the actions too.

Route.back holds the rule for Back as a pure function, with a test for
each branch. The step that follows a return from the action list now
runs only after the screen was really left for that list.

 CHANGELOG.md                                       |  6 ++++
 .../equwal/assistkey/setup/GuidedSetupActivity.kt  | 38 ++++++++++++++++------
 .../main/java/dev/equwal/assistkey/setup/Route.kt  | 19 +++++++++++
 .../java/dev/equwal/assistkey/setup/RouteTest.kt   | 28 ++++++++++++++++
 4 files changed, 81 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7659600..33a20d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## Unreleased
+
+- **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`.
+
 ## 0.0.12-alpha - 2026-09-21
 
 Version code 12.
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 85bb9c5..bf5f38d 100644
--- a/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt
+++ b/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt
@@ -20,6 +20,7 @@ import dev.equwal.assistkey.model.Trigger
 import dev.equwal.assistkey.route.ActionRouter
 import dev.equwal.assistkey.route.ServiceHolder
 import dev.equwal.assistkey.setup.Route.Need
+import dev.equwal.assistkey.setup.Route.Step
 import dev.equwal.assistkey.shell.PowerControl
 import dev.equwal.assistkey.shell.Shell
 import dev.equwal.assistkey.store.Store
@@ -52,8 +53,6 @@ import dev.equwal.assistkey.voice.Dictation
  */
 class GuidedSetupActivity : Activity() {
 
-    private enum class Step { BUTTON, PRESS, ACTION, ALLOW, DONE }
-
     private var step = Step.BUTTON
     private var keys: List<HwKey> = emptyList()
     private var trigger: Trigger? = null
@@ -64,6 +63,13 @@ class GuidedSetupActivity : Activity() {
     /** True when another screen made the binding. This screen then only asks, and leaves. */
     private var askOnly = false
 
+    /**
+     * True after the menu editor or the full action list is opened from the
+     * ACTION step. It marks a real trip away from this screen, so onResume
+     * can tell that from a return to ACTION chosen on purpose (from DONE).
+     */
+    private var leftForPicker = false
+
     private fun tokens(text: String?): List<HwKey> =
         text.orEmpty().split('+').mapNotNull(HwKey::fromToken)
 
@@ -76,6 +82,7 @@ class GuidedSetupActivity : Activity() {
             keys = tokens(s.getString("keys"))
             trigger = s.getString("trigger")?.let(Trigger::parse)
             fromHub = s.getBoolean("fromHub")
+            leftForPicker = s.getBoolean("leftForPicker")
         } ?: intent.getStringExtra(EXTRA_ASK)?.let(Trigger::parse)?.let {
             // Another screen made this binding. Ask for what it needs, and nothing else.
             trigger = it
@@ -95,6 +102,7 @@ class GuidedSetupActivity : Activity() {
         out.putString("step", step.name)
         out.putString("keys", keys.joinToString("+") { it.token })
         out.putBoolean("fromHub", fromHub)
+        out.putBoolean("leftForPicker", leftForPicker)
         trigger?.let { out.putString("trigger", it.id) }
     }
 
@@ -102,18 +110,19 @@ class GuidedSetupActivity : Activity() {
         super.onResume()
         Shell.connect(this)
         // Back from the action list or the menu editor: a binding now exists, or not.
-        if (step == Step.ACTION && trigger?.let { Store.bindings(this).isBound(it) } == true) step = Step.ALLOW
+        // This runs only after a real trip to one of those screens, not after
+        // a plain return to ACTION chosen from the DONE step.
+        if (leftForPicker) {
+            leftForPicker = false
+            if (step == Step.ACTION && trigger?.let { Store.bindings(this).isBound(it) } == true) step = Step.ALLOW
+        }
         build()
     }
 
     @Deprecated("Deprecated in Java")
     override fun onBackPressed() {
-        when (step) {
-            Step.BUTTON, Step.DONE -> finish()
-            Step.ALLOW -> if (askOnly) finish() else go(Step.ACTION)
-            Step.PRESS -> if (fromHub) finish() else go(Step.BUTTON)
-            else -> { step = Step.entries[step.ordinal - 1]; build() }
-        }
+        val next = Route.back(step, fromHub, askOnly)
+        if (next == null) finish() else go(next)
     }
 
     private fun go(next: Step) {
@@ -228,8 +237,12 @@ class GuidedSetupActivity : Activity() {
                 go(Step.ALLOW)
             }
         }
-        col.row("A menu of actions", "One press, many choices") { startActivity(MenuEditActivity.intent(this, t)) }
+        col.row("A menu of actions", "One press, many choices") {
+            leftForPicker = true
+            startActivity(MenuEditActivity.intent(this, t))
+        }
         col.row("Advanced", "Every action") {
+            leftForPicker = true
             startActivity(ActionPickerActivity.intent(this, t).putExtra(ActionPickerActivity.EXTRA_NO_ASK, true))
         }
     }
@@ -316,6 +329,11 @@ class GuidedSetupActivity : Activity() {
                 Channels.safeStart(this, Channels.claimIntent(this, Channel.WALLET))
             }
         }
+        col.button("Change what it does") { go(Step.ACTION) }
+        // The on-screen button has one gesture, so there is nothing to change here.
+        if (keys.singleOrNull() != HwKey.SCREEN) {
+            col.button("Change how you press it") { go(Step.PRESS) }
+        }
         col.button("Set up another button") {
             keys = emptyList()
             trigger = null
diff --git a/app/src/main/java/dev/equwal/assistkey/setup/Route.kt b/app/src/main/java/dev/equwal/assistkey/setup/Route.kt
index 00dbabf..a4e1897 100644
--- a/app/src/main/java/dev/equwal/assistkey/setup/Route.kt
+++ b/app/src/main/java/dev/equwal/assistkey/setup/Route.kt
@@ -15,6 +15,25 @@ import dev.equwal.assistkey.model.Trigger
  */
 object Route {
 
+    /** A step of the guided setup screen. The single definition; the activity uses this one. */
+    enum class Step { BUTTON, PRESS, ACTION, ALLOW, DONE }
+
+    /**
+     * Where hardware Back leads from [step]. Null means leave the screen.
+     *
+     * [fromHub] is true when the main screen chose the button, so Back from
+     * PRESS leaves instead of going to BUTTON. [askOnly] is true when another
+     * screen made the binding and this screen only asks, so Back from ALLOW
+     * or DONE leaves instead of going back into the flow.
+     */
+    fun back(step: Step, fromHub: Boolean, askOnly: Boolean): Step? = when (step) {
+        Step.BUTTON -> null
+        Step.PRESS -> if (fromHub) null else Step.BUTTON
+        Step.ACTION -> Step.PRESS
+        Step.ALLOW -> if (askOnly) null else Step.ACTION
+        Step.DONE -> if (askOnly) null else Step.ACTION
+    }
+
     /** One thing the user must allow. The order of the entries is the order to ask in. */
     enum class Need(val title: String, val why: String) {
         KEY_FILTER(
diff --git a/app/src/test/java/dev/equwal/assistkey/setup/RouteTest.kt b/app/src/test/java/dev/equwal/assistkey/setup/RouteTest.kt
index 884d849..15f6bb2 100644
--- a/app/src/test/java/dev/equwal/assistkey/setup/RouteTest.kt
+++ b/app/src/test/java/dev/equwal/assistkey/setup/RouteTest.kt
@@ -4,8 +4,10 @@ import dev.equwal.assistkey.model.GestureType
 import dev.equwal.assistkey.model.HwKey
 import dev.equwal.assistkey.model.Trigger
 import dev.equwal.assistkey.setup.Route.Need
+import dev.equwal.assistkey.setup.Route.Step
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Test
 
@@ -130,4 +132,30 @@ class RouteTest {
         assertEquals(listOf(Need.ASSISTANT), Route.plan(Trigger(power, GestureType.HOLD), false, fullBuild).needs)
         assertEquals(Trigger.MAX_TAPS + 1, Route.gestures(power, fullBuild).size)
     }
+
+    @Test fun `Back on the button step always leaves`() {
+        assertNull(Route.back(Step.BUTTON, fromHub = false, askOnly = false))
+        assertNull(Route.back(Step.BUTTON, fromHub = true, askOnly = true))
+    }
+
+    @Test fun `Back on the press step leaves when the hub chose the button, else goes to the button step`() {
+        assertNull(Route.back(Step.PRESS, fromHub = true, askOnly = false))
+        assertEquals(Step.BUTTON, Route.back(Step.PRESS, fromHub = false, askOnly = false))
+    }
+
+    @Test fun `Back on the action step always goes to the press step`() {
+        assertEquals(Step.PRESS, Route.back(Step.ACTION, fromHub = false, askOnly = false))
+        assertEquals(Step.PRESS, Route.back(Step.ACTION, fromHub = true, askOnly = true))
+    }
+
+    @Test fun `Back on the allow step leaves for an ask-only screen, else goes to the action step`() {
+        assertNull(Route.back(Step.ALLOW, fromHub = false, askOnly = true))
+        assertEquals(Step.ACTION, Route.back(Step.ALLOW, fromHub = false, askOnly = false))
+    }
+
+    @Test fun `Back on the done step leaves for an ask-only screen, else goes to the action step to edit again`() {
+        assertNull(Route.back(Step.DONE, fromHub = false, askOnly = true))
+        assertEquals(Step.ACTION, Route.back(Step.DONE, fromHub = false, askOnly = false))
+        assertEquals(Step.ACTION, Route.back(Step.DONE, fromHub = true, askOnly = false))
+    }
 }