Skip to content

feat(keypad): support the Sundial keypad's physical layout and holds - #44

Draft
manuel-rtsch wants to merge 4 commits into
HarlenPostill:mainfrom
manuel-rtsch:feat/sundial-keypad
Draft

manuel-rtsch wants to merge 4 commits into
HarlenPostill:mainfrom
manuel-rtsch:feat/sundial-keypad

Conversation

@manuel-rtsch

@manuel-rtsch manuel-rtsch commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What changed?

Closes #38.

Recognise the Sundial keypad and support its physical ring and holds.

  • Detect the swappable modules by an explicit device allowlist (gxa535_media, gxa535_qwerty, gxa535_t9, gxa535_gamepad, plus the older gxa535_keyboard). gxa535_phone is the phone's own key device and is not trusted for detection.
  • Add HoldToAct: a tap is delivered on release, a hold fires after the system long-press timeout without releasing the key.
  • Map the Sundial's Tab/Enter holds to Back/Options, and Previous/Next holds to left/right; a tap still sends the media command.
  • Follow ViewConfiguration.getLongPressTimeout() for T9 and Sundial holds. Back-to-dialer keeps its own 600 ms threshold.
  • Draw the physical 3x3 ring in onboarding and Keypad mapping, with hold hints.
  • Cover the detector, holds and onboarding layout with tests.

How to test

  1. Attach the Sundial module and open Settings → Setup & keypad tour: the demo shows the 3x3 ring and hold hints.
  2. Tap Tab / Enter: focus moves / activates as before.
  3. Hold Tab: Back. Hold Enter: Options.
  4. Tap Previous / Next: media previous / next. Hold them: focus left / right.
  5. Change the system long-press timeout (adb shell settings put secure long_press_timeout 700), relaunch, and confirm the hold threshold follows it.
  6. Attach a headset and press Play/Pause: the module must not switch to Sundial.

Real-device testing

  • Device model: SP01_FE_GE (Sidephone)
  • Android version: 12 (SDK 31)
  • Keypad or attached module: Sundial; T9, Mini QWERTY and Game controller for regression
  • Commit tested: f1e0a91 (298fa22 is a lint-only follow-up; the Back hold still calls the same dispatcher)
  • Test steps and results:
    • Key tester reported gxa535_media, gxa535_qwerty, gxa535_t9 and gxa535_gamepad for the modules. gxa535_phone did not change the detected module.
    • A headset Play/Pause did not switch the module to Sundial.
    • The Sundial demo showed the physical 3x3 ring and the hold hints.
    • Tab tap moved focus; Tab hold went Back. Enter tap activated; Enter hold toggled Options. Previous/Next taps sent media; holds moved focus left/right. A hold did not also fire the tap.
    • With long_press_timeout=700, holds followed the longer threshold; restored to 400 afterwards.
    • T9 long press followed the system value.
    • Back-to-dialer stayed at 600 ms with the keypad accessibility service enabled.
    • Custom mapping disabled the Sundial holds as intended.
    • No crashes or regressions observed.
  • Screenshots, recording, or relevant logs: key tester device names and hold timings recorded above; no additional capture.

Checklist

  • I created this branch from the latest main and this PR targets main.
  • I kept this PR focused and described its changes above.
  • I built the affected app and recorded the build command and result below.
  • I tested the changed behavior on a physical device and completed the device details above.
  • I checked nearby behavior for regressions, including keypad navigation and input when affected.
  • I ran the relevant automated tests and recorded commands and results below.
  • I added or updated tests where practical, or explained why no test is needed.
  • I updated documentation when behavior or setup changed.
  • I removed debug code, credentials, and personal data from the diff and test evidence.

Build and automated test results

Built with ./gradlew :app:installDev (APK produced; installed with adb install -r -d app/build/outputs/apk/dev/app-dev.apk because the previous dev build had a higher versionCode).

./gradlew :sidekit:testDebugUnitTest --tests '*KeypadDetectorTest' --tests '*NavControlsTest'   PASS
./gradlew :app:testDebugUnitTest --tests '*HoldToActTest' --tests '*OnboardingTest'             PASS
./gradlew :app:lintDebug                                                                        PASS
./gradlew spotlessCheck                                                                          PASS

Known issues and exceptions

  • The module allowlist is deliberate: a future gxa535_* module must be added to KeypadDetector.MODULE_DEVICES. This avoids trusting gxa535_phone by name alone.
  • The Sundial has no dedicated Back or Options key; those actions are only available as holds on Tab and Enter.

Maintainer review

Changes to main require approval from @HarlenPostill. Request review only when the required testing is complete.

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.
…e 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'
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'
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.
@manuel-rtsch

Copy link
Copy Markdown
Contributor Author

CI is now 9/10. The remaining Dependency review failure is not from this change: the action reports Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled (https://github.com/HarlenPostill/Sidekit/settings/security_analysis). It needs the upstream repository's Dependency graph enabled, or the workflow adjusted to skip on forks.

The earlier Android Lint failure was fixed in 298fa22: performSundialHold now suppresses GestureBackNavigation for the hardware Back hold, and dispatchTranslated carries the RestrictedApi suppression for super.dispatchKeyEvent.

@manuel-rtsch manuel-rtsch changed the title Support the Sundial keypad's physical layout and holds feat(keypad): support the Sundial keypad's physical layout and holds Nov 10, 2026
manuel-rtsch added a commit to manuel-rtsch/Sidekit that referenced this pull request Nov 10, 2026
Share the Item API with HarlenPostill#45 so the two branches merge cleanly, and leave
the Sundial demo layout to HarlenPostill#44.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support the Sundial keypad's physical layout and holds

2 participants