From 0792cfd737d60621fb749ee754de6c4b6484fae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Rottsch=C3=A4fer?= Date: Sat, 7 Nov 2026 14:52:42 +0100 Subject: [PATCH 1/4] Support the Sundial keypad's detection, holds and real layout Detect every gxa535_* module instead of only the old gxa535_keyboard name, so the Sundial is recognised and its layout is used. Add HoldToAct: a tap is delivered on release, while a 400 ms timer fires a hold without releasing the key. Map the Sundial's Tab and Enter holds to Back and Options, and hold Previous/Next for left/right; a tap still sends the media command. Use one KeypadTiming.LONG_PRESS_MS for these holds, T9 long presses and the Back-to-dialer gesture. Draw the onboarding Sundial keys as the physical 3x3 ring, add the hold hints there and in Keypad mapping, and cover the new behaviour with tests and an updated screenshot. --- app/screenshots/onboarding/08-sundial.png | Bin 41365 -> 48024 bytes .../hrln/interactive/sidekit/MainActivity.kt | 66 ++++++++- .../sidekit/onboarding/KeypadDemo.kt | 56 ++++++-- .../sidekit/apps/settings/HoldToActTest.kt | 89 ++++++++++++ .../sidekit/onboarding/OnboardingTest.kt | 22 ++- docs/keypad-mapping.md | 4 + .../sidekit/design/input/HoldToAct.kt | 135 ++++++++++++++++++ .../sidekit/design/input/KeyMapping.kt | 54 ++++--- .../sidekit/design/input/Keypad.kt | 33 +++-- .../sidekit/design/input/KeypadDetector.kt | 23 ++- .../sidekit/design/input/KeypadTiming.kt | 6 + .../sidekit/design/input/NavControls.kt | 25 +++- .../sidekit/design/input/SystemKeys.kt | 5 +- .../design/input/KeypadDetectorTest.kt | 33 +++++ .../sidekit/design/input/NavControlsTest.kt | 34 ++++- 15 files changed, 513 insertions(+), 72 deletions(-) create mode 100644 app/src/test/java/hrln/interactive/sidekit/apps/settings/HoldToActTest.kt create mode 100644 sidekit/src/main/java/hrln/interactive/sidekit/design/input/HoldToAct.kt create mode 100644 sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadTiming.kt create mode 100644 sidekit/src/test/java/hrln/interactive/sidekit/design/input/KeypadDetectorTest.kt diff --git a/app/screenshots/onboarding/08-sundial.png b/app/screenshots/onboarding/08-sundial.png index ff7e7eebd118f0fd6590b32db2ef4fd2387458b3..f9d17eded1080ee6462d995d7af5db9408a48869 100644 GIT binary patch literal 48024 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 literal 41365 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 kfD%@*Tn%GP*?uEHvuyM7 performSundialHold(action, source) }) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Opened from an sms: link or a message notification: go to that conversation. @@ -76,16 +82,70 @@ class MainActivity : ComponentActivity() { hrln.interactive.sidekit.apps.settings.SidekitPreferences(this).applyInputDefault(keypad) } + override fun onPause() { + sundialHold.reset() + super.onPause() + } + // Overriding dispatchKeyEvent is the only way to see a keypad event before Compose or the // system consumes it; lint flags the override because ComponentActivity marks it @RestrictTo. @SuppressLint("RestrictedApi") - override fun dispatchKeyEvent(raw: KeyEvent): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { // Detect the module from the raw key before navigation rewrites it, so a first QWERTY press // on a letter that is also a navigator (T/D/B/J) still reveals the module. - keypad.observe(raw) + keypad.observe(event) // Keypad mapping first, plus each module's default navigation, so Options, Back, focus and // typing all see the mapped key. - val event = keypad.translateForUi(raw) ?: return true + val translated = keypad.translateForUi(event) ?: return true + when (val hold = sundialHold.handle(translated, ::sundialHoldAction)) { + HoldToAct.Result.Pass -> Unit + + HoldToAct.Result.Consume -> return true + + is HoldToAct.Result.Deliver -> { + hold.events.forEach(::dispatchTranslated) + return true + } + } + return dispatchTranslated(translated) + } + + /** Performs a Sundial hold after the timer fires or the key is released late. */ + private fun performSundialHold(action: Int, source: KeyEvent) { + when (action) { + KeyEvent.KEYCODE_BACK -> onBackPressedDispatcher.onBackPressed() + + KeyEvent.KEYCODE_ENDCALL -> { + keypad.finishTyping() + chrome.toggleOptions() + } + + else -> dispatchNav(action, source) + } + } + + /** The Sundial keys that get a hold action, and only while its layout is auto-detected. */ + private fun sundialHoldAction(code: Int): Int? { + if (keypad.module != KeypadModule.Sundial || keypad.mapper.customModule != null) return null + return NavControls.sundialHoldFor[code] + } + + /** Sends a held Sundial key's navigation action as a normal press/release. */ + private fun dispatchNav(code: Int, from: KeyEvent) { + val down = KeyEvent( + from.downTime, from.eventTime, KeyEvent.ACTION_DOWN, code, 0, + from.metaState, from.deviceId, from.scanCode, from.flags, from.source + ) + val up = KeyEvent( + from.downTime, from.eventTime + 1, KeyEvent.ACTION_UP, code, 0, + from.metaState, from.deviceId, from.scanCode, from.flags, from.source + ) + dispatchTranslated(down) + dispatchTranslated(up) + } + + /** Routes an already-translated event; also used for the tap released by [sundialHold]. */ + private fun dispatchTranslated(event: KeyEvent): Boolean { when { // Keypad top-right: the universal Options key. Acts on release; the press is consumed // so the system's end-call handling never sees it. diff --git a/app/src/main/java/hrln/interactive/sidekit/onboarding/KeypadDemo.kt b/app/src/main/java/hrln/interactive/sidekit/onboarding/KeypadDemo.kt index d89ad3a6..3162800b 100644 --- a/app/src/main/java/hrln/interactive/sidekit/onboarding/KeypadDemo.kt +++ b/app/src/main/java/hrln/interactive/sidekit/onboarding/KeypadDemo.kt @@ -17,29 +17,48 @@ internal fun demoRows(module: KeypadModule): List> { val arrows = listOf(key("← ↑ ↓ →", "Move focus between controls. In text, left and right move the cursor or suggestion selection."), key("OK", "Activate the focused control or confirm a suggestion.")) return when (module) { KeypadModule.T9 -> listOf(navigation, arrows) + listOf( - listOf("1", "2 abc", "3 def"), listOf("4 ghi", "5 jkl", "6 mno"), listOf("7 pqrs", "8 tuv", "9 wxyz"), listOf("*", "0", "#"), - ).map { row -> row.map { label -> key(label, when (label) { - "*" -> "Tap * repeatedly to cycle punctuation and symbols." - "#" -> "Tap # to change letter case. Hold # to switch letters / numbers." - "0" -> "Tap 0 for a space in letter mode. Hold for the digit 0." - else -> "Tap repeatedly to cycle this key’s characters. Hold to type its digit." - }) } } + listOf(listOf(key("Delete", "Delete the previous character."))) + listOf("1", "2 abc", "3 def"), + listOf("4 ghi", "5 jkl", "6 mno"), + listOf("7 pqrs", "8 tuv", "9 wxyz"), + listOf("*", "0", "#") + ).map { row -> + row.map { label -> + key( + label, + when (label) { + "*" -> "Tap * repeatedly to cycle punctuation and symbols." + "#" -> "Tap # to change letter case. Hold # to switch letters / numbers." + "0" -> "Tap 0 for a space in letter mode. Hold for the digit 0." + else -> "Tap repeatedly to cycle this key’s characters. Hold to type its digit." + } + ) + } + } + listOf(listOf(key("Delete", "Delete the previous character."))) + KeypadModule.Qwerty -> listOf(navigation) + listOf( - listOf("QW", "ER", "TY", "UI", "OP"), listOf("AS", "DF", "GH", "JK", "L"), listOf("ZX", "CV", "BN", "M", "Delete"), + listOf("QW", "ER", "TY", "UI", "OP"), + listOf("AS", "DF", "GH", "JK", "L"), + listOf("ZX", "CV", "BN", "M", "Delete") ).map { row -> row.map { key(it, if (it == "Delete") "Delete the previous character." else "Tap repeatedly to choose a letter from $it. Hold for the digit or symbol printed above the key.") } } + listOf( listOf(key("↕", "Cycle the suggested words."), key("SYM", "Tap repeatedly to cycle symbols."), key("aA", "Tap to change letter case. Hold to switch letters / numbers.")), - listOf(key("Space", "Insert a space. Hold to type 0."), key("Enter", "Confirm a suggestion or insert a new line.")), + listOf(key("Space", "Insert a space. Hold to type 0."), key("Enter", "Confirm a suggestion or insert a new line.")) + ) + + // The Sundial is a ring of eight keys around the centre key. Lay it out exactly as it sits + // on the module: top-left is left, top-right is right, bottom-left is Tab, bottom-right is + // Enter, and the media keys are the middle row. + KeypadModule.Sundial -> listOf( + listOf(key("←", "Move focus left."), key("↑", "Move focus up."), key("→", "Move focus right.")), + listOf(key("Previous", "Previous media track; hold for left."), key("Play / pause", "Play or pause media where supported."), key("Next", "Next media track; hold for right.")), + listOf(key("Tab", "Move to the next focusable control; hold for Back."), key("↓", "Move focus down."), key("Enter", "Activate the focused control; hold for Options.")) ) - KeypadModule.Sundial -> listOf(navigation, - listOf(key("↑", "Move focus up.")), - listOf(key("Previous", "Previous media track where supported."), key("Play / pause", "Play or pause media where supported."), key("Next", "Next media track where supported.")), - listOf(key("↓", "Move focus down.")), - listOf(key("← / →", "Move focus left / right."), key("Tab", "Move to the next focusable control."), key("Enter", "Activate the focused control."))) + KeypadModule.GamePad -> listOf( listOf(key("D-pad", "Move focus using the directional pad.")), listOf(key("Y", "Game action: its meaning depends on the app or game.")), listOf(key("X", "Game action: its meaning depends on the app or game."), key("A", "Confirm where the app supports controller input."), key("B", "Back where the app supports controller input.")), - listOf(key("Shoulder", "Game action: its meaning depends on the app or game."))) + listOf(key("Shoulder", "Game action: its meaning depends on the app or game.")) + ) } } @@ -59,6 +78,13 @@ internal fun KeypadDemo(module: KeypadModule) { } } } + if (module == KeypadModule.Sundial) { + SidekitText( + "No Back or Options key: hold Tab for Back and Enter for Options. The corner arrows move left/right; hold Previous/Next for the same, and a tap is still media.", + style = SidekitTheme.typography.label.copy(fontSize = 16.sp, lineHeight = 18.sp), + maxLines = Int.MAX_VALUE + ) + } if (!module.typesOnKeypad) SidekitText("Text fields use the on-screen keyboard with this module.", style = caption, maxLines = Int.MAX_VALUE) } } diff --git a/app/src/test/java/hrln/interactive/sidekit/apps/settings/HoldToActTest.kt b/app/src/test/java/hrln/interactive/sidekit/apps/settings/HoldToActTest.kt new file mode 100644 index 00000000..f581dd38 --- /dev/null +++ b/app/src/test/java/hrln/interactive/sidekit/apps/settings/HoldToActTest.kt @@ -0,0 +1,89 @@ +package hrln.interactive.sidekit.apps.settings + +import android.view.KeyEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import hrln.interactive.sidekit.design.input.HoldToAct +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** A tap is delivered on release; a hold fires while the key is still down and never also taps. */ +@RunWith(AndroidJUnit4::class) +class HoldToActTest { + private class FakeScheduler : HoldToAct.Scheduler { + var scheduledDelay = -1L + private var action: (() -> Unit)? = null + override fun schedule(delayMs: Long, action: () -> Unit): HoldToAct.Cancellable { + scheduledDelay = delayMs + this.action = action + return HoldToAct.Cancellable { this.action = null } + } + fun fire() { + action?.invoke() + } + } + + private val holds = mutableListOf>() + private val scheduler = FakeScheduler() + + private fun hold() = HoldToAct( + holdMs = 600, + onHold = { action, source -> holds += action to source }, + scheduler = scheduler + ) + + private fun event(action: Int, code: Int, time: Long, repeat: Int = 0) = KeyEvent(0L, time, action, code, repeat, 0, 1, 0) + + private fun action(code: Int): Int? = when (code) { + KeyEvent.KEYCODE_TAB -> KeyEvent.KEYCODE_BACK + KeyEvent.KEYCODE_ENTER -> KeyEvent.KEYCODE_ENDCALL + else -> null + } + + @Test fun shortTapIsDeliveredOnRelease() { + val hold = hold() + assertEquals(HoldToAct.Result.Consume, hold.handle(event(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB, 0), ::action)) + assertEquals(600L, scheduler.scheduledDelay) + val result = hold.handle(event(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TAB, 100), ::action) + assertTrue(result is HoldToAct.Result.Deliver) + result as HoldToAct.Result.Deliver + assertEquals(listOf(KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_TAB), result.events.map { it.keyCode }) + assertEquals(listOf(KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP), result.events.map { it.action }) + } + + @Test fun holdFiresWhileHeldAndSwallowsTheRelease() { + val hold = hold() + assertEquals(HoldToAct.Result.Consume, hold.handle(event(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB, 0), ::action)) + scheduler.fire() + assertEquals(listOf(KeyEvent.KEYCODE_BACK), holds.map { it.first }) + assertEquals(HoldToAct.Result.Consume, hold.handle(event(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TAB, 500), ::action)) + } + + @Test fun holdFiresOnReleaseWhenTheTimerDidNotRun() { + val hold = hold() + assertEquals(HoldToAct.Result.Consume, hold.handle(event(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0), ::action)) + assertEquals( + HoldToAct.Result.Consume, + hold.handle(event(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 700), ::action) + ) + assertEquals(listOf(KeyEvent.KEYCODE_ENDCALL), holds.map { it.first }) + } + + @Test fun otherKeysPassAndDropAPendingTap() { + val hold = hold() + assertEquals(HoldToAct.Result.Pass, hold.handle(event(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP, 0), ::action)) + assertEquals(HoldToAct.Result.Consume, hold.handle(event(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB, 100), ::action)) + assertEquals(HoldToAct.Result.Pass, hold.handle(event(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP, 150), ::action)) + assertEquals(HoldToAct.Result.Consume, hold.handle(event(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TAB, 200), ::action)) + assertEquals(emptyList(), holds.map { it.first }) + } + + @Test fun resetCancelsAPendingHold() { + val hold = hold() + assertEquals(HoldToAct.Result.Consume, hold.handle(event(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB, 0), ::action)) + hold.reset() + scheduler.fire() + assertEquals(emptyList(), holds.map { it.first }) + } +} diff --git a/app/src/test/java/hrln/interactive/sidekit/onboarding/OnboardingTest.kt b/app/src/test/java/hrln/interactive/sidekit/onboarding/OnboardingTest.kt index 8a097717..cd93e2cb 100644 --- a/app/src/test/java/hrln/interactive/sidekit/onboarding/OnboardingTest.kt +++ b/app/src/test/java/hrln/interactive/sidekit/onboarding/OnboardingTest.kt @@ -43,6 +43,7 @@ class OnboardingTest { compose.mainClock.advanceTimeBy(1000) compose.onRoot().captureRoboImage("screenshots/onboarding/$name.png") } + @Test fun `setup saves choices and only completes on Finish`() { launch() shot("01-home") @@ -71,6 +72,7 @@ class OnboardingTest { assertTrue(prefs.setupComplete) assertEquals(0, prefs.setupStep) } + @Test fun `resumes at saved step and can go back`() { launch(2) compose.onNodeWithText("Your home screen").assertIsDisplayed() @@ -78,6 +80,7 @@ class OnboardingTest { assertEquals(1, prefs.setupStep) compose.onNodeWithText("Make it yours").assertIsDisplayed() } + @Test fun `current keypad is first and other demos do not change it`() { launch(6, KeypadModule.Qwerty) compose.onNodeWithText("1 / 4 · Mini QWERTY").assertIsDisplayed() @@ -100,6 +103,18 @@ class OnboardingTest { compose.onNode(hasScrollToIndexAction()).performScrollToNode(hasText("Mini QWERTY is your current selection.", substring = true)) compose.onNodeWithText("Mini QWERTY is your current selection.", substring = true).assertExists() } + + @Test fun `Sundial demo follows the physical ring`() { + assertEquals( + listOf( + listOf("←", "↑", "→"), + listOf("Previous", "Play / pause", "Next"), + listOf("Tab", "↓", "Enter") + ), + demoRows(KeypadModule.Sundial).map { row -> row.map { it.label } } + ) + } + @Test fun `access status reflects grants and revocations`() { val camera = setupAccess().first { it.title == "Camera" } val app = org.robolectric.Shadows.shadowOf(context as android.app.Application) @@ -110,6 +125,7 @@ class OnboardingTest { app.denyPermissions(android.Manifest.permission.CAMERA) assertFalse(camera.enabled(context)) } + @Test fun `each module appears once with the current one first`() { KeypadModule.entries.forEach { module -> assertEquals(module, orderedModules(module).first()) @@ -117,12 +133,16 @@ class OnboardingTest { assertEquals(4, orderedModules(module).size) } } - @Test @Config(sdk = [31]) fun `Android 12 uses storage access without newer permission prompts`() { + + @Test + @Config(sdk = [31]) + fun `Android 12 uses storage access without newer permission prompts`() { val permissions = setupAccess().flatMap { it.permissions } assertTrue(android.Manifest.permission.READ_EXTERNAL_STORAGE in permissions) assertFalse(android.Manifest.permission.POST_NOTIFICATIONS in permissions) assertFalse(android.Manifest.permission.READ_MEDIA_IMAGES in permissions) } + @Test fun `Android 15 uses granular media access and full screen alarm settings`() { val entries = setupAccess() assertTrue(entries.any { it.title == "Full-screen alarms" }) diff --git a/docs/keypad-mapping.md b/docs/keypad-mapping.md index 8606890a..77bedf73 100644 --- a/docs/keypad-mapping.md +++ b/docs/keypad-mapping.md @@ -22,6 +22,10 @@ While Sidekit waits for a key, it captures every press, including the release, s The key tester shows the key code and scan code for each press, the input device that sent it, and the control it becomes. Use it to set up a new keypad or to report how a device's keys behave. +## Sundial holds + +The Sundial has no Back or Options key. Hold **Tab** for Back and hold **Enter** for Options. Hold the left/right edges (**Previous** / **Next**) for left/right; a tap on them still sends the media command. All keypad holds use the same threshold, `KeypadTiming.LONG_PRESS_MS` (400 ms). + ## Scope - Mappings are saved for each input device, by device name. A key from another device isn't remapped, even if it sends the same code. Examples are an external keyboard and a headset button. Device names, unlike device ids, stay the same across restarts and reconnections. diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/HoldToAct.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/HoldToAct.kt new file mode 100644 index 00000000..8bdfcc17 --- /dev/null +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/HoldToAct.kt @@ -0,0 +1,135 @@ +package hrln.interactive.sidekit.design.input + +import android.os.Handler +import android.os.Looper +import android.view.KeyEvent + +/** + * Gives a key a hold action without changing what a tap does. + * + * The press is held back until it is released or [holdMs] passes. A timer fires the hold while the + * key is still down; a tap is delivered as a normal press/release pair, so a hold never also fires + * the key's tap action. Used for the Sundial, whose ring has no Back or Options key: hold Tab for + * Back, Enter for Options, and Previous/Next for left/right while a tap still sends the media + * command. + */ +class HoldToAct( + private val holdMs: Long = KeypadTiming.LONG_PRESS_MS, + private val onHold: (action: Int, source: KeyEvent) -> Unit = { _, _ -> }, + private val scheduler: Scheduler = MainLooperScheduler() +) { + sealed interface Result { + /** Not one of ours: deliver the event normally. */ + data object Pass : Result + + /** Swallow the event. */ + data object Consume : Result + + /** Deliver these synthetic events through the normal key path. */ + data class Deliver(val events: List) : Result + } + + fun interface Cancellable { + fun cancel() + } + + fun interface Scheduler { + fun schedule(delayMs: Long, action: () -> Unit): Cancellable + } + + private var pending: KeyEvent? = null + private var fired = false + private var timer: Cancellable? = null + + /** Forgets a press that never got its release (the window lost focus). */ + fun reset() = clear() + + /** + * [holdAction] returns the action key code for a held key, or null to leave that key alone. + * Only one key can be pending; pressing another key drops the pending tap. + */ + fun handle(event: KeyEvent, holdAction: (Int) -> Int?): Result { + val action = holdAction(event.keyCode) + if (action == null) { + clear() + return Result.Pass + } + if (event.isCanceled) { + clear() + return Result.Consume + } + when (event.action) { + KeyEvent.ACTION_DOWN -> { + if (event.repeatCount == 0) { + clear() + pending = event + timer = scheduler.schedule(holdMs) { fire(action, event) } + return Result.Consume + } + // The keypad doesn't send repeats, but if one arrives after the threshold and the + // timer hasn't run yet, fire now. + val down = pending + if (!fired && down != null && event.eventTime - down.downTime >= holdMs) { + timer?.cancel() + timer = null + fire(action, down) + } + return Result.Consume + } + + KeyEvent.ACTION_UP -> { + val down = pending + timer?.cancel() + timer = null + if (fired) { + pending = null + fired = false + return Result.Consume + } + if (down != null && down.keyCode == event.keyCode) { + // Releasing after the threshold without a timer still counts as a hold. + if (event.eventTime - down.downTime >= holdMs) { + fire(action, down) + pending = null + fired = false + return Result.Consume + } + pending = null + val up = KeyEvent( + down.downTime, event.eventTime, KeyEvent.ACTION_UP, down.keyCode, + event.repeatCount, event.metaState, event.deviceId, event.scanCode, + event.flags, event.source + ) + return Result.Deliver(listOf(down, up)) + } + pending = null + return Result.Consume + } + + else -> return Result.Pass + } + } + + private fun fire(action: Int, source: KeyEvent) { + if (pending !== source || fired) return + fired = true + timer = null + onHold(action, source) + } + + private fun clear() { + timer?.cancel() + timer = null + pending = null + fired = false + } + + private class MainLooperScheduler : Scheduler { + private val handler = Handler(Looper.getMainLooper()) + override fun schedule(delayMs: Long, action: () -> Unit): Cancellable { + val runnable = Runnable { action() } + handler.postDelayed(runnable, delayMs) + return Cancellable { handler.removeCallbacks(runnable) } + } + } +} diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeyMapping.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeyMapping.kt index cce743d8..b6ac7ad3 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeyMapping.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeyMapping.kt @@ -54,13 +54,14 @@ class KeypadControl(val keyCode: Int, val label: String, val hint: String) object KeypadControls { fun of(module: KeypadModule): List = when (module) { KeypadModule.T9 -> system + dpad + listOf( - control(KeyEvent.KEYCODE_ENTER, "OK", "Select, or take a suggestion"), + control(KeyEvent.KEYCODE_ENTER, "OK", "Select, or take a suggestion") ) + (1..9).map { digit(it) } + listOf( control(KeyEvent.KEYCODE_STAR, "*", "Punctuation and symbols"), digit(0), control(KeyEvent.KEYCODE_POUND, "#", "Case; hold for numbers"), - control(KeyEvent.KEYCODE_DEL, "Delete", "Delete the previous character"), + control(KeyEvent.KEYCODE_DEL, "Delete", "Delete the previous character") ) + KeypadModule.Qwerty -> system + QwertyLayout.letters.map { (code, letters) -> control(code, letters.uppercase(), "Hold for ${QwertyLayout.printed[code]}") } + listOf( @@ -69,25 +70,27 @@ object KeypadControls { control(KeyEvent.KEYCODE_ENTER, "Enter", "New line, or take a suggestion"), control(QwertyLayout.SUGGEST, "↕", "Next suggestion"), control(QwertyLayout.SYM, "SYM", "Symbols"), - control(QwertyLayout.CASE, "aA", "Case; hold for numbers"), + control(QwertyLayout.CASE, "aA", "Case; hold for numbers") ) + KeypadModule.Sundial -> system + listOf( control(KeyEvent.KEYCODE_DPAD_UP, "Top", "Move up"), - control(KeyEvent.KEYCODE_MEDIA_NEXT, "Right edge", "Next track"), + control(KeyEvent.KEYCODE_MEDIA_NEXT, "Right edge", "Next track; hold for right"), control(KeyEvent.KEYCODE_DPAD_DOWN, "Bottom", "Move down"), - control(KeyEvent.KEYCODE_MEDIA_PREVIOUS, "Left edge", "Previous track"), + control(KeyEvent.KEYCODE_MEDIA_PREVIOUS, "Left edge", "Previous track; hold for left"), control(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, "Centre", "Play or pause"), control(KeyEvent.KEYCODE_DPAD_LEFT, "Corner ←", "Move left"), control(KeyEvent.KEYCODE_DPAD_RIGHT, "Corner →", "Move right"), - control(KeyEvent.KEYCODE_TAB, "Tab", "Next control"), - control(KeyEvent.KEYCODE_ENTER, "Enter", "Select"), + control(KeyEvent.KEYCODE_TAB, "Tab", "Next control; hold for Back"), + control(KeyEvent.KEYCODE_ENTER, "Enter", "Select; hold for Options") ) + KeypadModule.GamePad -> system + dpad + listOf( control(KeyEvent.KEYCODE_BUTTON_A, "A", "Confirm"), control(KeyEvent.KEYCODE_BUTTON_B, "B", "Back in games"), control(KeyEvent.KEYCODE_BUTTON_X, "X", "Game action"), control(KeyEvent.KEYCODE_BUTTON_Y, "Y", "Game action"), - control(KeyEvent.KEYCODE_BUTTON_L1, "Shoulder", "Game action"), + control(KeyEvent.KEYCODE_BUTTON_L1, "Shoulder", "Game action") ) } @@ -96,20 +99,30 @@ object KeypadControls { private fun control(code: Int, label: String, hint: String) = KeypadControl(code, label, hint) private fun digit(n: Int): KeypadControl { val letters = listOf("", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz")[n] - return control(KeyEvent.KEYCODE_0 + n, if (letters.isEmpty()) "$n" else "$n $letters", if (n == 0) "Space; hold for 0" else if (n == 1) "Punctuation; hold for 1" else "Letters; hold for $n") + return control( + KeyEvent.KEYCODE_0 + n, + if (letters.isEmpty()) "$n" else "$n $letters", + if (n == 0) { + "Space; hold for 0" + } else if (n == 1) { + "Punctuation; hold for 1" + } else { + "Letters; hold for $n" + } + ) } private val system = listOf( // Back is the plain Back key here: the Sidephone's top-left key (CALL) only becomes Back // through the keypad accessibility service, which keeps working whatever is mapped. control(KeyEvent.KEYCODE_BACK, "Back", "Go back a screen"), - control(KeyEvent.KEYCODE_ENDCALL, "Options", "Show the screen's extra actions"), + control(KeyEvent.KEYCODE_ENDCALL, "Options", "Show the screen's extra actions") ) private val dpad = listOf( control(KeyEvent.KEYCODE_DPAD_UP, "Up", "Move up"), control(KeyEvent.KEYCODE_DPAD_DOWN, "Down", "Move down"), control(KeyEvent.KEYCODE_DPAD_LEFT, "Left", "Move left; cursor"), - control(KeyEvent.KEYCODE_DPAD_RIGHT, "Right", "Move right; cursor"), + control(KeyEvent.KEYCODE_DPAD_RIGHT, "Right", "Move right; cursor") ) } @@ -129,7 +142,7 @@ object KeypadControls { @Stable class KeyMapper internal constructor( private val prefs: SharedPreferences?, - private val deviceName: (KeyEvent) -> String = ::defaultDeviceName, + private val deviceName: (KeyEvent) -> String = ::defaultDeviceName ) { /** The module the custom mapping is for, or null for automatic detection. */ var customModule: KeypadModule? by mutableStateOf(null) @@ -174,8 +187,7 @@ class KeyMapper internal constructor( } /** The control [key] is bound to in [module], if any. */ - fun controlFor(module: KeypadModule, key: PhysicalKey): Int? = - bindings(module).entries.firstOrNull { it.value == key }?.key + fun controlFor(module: KeypadModule, key: PhysicalKey): Int? = bindings(module).entries.firstOrNull { it.value == key }?.key fun reset(module: KeypadModule, controlCode: Int) = save(module, bindings(module) - controlCode) @@ -229,6 +241,7 @@ class KeyMapper internal constructor( val taken = capture?.invoke(key, event) == true (if (taken) SWALLOWED else mappedCode(key)).also { held[key] = it } } + // A release finishes the press the way it started, even if the mapping changed meanwhile. else -> held.remove(key) ?: if (capture != null) SWALLOWED else mappedCode(key) } @@ -273,8 +286,7 @@ class KeyMapper internal constructor( return control to PhysicalKey(parts[3], code, scan) } - private fun defaultDeviceName(event: KeyEvent): String = - event.device?.name ?: if (event.deviceId == KeyCharacterMap.VIRTUAL_KEYBOARD) "Virtual" else "" + private fun defaultDeviceName(event: KeyEvent): String = event.device?.name ?: if (event.deviceId == KeyCharacterMap.VIRTUAL_KEYBOARD) "Virtual" else "" /** A mapper that saves to [prefs] (the keypad's own preferences). */ fun create(prefs: SharedPreferences?): KeyMapper = KeyMapper(prefs) @@ -285,10 +297,14 @@ class KeyMapper internal constructor( } private class Translation( - val event: KeyEvent, val action: Int, val keyCode: Int, val downTime: Long, val eventTime: Long, val repeat: Int, - val out: KeyEvent?, + val event: KeyEvent, + val action: Int, + val keyCode: Int, + val downTime: Long, + val eventTime: Long, + val repeat: Int, + val out: KeyEvent? ) { fun matches(e: KeyEvent) = e === event && e.action == action && e.keyCode == keyCode && e.downTime == downTime && e.eventTime == eventTime && e.repeatCount == repeat } - diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/Keypad.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/Keypad.kt index b215d1c1..32b64b24 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/Keypad.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/Keypad.kt @@ -3,32 +3,34 @@ package hrln.interactive.sidekit.design.input import android.content.Context import android.content.SharedPreferences import android.view.KeyEvent -import android.view.ViewConfiguration import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.focus.FocusRequester +import java.io.File +import java.io.IOException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.File -import java.io.IOException /** The Sidephone's swappable keypad modules. */ enum class KeypadModule(val label: String, val typesOnKeypad: Boolean) { /** Phone keypad with D-pad, OK and call keys. */ T9("T9", true), + /** Two letters per key, 20 keys. */ Qwerty("Mini QWERTY", true), - /** Click wheel with a centre button. */ + + /** Ring of eight keys with a centre button. */ Sundial("Sundial", false), + /** D-pad, A / B / X / Y and a shoulder key. */ - GamePad("Game controller", false), + GamePad("Game controller", false) } /** @@ -53,7 +55,7 @@ class Keypad internal constructor( initialModule: KeypadModule, val longPressMs: Long = DEFAULT_LONG_PRESS_MS, /** Custom key mapping (off unless the user turns it on in Settings). */ - val mapper: KeyMapper = KeyMapper.forTesting(), + val mapper: KeyMapper = KeyMapper.forTesting() ) { private var detected: KeypadModule by mutableStateOf(initialModule) @@ -168,8 +170,12 @@ class Keypad internal constructor( session?.finish() } - internal fun registerFallback(focus: FocusRequester, activate: () -> Unit = {}) { fallbacks.add(TypingTarget(focus, activate)) } - internal fun unregisterFallback(focus: FocusRequester) { fallbacks.removeAll { it.focus === focus } } + internal fun registerFallback(focus: FocusRequester, activate: () -> Unit = {}) { + fallbacks.add(TypingTarget(focus, activate)) + } + internal fun unregisterFallback(focus: FocusRequester) { + fallbacks.removeAll { it.focus === focus } + } private fun loadResources() { if (wordsJob != null || loadWords == null) return @@ -219,7 +225,7 @@ class Keypad internal constructor( private const val PREF_T9 = "t9" private const val PREF_MODULE = "module" private const val SAVE_DELAY_MS = 2_000L - private const val DEFAULT_LONG_PRESS_MS = 450L + private const val DEFAULT_LONG_PRESS_MS = KeypadTiming.LONG_PRESS_MS fun isTypingKey(code: Int) = code in KeyEvent.KEYCODE_0..KeyEvent.KEYCODE_9 || code == KeyEvent.KEYCODE_STAR || code == KeyEvent.KEYCODE_POUND || code == KeyEvent.KEYCODE_DEL || @@ -238,8 +244,8 @@ class Keypad internal constructor( ?.let { name -> KeypadModule.entries.firstOrNull { it.name == name } } // Before modules were told apart, T9 was the only one that typed. ?: if (prefs.getBoolean(PREF_T9, true)) KeypadModule.T9 else KeypadModule.Sundial, - longPressMs = ViewConfiguration.getLongPressTimeout().toLong(), - mapper = KeyMapper.create(prefs), + longPressMs = KeypadTiming.LONG_PRESS_MS, + mapper = KeyMapper.create(prefs) ) } @@ -248,12 +254,11 @@ class Keypad internal constructor( scope: CoroutineScope, words: T9Words, module: KeypadModule = KeypadModule.T9, - mapper: KeyMapper = KeyMapper.forTesting(), + mapper: KeyMapper = KeyMapper.forTesting() ) = Keypad(null, null, scope, null, module, mapper = mapper).also { it.words = words } } } -internal fun KeyEvent.withCode(code: Int) = - KeyEvent(downTime, eventTime, action, code, repeatCount, metaState, deviceId, scanCode, flags, source) +internal fun KeyEvent.withCode(code: Int) = KeyEvent(downTime, eventTime, action, code, repeatCount, metaState, deviceId, scanCode, flags, source) val LocalKeypad = staticCompositionLocalOf { null } diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadDetector.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadDetector.kt index 04162253..58708b72 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadDetector.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadDetector.kt @@ -1,39 +1,48 @@ package hrln.interactive.sidekit.design.input +import android.view.KeyCharacterMap import android.view.KeyEvent /** * Tells the attached keypad module from the keys it sends. * - * Every module is the same I2C input device (`gxa535_keyboard`, vendor / product 0, no readable - * id in sysfs), so the module is recognised by keys only it has. Measured with `getevent`: + * Every module appears as an input device named `gxa535_*`. This phone reports `gxa535_media`, + * `gxa535_qwerty`, `gxa535_t9`, `gxa535_gamepad` and `gxa535_phone`; older firmware exposed + * everything as `gxa535_keyboard`. The module is recognised by keys only it has. Measured with + * `getevent`: * * - T9: digits, `*`, `#` (plus D-pad, OK = `KEY_ENTER`, left soft key = Home, right = Delete). * - Mini QWERTY: letters, one per key (`KEY_Q` for QW...), Space, Ctrl / Alt / Shift. - * - Sundial: centre = Play/Pause, edges = Up / Next / Down / Previous, corners = Left / Right / - * Tab / Enter. Turning the wheel presses the edges in sequence. + * - Sundial: centre = Play/Pause, ring = Up / Next / Down / Previous, corners = Left / Right / + * Tab / Enter. The ring reports eight separate keys; it has no rotation axis. * - Game controller: gamepad buttons. * * Keys shared between modules (D-pad, Enter, call keys, Delete) say nothing. */ internal object KeypadDetector { - const val DEVICE_NAME = "gxa535_keyboard" + const val DEVICE_PREFIX = "gxa535_" + + /** True for a keypad module, the virtual keyboard, or an event with no readable device. */ + internal fun isKeypadDevice(name: String?, deviceId: Int): Boolean = name == null || name.startsWith(DEVICE_PREFIX) || deviceId == KeyCharacterMap.VIRTUAL_KEYBOARD fun moduleFor(event: KeyEvent): KeypadModule? { // Headset buttons send Play/Pause too: only trust the keypad (or injected / remapped keys, // which come from the virtual device). - val name = event.device?.name - if (name != null && name != DEVICE_NAME && event.deviceId != android.view.KeyCharacterMap.VIRTUAL_KEYBOARD) return null + if (!isKeypadDevice(event.device?.name, event.deviceId)) return null val code = event.keyCode return when { code in KeyEvent.KEYCODE_A..KeyEvent.KEYCODE_Z || code == KeyEvent.KEYCODE_SPACE || code == KeyEvent.KEYCODE_SHIFT_LEFT || code == KeyEvent.KEYCODE_ALT_LEFT || code == KeyEvent.KEYCODE_CTRL_LEFT -> KeypadModule.Qwerty + code == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || code == KeyEvent.KEYCODE_MEDIA_NEXT || code == KeyEvent.KEYCODE_MEDIA_PREVIOUS || code == KeyEvent.KEYCODE_TAB -> KeypadModule.Sundial + KeyEvent.isGamepadButton(code) -> KeypadModule.GamePad + code in KeyEvent.KEYCODE_0..KeyEvent.KEYCODE_9 || code == KeyEvent.KEYCODE_STAR || code == KeyEvent.KEYCODE_POUND -> KeypadModule.T9 + else -> null } } diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadTiming.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadTiming.kt new file mode 100644 index 00000000..95257005 --- /dev/null +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadTiming.kt @@ -0,0 +1,6 @@ +package hrln.interactive.sidekit.design.input + +/** One long-press threshold for every keypad gesture: Back, T9, and the Sundial holds. */ +object KeypadTiming { + const val LONG_PRESS_MS = 400L +} diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/NavControls.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/NavControls.kt index dfde98f0..fb45994e 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/NavControls.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/NavControls.kt @@ -1,3 +1,7 @@ +// The key constants read as navigation keys in code; ktlint's property-naming rule would force +// SCREAMING_SNAKE_CASE for these compile-time constants. +@file:Suppress("ktlint:standard:property-naming") + package hrln.interactive.sidekit.design.input import android.view.KeyEvent @@ -21,6 +25,7 @@ object NavControls { const val Down = KeyEvent.KEYCODE_DPAD_DOWN const val Left = KeyEvent.KEYCODE_DPAD_LEFT const val Right = KeyEvent.KEYCODE_DPAD_RIGHT + /** Select / open the focused control. [KeyEvent.KEYCODE_ENTER] is what T9 calls OK and ListItems act on. */ const val Confirm = KeyEvent.KEYCODE_ENTER const val Back = KeyEvent.KEYCODE_BACK @@ -49,17 +54,29 @@ object NavControls { KeypadModule.Sundial to emptyMap(), KeypadModule.GamePad to mapOf( KeyEvent.KEYCODE_BUTTON_A to Confirm, - KeyEvent.KEYCODE_BUTTON_B to Back, + KeyEvent.KEYCODE_BUTTON_B to Back ), KeypadModule.Qwerty to mapOf( // Measured on-device: T / D / B / J around the tablet's centre form a directional cross. KeyEvent.KEYCODE_T to Up, KeyEvent.KEYCODE_D to Left, KeyEvent.KEYCODE_B to Down, - KeyEvent.KEYCODE_J to Right, - ), + KeyEvent.KEYCODE_J to Right + ) + ) + + /** + * Hold actions for the Sundial's ring. A tap keeps the printed role (media, Tab, Enter); a + * hold turns it into the navigation action the module has no dedicated key for. Tab/Enter have + * no media role to preserve; Previous/Next keep theirs on a tap. + */ + val sundialHoldFor: Map = mapOf( + KeyEvent.KEYCODE_TAB to Back, + KeyEvent.KEYCODE_ENTER to Options, + KeyEvent.KEYCODE_MEDIA_PREVIOUS to Left, + KeyEvent.KEYCODE_MEDIA_NEXT to Right ) /** The canonical navigation key [code] becomes in [module]'s UI, or null to keep it as sent. */ internal fun applyDefault(module: KeypadModule, code: Int): Int? = defaultFor[module]?.get(code) -} \ No newline at end of file +} diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/SystemKeys.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/SystemKeys.kt index 293ec414..5d0c3f65 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/SystemKeys.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/SystemKeys.kt @@ -16,9 +16,8 @@ import android.view.KeyEvent object SystemKeys { fun isBack(event: KeyEvent): Boolean = event.keyCode == KeyEvent.KEYCODE_CALL - fun isOptions(event: KeyEvent): Boolean = - event.keyCode == KeyEvent.KEYCODE_ENDCALL || event.keyCode == KeyEvent.KEYCODE_MENU + fun isOptions(event: KeyEvent): Boolean = event.keyCode == KeyEvent.KEYCODE_ENDCALL || event.keyCode == KeyEvent.KEYCODE_MENU /** Hold the Back key this long to open the dialer instead. */ - const val LONG_PRESS_MS = 600L + const val LONG_PRESS_MS = KeypadTiming.LONG_PRESS_MS } diff --git a/sidekit/src/test/java/hrln/interactive/sidekit/design/input/KeypadDetectorTest.kt b/sidekit/src/test/java/hrln/interactive/sidekit/design/input/KeypadDetectorTest.kt new file mode 100644 index 00000000..df13e0e2 --- /dev/null +++ b/sidekit/src/test/java/hrln/interactive/sidekit/design/input/KeypadDetectorTest.kt @@ -0,0 +1,33 @@ +package hrln.interactive.sidekit.design.input + +import android.view.KeyCharacterMap +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** The keypad is told from other input devices by its `gxa535_*` name. */ +class KeypadDetectorTest { + + @Test fun acceptsEveryModuleName() { + for (name in listOf( + "gxa535_media", + "gxa535_qwerty", + "gxa535_t9", + "gxa535_gamepad", + "gxa535_phone", + "gxa535_keyboard" + )) { + assertTrue(name, KeypadDetector.isKeypadDevice(name, deviceId = 3)) + } + } + + @Test fun rejectsOtherDevices() { + assertFalse(KeypadDetector.isKeypadDevice("mt63xx-accdet Headset", deviceId = 4)) + assertFalse(KeypadDetector.isKeypadDevice("External keyboard", deviceId = 7)) + } + + @Test fun allowsVirtualAndUnnamedEvents() { + assertTrue(KeypadDetector.isKeypadDevice("Injected", KeyCharacterMap.VIRTUAL_KEYBOARD)) + assertTrue(KeypadDetector.isKeypadDevice(null, deviceId = 3)) + } +} diff --git a/sidekit/src/test/java/hrln/interactive/sidekit/design/input/NavControlsTest.kt b/sidekit/src/test/java/hrln/interactive/sidekit/design/input/NavControlsTest.kt index 90689aff..a9b3df53 100644 --- a/sidekit/src/test/java/hrln/interactive/sidekit/design/input/NavControlsTest.kt +++ b/sidekit/src/test/java/hrln/interactive/sidekit/design/input/NavControlsTest.kt @@ -15,9 +15,15 @@ class NavControlsTest { @Test fun t9AndSundialAlreadySendCanonicalKeys() { // T9's D-pad / OK are already what focus and select act on: nothing to rewrite. - for (code in listOf(KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, - KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_ENDCALL)) { + for (code in listOf( + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_BACK, + KeyEvent.KEYCODE_ENDCALL + )) { assertNull(mapped(KeypadModule.T9, code)) assertNull(mapped(KeypadModule.Sundial, code)) } @@ -51,12 +57,28 @@ class NavControlsTest { @Test fun qwertyModifierKeysAndConfirmStayTyping() { // The modifier keys and Enter keep their jobs: only the letter cross navigates. - for (code in listOf(QwertyLayout.SUGGEST, QwertyLayout.SYM, QwertyLayout.CASE, - KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_A)) { + for (code in listOf( + QwertyLayout.SUGGEST, + QwertyLayout.SYM, + QwertyLayout.CASE, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_BACK, + KeyEvent.KEYCODE_A + )) { assertNull(mapped(KeypadModule.Qwerty, code)) } } + @Test fun sundialHoldTargetsAreCanonicalNavKeys() { + NavControls.sundialHoldFor.forEach { (_, target) -> + assertTrue("unexpected Sundial hold target $target", target in NavControls.all) + } + assertEquals(NavControls.Back, NavControls.sundialHoldFor[KeyEvent.KEYCODE_TAB]) + assertEquals(NavControls.Options, NavControls.sundialHoldFor[KeyEvent.KEYCODE_ENTER]) + assertEquals(NavControls.Left, NavControls.sundialHoldFor[KeyEvent.KEYCODE_MEDIA_PREVIOUS]) + assertEquals(NavControls.Right, NavControls.sundialHoldFor[KeyEvent.KEYCODE_MEDIA_NEXT]) + } + @Test fun everyDefaultTargetIsACanonicalNavKey() { NavControls.defaultFor.forEach { (module, profile) -> profile.forEach { (_, target) -> @@ -70,4 +92,4 @@ class NavControlsTest { assertTrue("unexpected canonical target $it", isDirection(it) || it == NavControls.Confirm || it == NavControls.Back) } } -} \ No newline at end of file +} From 3ad32a46ef14e0c46d27879e5d4354286df3b9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Rottsch=C3=A4fer?= Date: Mon, 9 Nov 2026 12:37:06 +0100 Subject: [PATCH 2/4] fix(input): follow the system long-press timeout and trust only module devices KeypadTiming.LONG_PRESS_MS was hardcoded to 400 ms, replacing the ViewConfiguration.getLongPressTimeout() call the app used before. Read the system value again so a device or accessibility setting is honoured. Keep SystemKeys.LONG_PRESS_MS at 600 ms: opening the dialer is a deliberate gesture and should stay longer than an ordinary keypad hold. Replace the gxa535_* prefix with an allowlist of the four module device names (plus the older gxa535_keyboard). gxa535_phone shares the prefix but carries the phone's own keys, and a future device should be added deliberately rather than trusted by name alone. Tests: :sidekit:testDebugUnitTest --tests '*KeypadDetectorTest' --tests '*NavControlsTest' --- docs/keypad-mapping.md | 4 +++- .../sidekit/design/input/Keypad.kt | 3 +-- .../sidekit/design/input/KeypadDetector.kt | 24 ++++++++++++++----- .../sidekit/design/input/KeypadTiming.kt | 12 ++++++++-- .../sidekit/design/input/SystemKeys.kt | 8 +++++-- .../design/input/KeypadDetectorTest.kt | 13 ++++++++-- 6 files changed, 49 insertions(+), 15 deletions(-) diff --git a/docs/keypad-mapping.md b/docs/keypad-mapping.md index 77bedf73..90eb8147 100644 --- a/docs/keypad-mapping.md +++ b/docs/keypad-mapping.md @@ -22,9 +22,11 @@ While Sidekit waits for a key, it captures every press, including the release, s The key tester shows the key code and scan code for each press, the input device that sent it, and the control it becomes. Use it to set up a new keypad or to report how a device's keys behave. +Sidekit only treats the keypad modules' own input devices as keypads. A headset button or an external keyboard is ignored for module detection, even when it sends the same key code. + ## Sundial holds -The Sundial has no Back or Options key. Hold **Tab** for Back and hold **Enter** for Options. Hold the left/right edges (**Previous** / **Next**) for left/right; a tap on them still sends the media command. All keypad holds use the same threshold, `KeypadTiming.LONG_PRESS_MS` (400 ms). +The Sundial has no Back or Options key. Hold **Tab** for Back and hold **Enter** for Options. Hold the left/right edges (**Previous** / **Next**) for left/right; a tap on them still sends the media command. Keypad holds follow Android's long-press timeout (`ViewConfiguration.getLongPressTimeout()`, 400 ms by default), so a device or accessibility tool that changes the system value also changes them. The Back-to-dialer hold stays at 600 ms. ## Scope diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/Keypad.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/Keypad.kt index 32b64b24..8db04fbb 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/Keypad.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/Keypad.kt @@ -53,7 +53,7 @@ class Keypad internal constructor( private val scope: CoroutineScope, private val loadWords: (suspend () -> T9Words)?, initialModule: KeypadModule, - val longPressMs: Long = DEFAULT_LONG_PRESS_MS, + val longPressMs: Long = KeypadTiming.LONG_PRESS_MS, /** Custom key mapping (off unless the user turns it on in Settings). */ val mapper: KeyMapper = KeyMapper.forTesting() ) { @@ -225,7 +225,6 @@ class Keypad internal constructor( private const val PREF_T9 = "t9" private const val PREF_MODULE = "module" private const val SAVE_DELAY_MS = 2_000L - private const val DEFAULT_LONG_PRESS_MS = KeypadTiming.LONG_PRESS_MS fun isTypingKey(code: Int) = code in KeyEvent.KEYCODE_0..KeyEvent.KEYCODE_9 || code == KeyEvent.KEYCODE_STAR || code == KeyEvent.KEYCODE_POUND || code == KeyEvent.KEYCODE_DEL || diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadDetector.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadDetector.kt index 58708b72..2ae26ea5 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadDetector.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadDetector.kt @@ -6,10 +6,10 @@ import android.view.KeyEvent /** * Tells the attached keypad module from the keys it sends. * - * Every module appears as an input device named `gxa535_*`. This phone reports `gxa535_media`, - * `gxa535_qwerty`, `gxa535_t9`, `gxa535_gamepad` and `gxa535_phone`; older firmware exposed - * everything as `gxa535_keyboard`. The module is recognised by keys only it has. Measured with - * `getevent`: + * Each swappable module appears as its own input device: `gxa535_media`, `gxa535_qwerty`, + * `gxa535_t9` and `gxa535_gamepad`. Older firmware exposed everything as `gxa535_keyboard`. + * `gxa535_phone` is the phone's own key device, not a module, so it is not trusted here. The + * module is recognised by keys only it has. Measured with `getevent`: * * - T9: digits, `*`, `#` (plus D-pad, OK = `KEY_ENTER`, left soft key = Home, right = Delete). * - Mini QWERTY: letters, one per key (`KEY_Q` for QW...), Space, Ctrl / Alt / Shift. @@ -20,10 +20,22 @@ import android.view.KeyEvent * Keys shared between modules (D-pad, Enter, call keys, Delete) say nothing. */ internal object KeypadDetector { - const val DEVICE_PREFIX = "gxa535_" + /** + * The input devices that carry a swappable module. An allowlist, not a `gxa535_*` prefix: + * `gxa535_phone` shares the prefix but sends the phone's own keys, and a future device should + * be added deliberately rather than trusted by name alone. + */ + private val MODULE_DEVICES = setOf( + "gxa535_media", + "gxa535_qwerty", + "gxa535_t9", + "gxa535_gamepad", + // Older firmware exposed every module under this one name. + "gxa535_keyboard" + ) /** True for a keypad module, the virtual keyboard, or an event with no readable device. */ - internal fun isKeypadDevice(name: String?, deviceId: Int): Boolean = name == null || name.startsWith(DEVICE_PREFIX) || deviceId == KeyCharacterMap.VIRTUAL_KEYBOARD + internal fun isKeypadDevice(name: String?, deviceId: Int): Boolean = name == null || name in MODULE_DEVICES || deviceId == KeyCharacterMap.VIRTUAL_KEYBOARD fun moduleFor(event: KeyEvent): KeypadModule? { // Headset buttons send Play/Pause too: only trust the keypad (or injected / remapped keys, diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadTiming.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadTiming.kt index 95257005..e9800a13 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadTiming.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/KeypadTiming.kt @@ -1,6 +1,14 @@ package hrln.interactive.sidekit.design.input -/** One long-press threshold for every keypad gesture: Back, T9, and the Sundial holds. */ +import android.view.ViewConfiguration + +/** + * The system long-press threshold, shared by T9 and the Sundial holds. + * + * [ViewConfiguration.getLongPressTimeout] follows `Settings.Secure.LONG_PRESS_TIMEOUT` (400 ms by + * default), so a device or accessibility tool that changes the system value also changes these + * gestures. The Back-to-dialer hold keeps its own, longer threshold in [SystemKeys]. + */ object KeypadTiming { - const val LONG_PRESS_MS = 400L + val LONG_PRESS_MS: Long get() = ViewConfiguration.getLongPressTimeout().toLong() } diff --git a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/SystemKeys.kt b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/SystemKeys.kt index 5d0c3f65..149be4b7 100644 --- a/sidekit/src/main/java/hrln/interactive/sidekit/design/input/SystemKeys.kt +++ b/sidekit/src/main/java/hrln/interactive/sidekit/design/input/SystemKeys.kt @@ -18,6 +18,10 @@ object SystemKeys { fun isOptions(event: KeyEvent): Boolean = event.keyCode == KeyEvent.KEYCODE_ENDCALL || event.keyCode == KeyEvent.KEYCODE_MENU - /** Hold the Back key this long to open the dialer instead. */ - const val LONG_PRESS_MS = KeypadTiming.LONG_PRESS_MS + /** + * Hold the Back key this long to open the dialer instead. Kept separate from + * [KeypadTiming.LONG_PRESS_MS]: opening the dialer is a deliberate, destructive-feeling + * gesture, so it stays longer than an ordinary keypad hold. + */ + const val LONG_PRESS_MS = 600L } diff --git a/sidekit/src/test/java/hrln/interactive/sidekit/design/input/KeypadDetectorTest.kt b/sidekit/src/test/java/hrln/interactive/sidekit/design/input/KeypadDetectorTest.kt index df13e0e2..18bd8beb 100644 --- a/sidekit/src/test/java/hrln/interactive/sidekit/design/input/KeypadDetectorTest.kt +++ b/sidekit/src/test/java/hrln/interactive/sidekit/design/input/KeypadDetectorTest.kt @@ -5,7 +5,7 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -/** The keypad is told from other input devices by its `gxa535_*` name. */ +/** The keypad is told from other input devices by the module device's name. */ class KeypadDetectorTest { @Test fun acceptsEveryModuleName() { @@ -14,13 +14,22 @@ class KeypadDetectorTest { "gxa535_qwerty", "gxa535_t9", "gxa535_gamepad", - "gxa535_phone", "gxa535_keyboard" )) { assertTrue(name, KeypadDetector.isKeypadDevice(name, deviceId = 3)) } } + @Test fun rejectsThePhonesOwnKeys() { + // Same gxa535_ prefix, but this device carries the phone's keys, not a swappable module. + assertFalse(KeypadDetector.isKeypadDevice("gxa535_phone", deviceId = 3)) + } + + @Test fun rejectsUnknownModuleNames() { + // A new module must be added to the allowlist deliberately. + assertFalse(KeypadDetector.isKeypadDevice("gxa535_future", deviceId = 3)) + } + @Test fun rejectsOtherDevices() { assertFalse(KeypadDetector.isKeypadDevice("mt63xx-accdet Headset", deviceId = 4)) assertFalse(KeypadDetector.isKeypadDevice("External keyboard", deviceId = 7)) From f1e0a91bc14c7f569d0d603a85849c10cfd05c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Rottsch=C3=A4fer?= Date: Mon, 9 Nov 2026 12:37:06 +0100 Subject: [PATCH 3/4] test(input): move HoldToActTest beside the code it tests The test lives in the app module because that is where Robolectric is available, but its package now matches design.input instead of apps.settings. Tests: :app:testDebugUnitTest --tests '*HoldToActTest' --- .../sidekit/{apps/settings => design/input}/HoldToActTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename app/src/test/java/hrln/interactive/sidekit/{apps/settings => design/input}/HoldToActTest.kt (97%) diff --git a/app/src/test/java/hrln/interactive/sidekit/apps/settings/HoldToActTest.kt b/app/src/test/java/hrln/interactive/sidekit/design/input/HoldToActTest.kt similarity index 97% rename from app/src/test/java/hrln/interactive/sidekit/apps/settings/HoldToActTest.kt rename to app/src/test/java/hrln/interactive/sidekit/design/input/HoldToActTest.kt index f581dd38..80abc831 100644 --- a/app/src/test/java/hrln/interactive/sidekit/apps/settings/HoldToActTest.kt +++ b/app/src/test/java/hrln/interactive/sidekit/design/input/HoldToActTest.kt @@ -1,8 +1,7 @@ -package hrln.interactive.sidekit.apps.settings +package hrln.interactive.sidekit.design.input import android.view.KeyEvent import androidx.test.ext.junit.runners.AndroidJUnit4 -import hrln.interactive.sidekit.design.input.HoldToAct import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test From 298fa222841c81733917ccb2c8d81611d85eb5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Rottsch=C3=A4fer?= Date: Mon, 9 Nov 2026 13:31:07 +0100 Subject: [PATCH 4/4] fix(input): satisfy lint for the Sundial Back hold performSundialHold called onBackPressedDispatcher.onBackPressed() directly, which Android Lint reports as GestureBackNavigation. Suppress it there: the call comes from a hardware-key hold, not a system back gesture, and the dispatcher still routes through registered callbacks. dispatchTranslated now owns the super.dispatchKeyEvent() call, so it needs the same RestrictedApi suppression the override had. --- .../main/java/hrln/interactive/sidekit/MainActivity.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/hrln/interactive/sidekit/MainActivity.kt b/app/src/main/java/hrln/interactive/sidekit/MainActivity.kt index 13a8ac6f..28b3d659 100644 --- a/app/src/main/java/hrln/interactive/sidekit/MainActivity.kt +++ b/app/src/main/java/hrln/interactive/sidekit/MainActivity.kt @@ -110,7 +110,14 @@ class MainActivity : ComponentActivity() { return dispatchTranslated(translated) } - /** Performs a Sundial hold after the timer fires or the key is released late. */ + /** + * Performs a Sundial hold after the timer fires or the key is released late. + * + * Back comes from holding a hardware key, not from a system back gesture, so the + * predictive-back lint does not apply; the dispatcher still routes through registered + * callbacks. + */ + @SuppressLint("GestureBackNavigation") private fun performSundialHold(action: Int, source: KeyEvent) { when (action) { KeyEvent.KEYCODE_BACK -> onBackPressedDispatcher.onBackPressed() @@ -145,6 +152,7 @@ class MainActivity : ComponentActivity() { } /** Routes an already-translated event; also used for the tap released by [sundialHold]. */ + @SuppressLint("RestrictedApi") private fun dispatchTranslated(event: KeyEvent): Boolean { when { // Keypad top-right: the universal Options key. Acts on release; the press is consumed