Skip to content

Keyboard Handling — Android Validation Handoff

Self-contained brief for validating the react-native-keyboard-controller keyboard work on Android. The migration was implemented and hand-verified on iOS (iPad + simulator). Android automated validation was deferred to a Linux machine with a KVM emulator; this page is what an agent (or engineer) reads to run it cold.

What was changed (the one pattern)

Keyboard avoidance is standardized on react-native-keyboard-controller:

  • KeyboardProvider wraps the app once in frontend/app/_layout.tsx (inside SafeAreaProvider, above ThemeProvider; an ancestor of TrueSheetProvider).
  • frontend/hooks/useKeyboard.ts exposes useKeyboard() (Reanimated height/ progress + visible); useKeyboardVisible() re-exports from it.
  • frontend/components/ui/KeyboardAwareScroll.tsx is the one scroll container for normal (non-sheet) screens. Web falls back to a plain Animated.ScrollView.
  • Docked bars that must ride the keyboard use the library's KeyboardStickyView (chat/QuickReplyBar.tsx, app/(capture)/compose-moment.tsx publish footer).
  • Sheets keep TrueSheet's native keyboard handling. No keyboard-controller view is nested inside a TrueSheet. components/ui/Sheet.tsx now defaults footerOptions.keyboardOffset so docked sheet footers clear the keyboard.

The Android-specific reason this matters: Expo SDK 55 forces edge-to-edge, under which the IME arrives as a window inset. The library consumes that inset (WindowInsetsCompat), which is what the manifest adjustResize alone no longer does. Confirming that on a real Android surface is the point of this pass.

Environment (Linux emulator)

Follow the emulator setup in Native Testing ("Quick Android emulator") and the android-emulator-qa skill. Key points:

  • Backend host: the emulator's localhost is itself. Point the app at 10.0.2.2:8080 (emulator alias for the host loopback) or the Mac/host LAN IP. EXPO_PUBLIC_API_URL is inlined at bundle time and .env.local beats a shell export, so edit .env.local then expo start --clear.
  • Credentials: env only, never hardcoded. TOMODA_E2E_USER / TOMODA_E2E_PASS. Local seed logins are testuserN@tomoda.life / password; the adb login flow prefers a username without special characters (e.g. tanakatesto), since soft-keyboard text injection drops @/..
  • Driver: frontend/scripts/android-e2e.sh (adb: tap_id/wait_id/open_sheet by uiautomator resource-id = testID; input text; screencap pulled to .android-e2e/). The "vision" check is the agent reading each PNG.

The keyboard flow to add to android-e2e.sh

Add a keyboard case to the case block (run after login). Each row: focus the field, type, capture a PNG, then read the PNG and assert the condition.

Flow Open via testID Assert on the screenshot Fixes
Chat composer open a chat chat-message-input composer + send visible above keyboard iOS-cover / Android inset
compose-moment capture flow → compose compose-publish caption visible; Publish riding the keyboard caption covered; footer covered
QuickReplyBar open a moment → reply quick-reply-input reply bar sits ~8px above the keyboard, not a keyboard-height too high Android double-lift
Friends search friends → search, tap first result search field + first row first tap selects the row (navigates), not just dismiss first-tap swallowed
MomentEditSheet navigate to a moment → tap edit (see note) sheet root + -footer caption scrolls clear; docked Save above keyboard non-scroll form stranded inputs
Credential form change-password route change-password-new field + submit above keyboard inconsistent Android behavior

testID / open-path prerequisites

Instrumented in code already: chat-message-input, quick-reply-input, compose-publish, change-email-input, change-username-input, change-password-new, change-password-confirm.

Sheets that a headless adb tap cannot present (native BottomSheetDialog) open via the __DEV__ useE2ESheet deep link (frontend/utils/e2eSheet.ts), registered for quickadd, create-plan, create-event: tomoda://<route>?__sheet=<name>.

MomentEditSheet is a per-card sheet (components/moments/MomentPostcard.tsx), so it is not deep-link registered. Reach it by navigating to a moment and tapping its edit affordance by testID rather than a __sheet= link.

Expected results

Each row above should pass, matching the iOS behavior. Classify a failure before filing:

  • CODE: the keyboard covers the field / the bar sits at the wrong height / the first tap is swallowed. File against the surface name.
  • NETWORK / env: login fails, no data, blank lists. Fix the backend host / seed / .env.local and re-bundle; not a keyboard bug.

The single most load-bearing Android check is that focusing any migrated input under edge-to-edge actually shifts content up (the library consuming the IME inset). If that holds, the pattern is proven on Android.

See also

  • Native Testing — the emulator setup and the broader focus/keyboard checklist
  • android-emulator-qa skill — the authoritative adb driver writeup