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 a4f50eb15068384a7ee0df89da5a38e11e1279cd
truex <truex@equwal.com>
2026-09-21 03:55:29 -0700

Ask for a grant at the moment the user needs it

GuidedSetupActivity.askIfMissing takes any context and says whether it
opened the allow step. The menu editor calls it after a save. The
Navigation screen calls it for the first Power binding that needs
something. A Power press that arrives while button remapping is off
opens the allow step and no longer shows a toast, which the reader
hides. Voice typing with no microphone grant opens the Voice typing
screen.

 CHANGELOG.md                                       |  5 ++++
 .../dev/equwal/assistkey/channel/ChannelEntry.kt   |  7 ++----
 .../dev/equwal/assistkey/menu/MenuEditActivity.kt  |  4 ++++
 .../equwal/assistkey/setup/GuidedSetupActivity.kt  | 27 ++++++++++++++--------
 .../equwal/assistkey/ui/ActionPickerActivity.kt    |  6 ++++-
 .../dev/equwal/assistkey/ui/NavigationActivity.kt  |  4 ++++
 .../java/dev/equwal/assistkey/voice/Dictation.kt   | 10 +++++++-
 7 files changed, 47 insertions(+), 16 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f4e300..3831071 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`.
+- **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
+  arrives while button remapping is off, and when voice typing starts with no
+  microphone grant. Before, those places showed a short message or nothing.
 - **Fix: system Back always closed the button setup.** From Android 13 the
   system does not call `onBackPressed` for an app with this target version.
   The setup now registers the Back callback, and Back goes one step back.
diff --git a/app/src/main/java/dev/equwal/assistkey/channel/ChannelEntry.kt b/app/src/main/java/dev/equwal/assistkey/channel/ChannelEntry.kt
index 6f9f824..3d4ec09 100644
--- a/app/src/main/java/dev/equwal/assistkey/channel/ChannelEntry.kt
+++ b/app/src/main/java/dev/equwal/assistkey/channel/ChannelEntry.kt
@@ -61,11 +61,8 @@ object ChannelEntry {
 
         if (ActionRouter.requiresAccessibility(spec)) {
             if (!ServiceHolder.isRunning) {
-                Toast.makeText(
-                    activity,
-                    "Turn on button remapping first",
-                    Toast.LENGTH_SHORT
-                ).show()
+                // The user tried the button, and something is not allowed yet. Ask now.
+                dev.equwal.assistkey.setup.GuidedSetupActivity.askIfMissing(activity.applicationContext, trigger)
                 activity.finishAndVanish()
                 return
             }
diff --git a/app/src/main/java/dev/equwal/assistkey/menu/MenuEditActivity.kt b/app/src/main/java/dev/equwal/assistkey/menu/MenuEditActivity.kt
index 5200e6f..27083f4 100644
--- a/app/src/main/java/dev/equwal/assistkey/menu/MenuEditActivity.kt
+++ b/app/src/main/java/dev/equwal/assistkey/menu/MenuEditActivity.kt
@@ -59,6 +59,10 @@ class MenuEditActivity : Activity() {
 
         col.primaryButton(if (items.isEmpty()) "Remove the menu" else "Save the menu") {
             Store.bind(this, trigger, if (items.isEmpty()) ActionSpec.PASS else Menu.spec(items))
+            // Ask now for what the menu needs. The guided setup asks by itself.
+            if (items.isNotEmpty() && !intent.getBooleanExtra(ActionPickerActivity.EXTRA_NO_ASK, false)) {
+                dev.equwal.assistkey.setup.GuidedSetupActivity.askIfMissing(this, trigger)
+            }
             setResult(RESULT_OK)
             finish()
         }
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 7ade463..6c5523d 100644
--- a/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt
+++ b/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt
@@ -247,7 +247,7 @@ class GuidedSetupActivity : Activity() {
         }
         col.row("A menu of actions", "One press, many choices") {
             leftForPicker = true
-            startActivity(MenuEditActivity.intent(this, t))
+            startActivity(MenuEditActivity.intent(this, t).putExtra(ActionPickerActivity.EXTRA_NO_ASK, true))
         }
         col.row("Advanced", "Every action") {
             leftForPicker = true
@@ -356,16 +356,25 @@ class GuidedSetupActivity : Activity() {
         private const val EXTRA_ASK = "ask"
 
         /**
-         * For a screen that has just made a binding for [t]. Opens the allow
-         * step when the binding needs something the user has not allowed yet.
+         * For any code that has just made a binding for [t], or has just tried
+         * to run it. Opens the allow step when the binding needs something the
+         * user has not allowed yet. True when it opened the step.
          */
-        fun askIfMissing(a: Activity, t: Trigger) {
-            val spec = Store.bindings(a)[t] ?: return
+        fun askIfMissing(c: android.content.Context, t: Trigger): Boolean {
+            if (!missing(c, t)) return false
+            val ask = Intent(c, GuidedSetupActivity::class.java).putExtra(EXTRA_ASK, t.id)
+            // A service or a channel entry is not a screen. It needs a task of its own.
+            if (c !is Activity) ask.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            return runCatching { c.startActivity(ask) }.isSuccess
+        }
+
+        /** True when the binding of [t] needs something that the user has not allowed yet. */
+        fun missing(c: android.content.Context, t: Trigger): Boolean {
+            val spec = Store.bindings(c)[t] ?: return false
             val needsFilter = ActionRouter.requiresAccessibility(spec) || spec.kind == ActionKind.MENU
-            val plan = Route.plan(t, needsFilter, env(a))
-            val microphone = spec.kind == ActionKind.VOICE && !Dictation.hasMicrophone(a)
-            if (plan.possible && plan.needs.all { satisfied(a, it) } && !microphone) return
-            a.startActivity(Intent(a, GuidedSetupActivity::class.java).putExtra(EXTRA_ASK, t.id))
+            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)
         }
 
         private fun env(c: android.content.Context): Route.Env =
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 552dff7..a404c0f 100644
--- a/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt
+++ b/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt
@@ -48,7 +48,11 @@ class ActionPickerActivity : Activity() {
             col.row("Now", Store.bindings(this).raw(trigger).describe(), enabled = false)
             col.header("More than one")
             col.row("Build a menu", "As many actions as you want") {
-                startActivityForResult(dev.equwal.assistkey.menu.MenuEditActivity.intent(this, trigger), MENU)
+                startActivityForResult(
+                    dev.equwal.assistkey.menu.MenuEditActivity.intent(this, trigger)
+                        .putExtra(EXTRA_NO_ASK, intent.getBooleanExtra(EXTRA_NO_ASK, false)),
+                    MENU
+                )
             }
             dev.equwal.assistkey.menu.Menu.presets(this).forEach { (name, items) ->
                 col.row(name + " menu", items.joinToString(", ") { it.describe() }) {
diff --git a/app/src/main/java/dev/equwal/assistkey/ui/NavigationActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/NavigationActivity.kt
index 61778a9..0d320df 100644
--- a/app/src/main/java/dev/equwal/assistkey/ui/NavigationActivity.kt
+++ b/app/src/main/java/dev/equwal/assistkey/ui/NavigationActivity.kt
@@ -16,6 +16,7 @@ import dev.equwal.assistkey.model.HwKey
 import dev.equwal.assistkey.model.Trigger
 import dev.equwal.assistkey.native.NavNative
 import dev.equwal.assistkey.route.ServiceHolder
+import dev.equwal.assistkey.setup.GuidedSetupActivity
 import dev.equwal.assistkey.shell.PowerControl
 import dev.equwal.assistkey.shell.Shell
 import dev.equwal.assistkey.store.Store
@@ -117,6 +118,9 @@ class NavigationActivity : Activity() {
             Channels.setEnabled(this, Channel.ASSISTANT, true)
         }
         (ServiceHolder.service as? KeyFilterService)?.syncPower()
+        // One question at a time: the first of these bindings that needs something.
+        if (on) defaults.map { it.first }.firstOrNull { GuidedSetupActivity.missing(this, it) }
+            ?.let { GuidedSetupActivity.askIfMissing(this, it) }
     }
 
     // ---- the bar and the gestures ----------------------------------------------------------
diff --git a/app/src/main/java/dev/equwal/assistkey/voice/Dictation.kt b/app/src/main/java/dev/equwal/assistkey/voice/Dictation.kt
index a984251..b70829b 100644
--- a/app/src/main/java/dev/equwal/assistkey/voice/Dictation.kt
+++ b/app/src/main/java/dev/equwal/assistkey/voice/Dictation.kt
@@ -110,7 +110,15 @@ object Dictation {
             it()
             return true
         }
-        if (!hasMicrophone(svc)) return fail(svc, "Voice typing needs the microphone")
+        if (!hasMicrophone(svc)) {
+            // The user tried voice typing. Open the screen that asks for the microphone.
+            runCatching {
+                svc.startActivity(
+                    Intent(svc, dev.equwal.assistkey.ui.VoiceActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                )
+            }
+            return false
+        }
         if (!available(svc)) return fail(svc, "No speech app on this device")
         return runCatching {
             svc.startActivity(