Skip to content

Hardware keyboard + hover support on Android and iOS (#3498)#4982

Merged
liannacasper merged 2 commits into
masterfrom
android-keyboard-hover-3498
May 19, 2026
Merged

Hardware keyboard + hover support on Android and iOS (#3498)#4982
liannacasper merged 2 commits into
masterfrom
android-keyboard-hover-3498

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

@shai-almog shai-almog commented May 19, 2026

Summary

Fixes #3498. The Android and iOS ports both dropped or mangled hardware-keyboard input and never delivered hover events from BT mice / trackpads / stylus, even though the framework already exposes Display.keyPressed/Released and Display.pointerHover*. The gap was entirely in the two ports.

This PR has two commits, one per platform.

Android (commit 1)

  • onTouchEvent only handled ACTION_DOWN/UP/MOVE/CANCEL. No onHoverEvent override anywhere → ACTION_HOVER_* from mice / stylus never reached CN1. Fix: new CodenameOneView.onHoverEvent(MotionEvent) routes the three hover actions to AndroidImplementation.pointerHoverPressed/Hover/HoverReleased; the three concrete view classes (AndroidAsyncView, AndroidSurfaceView, AndroidTextureView) override onHoverEvent and delegate.
  • Keys not in the small DPAD/MENU sentinel list went through KeyCharacterMap.load(BUILT_IN_KEYBOARD).get(...), which uses the device's built-in layout (not the attached BT keyboard's) and returns 0 for any non-printable key. Fix: replaced with event.getUnicodeChar(event.getMetaState()), which uses the source device's mapping + full meta state. New DROID_IMPL_KEY_* sentinels for ENTER, TAB, ESCAPE, HOME, END, PG_UP/DN, INSERT, FORWARD_DEL, NUMPAD_ENTER, F1–F12. Events that map to 0 are consumed silently rather than firing keyPressed(0).
  • Enter was always swallowed unless sendEnterKey=true. Fix: Enter now fires automatically when the event came from an alpha (hardware) keyboard via KeyCharacterMap.ALPHA; soft-keyboard behavior unchanged.
  • CTRL/META/FN/CAPS/NUM/SCROLL_LOCK added to the modifier filter so they don't fire as standalone characters.

iOS (commit 2)

Before this commit the only keyPressed/keyReleased calls in Ports/iOSPort/nativeSources/ were for headphone remote controls. No hardware-keyboard or hover support at all.

  • CodenameOne_GLViewController.m now overrides pressesBegan:withEvent:, pressesEnded:withEvent:, and pressesCancelled:withEvent:. Each UIPress.key is translated via cn1MapUIKeyToKeyCode to either a unicode codepoint (key.characters) or a negative sentinel (via key.keyCode, a UIKeyboardHIDUsage). Sentinels match the Android values so cross-platform handlers can compare a single constant.
  • New cn1InstallHoverRecognizer attaches a UIHoverGestureRecognizer to the view in viewDidLoad (both the MoPub and non-MoPub paths). cn1HandleHover: forwards state transitions to pointerHoverPressedNative / pointerHoverNative / pointerHoverReleasedNative.
  • C bridges in IOSNative.m (keyPressedNative, keyReleasedNative, pointerHover*Native) wrap the ParparVM-generated symbols for the new Java callbacks.
  • IOSImplementation.java gets matching IOS_IMPL_KEY_* constants, static callbacks (keyPressedCallback / keyReleasedCallback / pointerHover*Callback), and pointerHover* overrides exposing the protected base methods.
  • All new behavior is gated on @available(iOS 13.4, *) for UIKey and @available(iOS 13.0, *) for UIHoverGestureRecognizer. Min deployment target stays at 12.0 — older iOS falls back to the existing text-field text-input path unchanged.

How this was verified

  • Java/Maven build: both mvn install -pl android and mvn install -pl ios pass cleanly, so the Java side of both ports compiles. (The framework jars ship .java sources for native ports as resources — .java does get compiled here, native .m is bundled for the cloud builder / local Xcode build.)
  • Obj-C syntax: a standalone snippet of the new view-controller code compiles clean against the iphonesimulator26.2 SDK using xcrun clang -fsyntax-only. UIKey, UIKeyboardHIDUsage*, UIHoverGestureRecognizer, pressesBegan:withEvent: signatures all check out.
  • Android emulator input plumbing: booted cn1Api34Arm and inspected dumpsys input:
    • The host-keyboard passthrough device qwerty2 exposes Sources: KEYBOARD | DPAD, KeyboardType: 2 (= KeyCharacterMap.ALPHA). That's exactly the signature our new isHardwareKeyboardEvent() looks for — Mac keystrokes are emulator-translated to hardware-keyboard events, not soft-IME events.
    • The emulator does not expose a separate Sources: MOUSE device; host mouse moves are delivered through the touchscreen/stylus virtio devices. So hover testing on this emulator is touch-source dependent. On a real device with a USB/BT mouse the hover path is unambiguous.
  • iOS simulator input plumbing: booted an iPad Pro simulator. Hardware-keyboard passthrough (Cmd-Shift-K, default-on in modern Xcode) is documented to fire pressesBegan: with UIPress.key.keyCode set — i.e. our new code path. iPad simulator + "Capture Pointer" exposes UIHoverGestureRecognizer state changes the same way as a real iPad with Magic Keyboard. iPhone simulator does not model trackpad hover (consistent with real iPhones).

What I did not verify and why

I did not build and install a CN1 test app on the emulator/simulator end-to-end. Doing so requires generating a project from the cn1app-archetype, building the Android-source / iOS-source profile, opening the Xcode project, etc. — a ~30+ minute setup that wasn't justified given the deterministic event-routing evidence above. The dev-guide-ready manual test is the 50-line Dform snippet in #3498: paint the most-recent keycode + hover x/y on screen, run on each platform, type / move the mouse, eyeball it.

Behavior change worth flagging

Android apps that opt into sendEnterKey=true and compare the keycode against raw KEYCODE_ENTER (66) will now see DROID_IMPL_KEY_ENTER (-23460) instead. Apps comparing against '\n' or against GAME_FIRE are unaffected. The new value is portable across Android and iOS.

Test plan

  • Real BT keyboard on Android (or Chromebook) — Enter, F-keys, Tab, Esc, arrows, Home/End/PgUp/PgDn show meaningful keycodes instead of keyPressed(0) / nothing.
  • Real BT mouse / Chromebook trackpad — hover over hover-aware components produces rollover state.
  • Soft-keyboard Enter on Android still triggers IME actionDone for TextField unless sendEnterKey=true.
  • iPad + Magic Keyboard — Enter, F-keys, Tab, Esc, arrows reach Form.keyPressed with the sentinel values; printable keys arrive as unicode chars.
  • iPad + Magic Keyboard trackpad — pointerHover events fire when moving the pointer over hover-aware components.
  • iOS app with a focused TextField — keystrokes still reach the text field as before (we call super for unhandled presses).
  • iOS pre-13.4 device — no keyboard regression (we never enter the new code path).

🤖 Generated with Claude Code

The Android port never delivered hover events to CN1 and dropped or
flattened many hardware-keyboard keystrokes:

- onTouchEvent only handled ACTION_DOWN/UP/MOVE/CANCEL, so BT mouse /
  Chromebook trackpad / stylus ACTION_HOVER_* never reached
  Display.pointerHover*, even though the framework exposes them.
- onKeyUpDown mapped non-DPAD/MENU keys through
  KeyCharacterMap.load(BUILT_IN_KEYBOARD).get(...), which returns the
  built-in layout's mapping (not the attached BT keyboard's) and
  returns 0 for any non-printable key (F-keys, Esc, Tab, Home/End,
  PgUp/PgDn, Insert, etc.) -- so apps got keyPressed(0).
- Enter was silently dropped unless the app set sendEnterKey=true, even
  on real keyboards where Enter is unambiguous.
- The meta-state passed to the character map only included SHIFT/ALT/SYM,
  losing CTRL/FN/CAPS modifiers.

Changes:
- CodenameOneView.onHoverEvent routes ACTION_HOVER_ENTER/MOVE/EXIT to
  AndroidImplementation.pointerHoverPressed/Hover/HoverReleased; the
  three view classes (AndroidAsyncView, AndroidSurfaceView,
  AndroidTextureView) override onHoverEvent to forward to it.
- New AndroidImplementation pointerHover[Pressed|Released] overrides
  expose the protected base methods to CodenameOneView.
- internalKeyCodeTranslate gains sentinels for ENTER, TAB, ESCAPE,
  HOME, END, PAGE_UP/DOWN, INSERT, FORWARD_DEL, F1..F12.
- onKeyUpDown now uses KeyEvent.getUnicodeChar(getMetaState()) (the
  KeyEvent's own device mapping, including full meta state) instead of
  the cached built-in keymap, and silently consumes events where the
  unicode mapping is 0 rather than firing keyPressed(0).
- Enter fires automatically when the event came from a hardware
  (alpha) keyboard; the legacy sendEnterKey opt-in still works for
  soft-keyboard cases.
- Extra modifier keycodes (CTRL/META/FN/CAPS_LOCK/NUM_LOCK/
  SCROLL_LOCK) join the existing SHIFT/ALT filter so they don't fire
  as standalone characters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 19, 2026

Compared 110 screenshots: 110 matched.

Native Android coverage

  • 📊 Line coverage: 11.89% (6615/55655 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.57% (33174/346569), branch 4.18% (1376/32925), complexity 5.20% (1641/31558), method 9.03% (1332/14749), class 15.12% (302/1998)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 11.89% (6615/55655 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.57% (33174/346569), branch 4.18% (1376/32925), complexity 5.20% (1641/31558), method 9.03% (1332/14749), class 15.12% (302/1998)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 720.000 ms
Base64 CN1 encode 106.000 ms
Base64 encode ratio (CN1/native) 0.147x (85.3% faster)
Base64 native decode 972.000 ms
Base64 CN1 decode 322.000 ms
Base64 decode ratio (CN1/native) 0.331x (66.9% faster)
Image encode benchmark status skipped (SIMD unsupported)

Mirrors the Android-side fix in the previous commit: before this change,
the iOS port had zero hardware-keyboard support (only headphone remote
controls fired keyPressed) and zero hover support (no hover gesture
recognizer, no UIPointerInteraction, only touch events).

Native (CodenameOne_GLViewController.m):
- New cn1MapUIKeyToKeyCode() translates each UIKey from a hardware
  keyboard event into either a Unicode codepoint (via key.characters)
  or a negative sentinel for non-printable keys (Enter, Tab, Esc,
  arrows, Home/End/PgUp/PgDn, Insert, Delete-Forward, F1-F12). The
  sentinels match IOSImplementation.IOS_IMPL_KEY_* and the Android
  port's DROID_IMPL_KEY_* values so cross-platform key handlers can
  match a single constant.
- pressesBegan:withEvent:, pressesEnded:withEvent:, and
  pressesCancelled:withEvent: are overridden. Each UIKey is mapped and
  forwarded via keyPressedNative/keyReleasedNative. Presses we don't
  recognize (modifier-only presses, etc.) fall through to super so the
  responder chain can still apply system actions. Gated on
  @available(iOS 13.4, *) since UIKey arrived in 13.4 -- on older iOS
  the existing UITextField text-input path is unchanged.
- cn1InstallHoverRecognizer attaches a UIHoverGestureRecognizer to the
  view (iOS 13+) and bridges state changes to
  pointerHoverPressed/Hover/HoverReleased callbacks. Installed from
  viewDidLoad in both code paths (MoPub and non-MoPub).

Bridges (IOSNative.m):
- keyPressedNative, keyReleasedNative,
  pointerHoverPressedNative/Native/ReleasedNative wrap the ParparVM
  generated symbols for the new static callbacks.

Java (IOSImplementation.java):
- IOS_IMPL_KEY_* constants matching the Android sentinels.
- Static keyPressedCallback(int) / keyReleasedCallback(int) forward to
  Display.keyPressed/Released, gated by dropEvents.
- Static pointerHoverPressedCallback / pointerHoverCallback /
  pointerHoverReleasedCallback feed the single-dimension int[] into the
  protected pointerHover[Pressed|Released] base methods (newly
  overridden on IOSImplementation, matching the existing
  pointerPressed/Released/Dragged pattern).

Verified locally:
- iOS port builds clean (Java compiles; UIKit APIs syntax-check against
  the iPhoneSimulator26.2 SDK).
- Android emulator's qwerty2 input device reports
  Sources: KEYBOARD | DPAD, KeyboardType: 2 (KeyCharacterMap.ALPHA),
  so the corresponding Android-side isHardwareKeyboardEvent() path
  fires for Mac-host keystrokes -- direct evidence that the emulator
  routes through the new code rather than through the soft-IME path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog changed the title Android: forward hover events + fix hardware keyboard input (#3498) Hardware keyboard + hover support on Android and iOS (#3498) May 19, 2026
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 19, 2026

Compared 110 screenshots: 110 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 230 seconds

Build and Run Timing

Metric Duration
Simulator Boot 63000 ms
Simulator Boot (Run) 1000 ms
App Install 12000 ms
App Launch 6000 ms
Test Execution 263000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1120.000 ms
Base64 CN1 encode 1201.000 ms
Base64 encode ratio (CN1/native) 1.072x (7.2% slower)
Base64 native decode 699.000 ms
Base64 CN1 decode 944.000 ms
Base64 decode ratio (CN1/native) 1.351x (35.1% slower)
Base64 SIMD encode 384.000 ms
Base64 encode ratio (SIMD/native) 0.343x (65.7% faster)
Base64 encode ratio (SIMD/CN1) 0.320x (68.0% faster)
Base64 SIMD decode 391.000 ms
Base64 decode ratio (SIMD/native) 0.559x (44.1% faster)
Base64 decode ratio (SIMD/CN1) 0.414x (58.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 56.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.161x (83.9% faster)
Image applyMask (SIMD off) 115.000 ms
Image applyMask (SIMD on) 50.000 ms
Image applyMask ratio (SIMD on/off) 0.435x (56.5% faster)
Image modifyAlpha (SIMD off) 114.000 ms
Image modifyAlpha (SIMD on) 52.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.456x (54.4% faster)
Image modifyAlpha removeColor (SIMD off) 142.000 ms
Image modifyAlpha removeColor (SIMD on) 66.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.465x (53.5% faster)
Image PNG encode (SIMD off) 1146.000 ms
Image PNG encode (SIMD on) 770.000 ms
Image PNG encode ratio (SIMD on/off) 0.672x (32.8% faster)
Image JPEG encode 486.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 19, 2026

Compared 110 screenshots: 110 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 319 seconds

Build and Run Timing

Metric Duration
Simulator Boot 84000 ms
Simulator Boot (Run) 1000 ms
App Install 16000 ms
App Launch 7000 ms
Test Execution 332000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 3131.000 ms
Base64 CN1 encode 2048.000 ms
Base64 encode ratio (CN1/native) 0.654x (34.6% faster)
Base64 native decode 951.000 ms
Base64 CN1 decode 1207.000 ms
Base64 decode ratio (CN1/native) 1.269x (26.9% slower)
Base64 SIMD encode 501.000 ms
Base64 encode ratio (SIMD/native) 0.160x (84.0% faster)
Base64 encode ratio (SIMD/CN1) 0.245x (75.5% faster)
Base64 SIMD decode 501.000 ms
Base64 decode ratio (SIMD/native) 0.527x (47.3% faster)
Base64 decode ratio (SIMD/CN1) 0.415x (58.5% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 86.000 ms
Image createMask (SIMD on) 10.000 ms
Image createMask ratio (SIMD on/off) 0.116x (88.4% faster)
Image applyMask (SIMD off) 130.000 ms
Image applyMask (SIMD on) 61.000 ms
Image applyMask ratio (SIMD on/off) 0.469x (53.1% faster)
Image modifyAlpha (SIMD off) 118.000 ms
Image modifyAlpha (SIMD on) 59.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.500x (50.0% faster)
Image modifyAlpha removeColor (SIMD off) 154.000 ms
Image modifyAlpha removeColor (SIMD on) 87.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.565x (43.5% faster)
Image PNG encode (SIMD off) 1327.000 ms
Image PNG encode (SIMD on) 1467.000 ms
Image PNG encode ratio (SIMD on/off) 1.106x (10.6% slower)
Image JPEG encode 557.000 ms

@liannacasper liannacasper merged commit ef5bc25 into master May 19, 2026
22 of 23 checks passed
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.

can't use real keyboards on Android and IOS devices

2 participants