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 a45553746c352033b7f7c80e9134266b7f513d33
truex <truex@equwal.com>
2026-09-21 03:03:52 -0700

Fix a Camera key in the drawing of a device that has none

The Viwoods reader declares a camera key to Android and has no such
button. Device.keys added the keys that detection found, so the drawing
showed a Camera key. A declared key is not a button.

Device.keys now lists the keys of the device profile and the keys that
were really pressed in normal use. The Detect screen still lists what
the device declares, and marks a key that was never seen.

DeviceKeysTest holds the input that caused the defect.

Version 0.0.11-alpha, version code 11.

 CHANGELOG.md                                       | 13 ++++++++
 .../java/dev/equwal/assistkey/device/Device.kt     | 25 +++++++-------
 .../java/dev/equwal/assistkey/ui/DetectActivity.kt | 11 ++++--
 .../main/java/dev/equwal/assistkey/ui/Summary.kt   |  4 +--
 .../dev/equwal/assistkey/device/DeviceKeysTest.kt  | 39 ++++++++++++++++++++++
 .../java/dev/equwal/assistkey/ui/SummaryTest.kt    |  6 ++--
 gradle.properties                                  |  4 +--
 7 files changed, 81 insertions(+), 21 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cbf437..890f6e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
 # Changelog
 
+## 0.0.11-alpha - 2026-09-21
+
+Version code 11.
+
+- **Fix: a Camera key in the drawing of a device that has none.** The Viwoods
+  reader declares a camera key to Android and has no such button. Detection
+  put the declared key in the drawing. The drawing now shows only the buttons
+  that the device profile knows, and a button that was really pressed in
+  normal use. The Detect screen still lists what the device declares, and
+  marks a key that was never seen. Regression test: `DeviceKeysTest`.
+  The double tap of Power uses the camera *intent* of Android. It needs no
+  camera button.
+
 ## 0.0.10-alpha - 2026-09-21
 
 Version code 10.
diff --git a/app/src/main/java/dev/equwal/assistkey/device/Device.kt b/app/src/main/java/dev/equwal/assistkey/device/Device.kt
index d0649a6..561c2f8 100644
--- a/app/src/main/java/dev/equwal/assistkey/device/Device.kt
+++ b/app/src/main/java/dev/equwal/assistkey/device/Device.kt
@@ -74,20 +74,21 @@ object Device {
         else listOf(HwKey.VOL_UP, HwKey.VOL_DOWN)
 
     /**
-     * The remappable keys to show: what the profile knows about, what detection
-     * found, plus any other supported key this device has actually produced.
+     * The remappable keys to show: what the profile knows about, plus any other
+     * supported key this device has actually produced in normal use.
      *
-     * Detection only adds. A profile key stays on the list even if no source
-     * reports it, because a firmware hook can hide a key that is really there.
+     * What [Detect] finds is NOT on this list. A device declares keys it does
+     * not have: the Viwoods reader declares a camera key, and has none. Only a
+     * real press proves a real button.
      */
-    fun keys(c: Context): List<HwKey> {
-        val seen = c.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
-            .getStringSet(K_SEEN, emptySet()).orEmpty()
-        val detected = Detect.stored(c)?.keys.orEmpty().filter { it.interceptable }
-        // The profile knows where its buttons sit. The other keys follow in a fixed order.
-        val others = (detected + HwKey.interceptable.filter { it.token in seen }).sortedBy { it.ordinal }
-        return (builtIn + others).distinct()
-    }
+    fun keys(c: Context): List<HwKey> = listed(
+        builtIn,
+        c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getStringSet(K_SEEN, emptySet()).orEmpty()
+    )
+
+    /** The profile knows where its buttons sit. The keys that were seen follow in a fixed order. */
+    fun listed(profileKeys: List<HwKey>, seenTokens: Set<String>): List<HwKey> =
+        (profileKeys + HwKey.interceptable.filter { it.token in seenTokens }).distinct()
 
     private const val K_UNKNOWN = "unknown_keys"
 
diff --git a/app/src/main/java/dev/equwal/assistkey/ui/DetectActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/DetectActivity.kt
index b824770..4f883b6 100644
--- a/app/src/main/java/dev/equwal/assistkey/ui/DetectActivity.kt
+++ b/app/src/main/java/dev/equwal/assistkey/ui/DetectActivity.kt
@@ -4,6 +4,8 @@ import android.app.Activity
 import android.os.Build
 import android.widget.LinearLayout
 import dev.equwal.assistkey.device.Detect
+import dev.equwal.assistkey.model.HwKey
+import dev.equwal.assistkey.device.Device
 import dev.equwal.assistkey.shell.Shell
 import dev.equwal.assistkey.ui.Ui.header
 import dev.equwal.assistkey.ui.Ui.note
@@ -66,13 +68,18 @@ class DetectActivity : Activity() {
     }
 
     private fun buttons(col: LinearLayout, found: Detect.Result) {
-        col.header("Buttons found")
+        col.header("Keys the device declares")
         if (found.keys.isEmpty()) {
             col.note("None found.")
             return
         }
         found.keys.forEach { key ->
-            col.row(key.label, "Code " + key.code + (if (key.interceptable) "" else " · kept by the system"))
+            val real = key == HwKey.POWER || key in Device.keys(this)
+            col.row(
+                key.label,
+                "Code " + key.code + (if (key.interceptable) "" else " · kept by the system"),
+                state = if (real) null else "No such button seen"
+            )
         }
     }
 
diff --git a/app/src/main/java/dev/equwal/assistkey/ui/Summary.kt b/app/src/main/java/dev/equwal/assistkey/ui/Summary.kt
index 8a96919..a4c6c95 100644
--- a/app/src/main/java/dev/equwal/assistkey/ui/Summary.kt
+++ b/app/src/main/java/dev/equwal/assistkey/ui/Summary.kt
@@ -24,8 +24,8 @@ object Summary {
     /** The detection row: how many buttons the last detection found. */
     fun detected(keyCount: Int?): String = when {
         keyCount == null -> "Not run yet"
-        keyCount == 0 -> "No named buttons found"
-        else -> count(keyCount, "button") + " found"
+        keyCount == 0 -> "The device declares no named keys"
+        else -> count(keyCount, "key") + " declared by the device"
     }
 
     /** The Power button, which has its own screen. */
diff --git a/app/src/test/java/dev/equwal/assistkey/device/DeviceKeysTest.kt b/app/src/test/java/dev/equwal/assistkey/device/DeviceKeysTest.kt
new file mode 100644
index 0000000..5cd0fe8
--- /dev/null
+++ b/app/src/test/java/dev/equwal/assistkey/device/DeviceKeysTest.kt
@@ -0,0 +1,39 @@
+package dev.equwal.assistkey.device
+
+import dev.equwal.assistkey.model.HwKey
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.util.Random
+
+class DeviceKeysTest {
+
+    private val viwoods = listOf(HwKey.VOL_UP, HwKey.VOL_DOWN, HwKey.AI)
+
+    /**
+     * Regression, 0.0.10: the Viwoods reader declares a camera key and has
+     * none. Detection stored ["ai","vol_up","vol_down","camera","power"], and
+     * the drawing showed a Camera key. A declared key is not a button.
+     */
+    @Test fun `a key that the device only declares is not in the drawing`() {
+        val shown = Device.listed(viwoods, seenTokens = emptySet())
+        assertEquals(viwoods, shown)
+        assertFalse(HwKey.CAMERA in shown)
+    }
+
+    @Test fun `a key that was really pressed is in the drawing, after the profile keys`() {
+        assertEquals(viwoods + HwKey.PAGE_DOWN, Device.listed(viwoods, setOf("page_down")))
+    }
+
+    @Test fun `the drawing has the profile keys first, no key twice, and nothing that was not seen`() {
+        val r = Random(5)
+        repeat(300) {
+            val seen = HwKey.entries.filter { r.nextInt(3) == 0 }.map { it.token }.toSet() + "no_such_key"
+            val shown = Device.listed(viwoods, seen)
+            assertEquals(viwoods, shown.take(viwoods.size))
+            assertEquals(shown.distinct(), shown)
+            assertTrue(shown.all { it in viwoods || (it.token in seen && it.interceptable) })
+        }
+    }
+}
diff --git a/app/src/test/java/dev/equwal/assistkey/ui/SummaryTest.kt b/app/src/test/java/dev/equwal/assistkey/ui/SummaryTest.kt
index bd89474..ad18b13 100644
--- a/app/src/test/java/dev/equwal/assistkey/ui/SummaryTest.kt
+++ b/app/src/test/java/dev/equwal/assistkey/ui/SummaryTest.kt
@@ -24,9 +24,9 @@ class SummaryTest {
     @Test
     fun `detected says whether it has run, then what it found`() {
         assertEquals("Not run yet", Summary.detected(null))
-        assertEquals("No named buttons found", Summary.detected(0))
-        assertEquals("1 button found", Summary.detected(1))
-        assertEquals("4 buttons found", Summary.detected(4))
+        assertEquals("The device declares no named keys", Summary.detected(0))
+        assertEquals("1 key declared by the device", Summary.detected(1))
+        assertEquals("4 keys declared by the device", Summary.detected(4))
     }
 
     @Test
diff --git a/gradle.properties b/gradle.properties
index 1af4b2a..dd01f89 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,8 +6,8 @@ android.nonTransitiveRClass=true
 kotlin.code.style=official
 
 # Release identity. versionCode must rise with every upload to Play.
-assistkey.versionName=0.0.10-alpha
-assistkey.versionCode=10
+assistkey.versionName=0.0.11-alpha
+assistkey.versionCode=11
 
 # The free beta ends on this date (UTC) at the latest. It is the backstop for
 # installs that cannot reach Google Play; the real switch is the