Skip to content

Tapjacking / screen-overlay detection in DeviceIntegrity - #5553

Merged
shai-almog merged 15 commits into
masterfrom
tapjacking-detection
Aug 16, 2026
Merged

Tapjacking / screen-overlay detection in DeviceIntegrity#5553
shai-almog merged 15 commits into
masterfrom
tapjacking-detection

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

What

Adds tapjacking / screen-overlay defense as the fourth family in com.codename1.security.DeviceIntegrity, next to the accessibility-abuse defense whose docs already named overlay malware as the threat but shipped nothing against it.

Tapjacking is the attack where a malicious app draws its own window over yours -- a fake confirmation button, or an invisible layer -- so the user authorizes something they cannot see. It is the standard companion to accessibility-service abuse in banking-malware kits. Before this PR the repo had no support for it: FLAG_WINDOW_IS_OBSCURED, setFilterTouchesWhenObscured and setHideOverlayWindows appeared in no port.

API

New TapjackingPolicy (OFF / REPORT / BLOCK / STRICT), which carries the blocking truth table as pure logic so every port decides identically and it is testable without a device. On DeviceIntegrity:

DeviceIntegrity.setTapjackingProtection(TapjackingPolicy.BLOCK);
DeviceIntegrity.addTapjackingListener(e -> { /* warn the user */ });
if (DeviceIntegrity.isHideOverlayWindowsSupported()) {
    DeviceIntegrity.setHideOverlayWindows(true);   // Android 12+
}

Threaded through Display to CodenameOneImplementation in the existing style. Zero-code alternative: android.tapjackingGuard=true (+ .mode=block|strict|report, .hideOverlays).

Three decisions worth reviewing

1. Tapjacking is deliberately NOT a compromise reason. Adding "tapjack" to getCompromiseReasons() would have been the one-line route -- it auto-flows to signals via UnprotectedEngine.toSignal(). But those reasons describe a standing property of the device and feed isDeviceCompromised(), whereas obscuring is transient with benign causes. Folding it in would report a rooted device whenever the notification shade was open. It gets its own state instead, and raises ShieldSignal.TAPJACK (severity 80) directly.

2. Android's own filtering does not work here. AndroidAsyncView.dispatchTouchEvent calls straight into CodenameOneView.onTouchEvent and only falls back to super when we decline, so View.onFilterTouchEventForSecurity -- what setFilterTouchesWhenObscured hooks -- never runs on the path CN1 components are reached through. The check is therefore explicit. setFilterTouchesWhenObscured is still set as defense-in-depth for the fallback path.

3. Blocked gestures are latched and claimed. Dropping only ACTION_DOWN would deliver a pointerReleased with no matching pointerPressed, so the block is latched through to UP/CANCEL. The handler returns true rather than the computed consumeEvent -- that value is false over a native peer, and returning it would send the obscured touch on to a BrowserComponent or native text field, i.e. exactly what is being withheld from everything else.

Platform support

  • Android -- full. Detection reads the obscured flags on each touch; setHideOverlayWindows needs API 31 and is reached reflectively (absent from the port's android.jar), as is FLAG_WINDOW_IS_PARTIALLY_OBSCURED (inlined 0x2).
  • iOS / other ports -- inherit the no-op default. No iOS app can draw over another, so there is nothing to detect; the docs say so rather than faking it. Screen recording is a separate threat already covered by ios.disableScreenshots.
  • Simulator -- Simulate > App Shield > Screen Overlay (Tapjacking), which drives the real reporting path rather than overriding the getter, so listeners and the signal actually fire.

Honest limitations (stated in the docs)

  • Detection is touch-driven, not a live window query. Android reports obscuring as a flag on a delivered MotionEvent, so if an overlay appears and the user never touches the screen, nothing is observed. That is why setHideOverlayWindows matters -- it is preventive rather than reactive.
  • STRICT carries real false-positive risk. Benign system UI sets the partial-obscured flag, so STRICT can discard taps the user meant. It is opt-in and documented as such; BLOCK is the recommendation.

Testing

13 new tests covering the truth table, the transition-only notification contract, listener lifecycle and the signal. Full local gate run (CI-faithful):

  • Quality report gate: EXIT=0 -- SpotBugs 0 across core-unittests/android/ios/ByteCodeTranslator/codenameone-maven-plugin, PMD 0, Checkstyle 0
  • 4863 tests, 0 failed; JavaSE port 224/224
  • check-cast-semantics.sh exit 0; check-since-tags.sh clean; all sources ASCII

Follow-up

The android.tapjackingGuard hint is implemented in this repo's AndroidGradleBuilder, covering local builds. Cloud builds need the same hint mirrored into com.codename1.build.daemon in the BuildDaemon repo. Until that lands the runtime API works everywhere but the build hint is a no-op on cloud builds.

🤖 Generated with Claude Code

Adds the fourth family to com.codename1.security.DeviceIntegrity, next to the
accessibility-abuse defense whose docs already named overlay malware as the
threat but shipped nothing against it. Tapjacking -- a malicious app drawing a
window over yours so the user confirms something they cannot see -- had no
support anywhere in the tree: FLAG_WINDOW_IS_OBSCURED, setFilterTouchesWhenObscured
and setHideOverlayWindows appeared in no port.

API: new TapjackingPolicy (OFF/REPORT/BLOCK/STRICT) carrying the blocking truth
table as pure logic, plus setTapjackingProtection, getTapjackingPolicy,
isScreenObscured, add/removeTapjackingListener, setHideOverlayWindows and
isHideOverlayWindowsSupported, threaded through Display to
CodenameOneImplementation in the existing style.

Android: the obscured-flag check sits in CodenameOneView.onTouchEvent (plus
hover and generic motion) because AndroidAsyncView.dispatchTouchEvent never
calls super, so the framework's own onFilterTouchEventForSecurity -- what
setFilterTouchesWhenObscured hooks -- is bypassed on the path CN1 components
are actually reached through. A blocked gesture is latched from ACTION_DOWN to
UP/CANCEL so the framework never sees a release without a press, and the
handler claims the event rather than returning the computed consumeEvent, which
is false over a native peer and would have let the touch fall through to a
BrowserComponent. setHideOverlayWindows reaches the API 31 call reflectively;
FLAG_WINDOW_IS_PARTIALLY_OBSCURED is inlined as 0x2 since it is absent from the
android.jar the port compiles against.

Reporting is transition-only -- ports call in per touch, so notifying
unconditionally would queue a runnable per touch and re-raise the same signal
forever. A change raises ShieldSignal.TAPJACK (severity 80) on the existing bus
and fires the listener. Deliberately NOT a compromise reason: those describe a
standing property of the device and feed isDeviceCompromised(), while obscuring
is transient, so folding it in would report a rooted device whenever the
notification shade was open.

iOS and the other ports inherit the no-op default -- no iOS app can draw over
another, so there is nothing to detect and the docs say so rather than faking
it. The simulator gets Simulate > App Shield > Screen Overlay (Tapjacking),
which drives the real reporting path instead of overriding the getter. The
android.tapjackingGuard build hint (+ .mode, .hideOverlays) wires it with no app
code.

Tests: 13 covering the truth table, the transition-only contract, listener
lifecycle and the signal. Docs: a security-chapter section that states the two
properties most easily got wrong -- detection is touch-driven rather than a live
window query, and a blocked gesture is dropped in full.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 049163e352

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/Android/src/com/codename1/impl/android/CodenameOneView.java Outdated
@shai-almog

shai-almog commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

✅ 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.

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 8.10% (7872/97198 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.08% (41675/515701), branch 2.89% (1406/48723), complexity 3.19% (1667/52272), method 4.91% (1357/27642), class 10.00% (368/3680)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 8.10% (7872/97198 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.08% (41675/515701), branch 2.89% (1406/48723), complexity 3.19% (1667/52272), method 4.91% (1357/27642), class 10.00% (368/3680)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 266ms / native 121ms = 2.1x speedup
SIMD float-mul (64K x300) java 149ms / native 96ms = 1.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 78.000 ms
Base64 CN1 decode 90.000 ms
Base64 native encode 368.000 ms
Base64 encode ratio (CN1/native) 0.212x (78.8% faster)
Base64 native decode 355.000 ms
Base64 decode ratio (CN1/native) 0.254x (74.6% faster)
Image encode benchmark status skipped (SIMD unsupported)

P1 -- setHideOverlayWindows silently did nothing. Window.setHideOverlayWindows
throws SecurityException without android.permission.HIDE_OVERLAY_WINDOWS, the
reflective call wrapped it and the catch only logged it, so the default
android.tapjackingGuard.hideOverlays=true bought no protection at all -- and
isHideOverlayWindowsSupported() still returned true, telling the app its native
peers were covered when nothing had happened.

The builder now declares the permission for projects that ask for the
mitigation, either through android.tapjackingGuard (unless .hideOverlays=false)
or the new standalone android.hideOverlayWindows hint for apps that drive the
runtime API without the launch-time guard. It is a normal install-time
permission, so no user prompt and no special-access screen, and it is declared
only for projects that opted in rather than added to every build.
isHideOverlayWindowsSupported() now reports the permission as well as the API
level, and setHideOverlayWindows logs a directed message instead of letting the
SecurityException be swallowed.

P2 -- the tapjacking latch ran too late. onTouchEvent calls
showSoftInputForActiveClient() on ACTION_UP before the old check position, so a
blocked gesture could still reopen the keyboard: a side effect surviving from a
touch the BLOCK/STRICT policy promised to withhold. The check now runs
immediately after the current-form guard, ahead of every side effect rather
than merely ahead of the pointer dispatch, with a comment saying so since the
ordering is the whole point.

Also fixes the CI copyright gate on the new snippet file and five Vale findings
in the new prose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8208b713f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java Outdated
notifyScreenObscured returned early whenever the boolean state was unchanged,
which conflated two consumers that want different things. The listener is a
state-change notification and must stay on the transition. The signal bus is
not: ShieldSignals.add already de-duplicates, refreshing the stored observation
and staying silent when nothing changed.

Gating the bus on the transition meant a partially obscured touch followed by a
fully obscured one left AppShield.getSignals() reporting "partiallyObscured"
while BLOCK was dropping a fully obscured attack, and left the timestamp -- the
answer to "when did this device last look obscured" -- stuck at the first
sighting for the whole duration of an attack.

Every obscured touch now reaches the bus; only a change fires the listener. Two
tests pin it: the partial-then-full escalation updates the detail while still
producing one callback, and a repeat sighting refreshes the timestamp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72ff4702a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java Outdated
…he simulator

P1 -- Window.setHideOverlayWindows sets a flag on the Window, and a
configuration change destroys and recreates the activity without touching this
AndroidImplementation instance. initSurface() already replayed the tapjacking
policy onto the new view but nothing replayed the overlay hiding, so an app that
hid overlays on a sensitive screen came back from a rotation with them allowed
and no way to notice. The request is now recorded before the API-level and
permission guards -- it is an intent, not a result -- and replayed in
initSurface() alongside the policy.

P2 -- the simulator's Screen Overlay toggle reported unconditionally, so it
raised the TAPJACK signal and flipped isScreenObscured() even under
TapjackingPolicy.OFF, which promises no detection and no reporting. Android
keeps that promise by returning from tapjacked() before it reports, so the
simulator was behaving unlike the device it stands in for. Reporting is now
gated on the active policy, and setTapjackingProtection re-applies the toggle's
state so switching detection on picks up an overlay the menu already has
switched on, and switching it off retracts it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 217 screenshots: 217 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69689537ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Comment thread Ports/Android/src/com/codename1/impl/android/CodenameOneView.java Outdated
@shai-almog

shai-almog commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 58000 ms
Simulator Boot (Run) 0 ms
App Install 22000 ms
App Launch 1000 ms
Test Execution 517000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 68ms / native 3ms = 22.6x speedup
SIMD float-mul (64K x300) java 71ms / native 4ms = 17.7x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 549.000 ms
Base64 CN1 decode 333.000 ms
Base64 native encode 1101.000 ms
Base64 encode ratio (CN1/native) 0.499x (50.1% faster)
Base64 native decode 770.000 ms
Base64 decode ratio (CN1/native) 0.432x (56.8% faster)
Base64 SIMD encode 58.000 ms
Base64 encode ratio (SIMD/CN1) 0.106x (89.4% faster)
Base64 SIMD decode 85.000 ms
Base64 decode ratio (SIMD/CN1) 0.255x (74.5% faster)
Base64 encode ratio (SIMD/native) 0.053x (94.7% faster)
Base64 decode ratio (SIMD/native) 0.110x (89.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 14.000 ms
Image createMask (SIMD on) 3.000 ms
Image createMask ratio (SIMD on/off) 0.214x (78.6% faster)
Image applyMask (SIMD off) 70.000 ms
Image applyMask (SIMD on) 47.000 ms
Image applyMask ratio (SIMD on/off) 0.671x (32.9% faster)
Image modifyAlpha (SIMD off) 389.000 ms
Image modifyAlpha (SIMD on) 310.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.797x (20.3% faster)
Image modifyAlpha removeColor (SIMD off) 171.000 ms
Image modifyAlpha removeColor (SIMD on) 209.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.222x (22.2% slower)

P2 -- setTapjackingProtection(OFF) changed only the policy, and switching off is
also what stops anything from clearing the state later: a port stops reporting
under OFF (Android returns from tapjacked() before it reports), so a
screenObscured left true had isScreenObscured() answering true for the rest of
the process and the listener never received its closing transition. The base
implementation now retracts the state when the policy stops detecting, which is
what the JavaSE override was already doing on its own.

P2 -- hover is a paired sequence, not a gesture. Filtering it the way touches
are filtered meant an overlay appearing mid-hover swallowed the ACTION_HOVER_EXIT
for an ACTION_HOVER_ENTER that had already reached pointerHoverPressed, leaving
a hover-aware component stuck hovered with nothing to release it. A sequence
that starts obscured is still withheld in full -- no enter was delivered, so no
exit is owed -- while one that started clean always gets its release regardless
of the flags by then. Tracked in its own latch rather than the gesture one,
since a stylus can hover while a finger is down.

Two tests: OFF retracts the state and delivers the closing transition, and
tightening BLOCK to STRICT does not pretend the overlay went away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63c7772ce5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@shai-almog

shai-almog commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

The example in the developer guide, and the matching one in the DeviceIntegrity
javadoc, re-read isScreenObscured() inside the callback. That races with the
thing the callback is announcing: the state is written on the platform's input
thread while callbacks are delivered on the EDT, so a busy EDT can let a later
clean touch land first and the global query then answers false -- skipping the
security warning for the very event that raised it.

The listener already carries the state in the ActionEvent source precisely so
the callback does not have to ask again; the documentation just failed to use
it. Both examples now branch on the event, via Boolean.TRUE.equals rather than a
cast, and the addTapjackingListener javadoc states the rule instead of merely
mentioning that the source exists.

A test pins it: two changes queued before the EDT drains each arrive carrying
their own value while the global query has already moved on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 734eb18763

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 366ff2feba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
shai-almog and others added 2 commits August 16, 2026 15:32
The zero-code hint is honoured by two separate codebases -- this local builder
and com.codename1.build.daemon.AndroidGradleBuilder in the BuildDaemon repo --
and a hint wired on only one side is worse than an absent one: the documentation
promises launch-time protection and a cloud-built app ships believing it is
guarded when the policy was never set and the permission never declared.

The mirror is BuildDaemon#181, verified against that repo's 316 tests and both
guard scripts, and the two are meant to land together. Recorded next to the
block rather than only in the PR description, so the next person changing either
side sees the obligation in the code they are editing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… filter

setFilterTouchesWhenObscured looked like free defense in depth for the
super.dispatchTouchEvent fallback, but it filters per event, and per-event
filtering is exactly what this feature has been avoiding everywhere else. An
overlay appearing after a clean ACTION_DOWN on a native peer had the framework
reject the ACTION_UP that peer was already owed, leaving a BrowserComponent or
native text field pressed with nothing to release it -- the same pairing failure
the gesture latch and the hover latch both exist to prevent.

The flag is gone, and with it the only reason AndroidImplementation overrode
setTapjackingProtection and initSurface replayed the policy onto a new view. A
gesture that starts obscured never reaches super.dispatchTouchEvent anyway,
because onTouchEvent claims it, so peers are covered by the whole-gesture
decision rather than a per-event one. Recorded on the latch field, which is
where the next person would go looking for why the obvious flag is unused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almog force-pushed the tapjacking-detection branch from 366ff2f to f400eea Compare August 16, 2026 12:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f400eea7ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java Outdated
@shai-almog

shai-almog commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 106000 ms
Simulator Boot (Run) 1000 ms
App Install 19000 ms
App Launch 9000 ms
Test Execution 605000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 55ms / native 3ms = 18.3x speedup
SIMD float-mul (64K x300) java 56ms / native 3ms = 18.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 419.000 ms
Base64 CN1 decode 163.000 ms
Base64 native encode 925.000 ms
Base64 encode ratio (CN1/native) 0.453x (54.7% faster)
Base64 native decode 537.000 ms
Base64 decode ratio (CN1/native) 0.304x (69.6% faster)
Base64 SIMD encode 93.000 ms
Base64 encode ratio (SIMD/CN1) 0.222x (77.8% faster)
Base64 SIMD decode 71.000 ms
Base64 decode ratio (SIMD/CN1) 0.436x (56.4% faster)
Base64 encode ratio (SIMD/native) 0.101x (89.9% faster)
Base64 decode ratio (SIMD/native) 0.132x (86.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.286x (71.4% faster)
Image applyMask (SIMD off) 248.000 ms
Image applyMask (SIMD on) 176.000 ms
Image applyMask ratio (SIMD on/off) 0.710x (29.0% faster)
Image modifyAlpha (SIMD off) 155.000 ms
Image modifyAlpha (SIMD on) 195.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.258x (25.8% slower)
Image modifyAlpha removeColor (SIMD off) 246.000 ms
Image modifyAlpha removeColor (SIMD on) 187.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.760x (24.0% faster)

@shai-almog

shai-almog commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

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

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 77ms / native 3ms = 25.6x speedup
SIMD float-mul (64K x300) java 55ms / native 3ms = 18.3x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 184.000 ms
Base64 CN1 decode 108.000 ms
Base64 native encode 755.000 ms
Base64 encode ratio (CN1/native) 0.244x (75.6% faster)
Base64 native decode 386.000 ms
Base64 decode ratio (CN1/native) 0.280x (72.0% faster)
Base64 SIMD encode 77.000 ms
Base64 encode ratio (SIMD/CN1) 0.418x (58.2% faster)
Base64 SIMD decode 64.000 ms
Base64 decode ratio (SIMD/CN1) 0.593x (40.7% faster)
Base64 encode ratio (SIMD/native) 0.102x (89.8% faster)
Base64 decode ratio (SIMD/native) 0.166x (83.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 19.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.105x (89.5% faster)
Image applyMask (SIMD off) 93.000 ms
Image applyMask (SIMD on) 70.000 ms
Image applyMask ratio (SIMD on/off) 0.753x (24.7% faster)
Image modifyAlpha (SIMD off) 59.000 ms
Image modifyAlpha (SIMD on) 82.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.390x (39.0% slower)
Image modifyAlpha removeColor (SIMD off) 87.000 ms
Image modifyAlpha removeColor (SIMD on) 78.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.897x (10.3% faster)

The policy is set from the EDT while the state is reported from the platform's
input thread, and the two fields form one invariant. The losing interleaving:
the input thread reads BLOCK inside tapjacked() and decides to report, the EDT
then stores OFF and clears the state, and only afterwards does the report land.
That left isScreenObscured() true with nothing able to clear it -- reporting has
stopped under OFF -- and handed a listener an obscured callback for protection
the app had already switched off.

Both fields now move under one lock, and an obscured report arriving while the
policy is not detecting is dropped. Retractions are never dropped, only
assertions, so the opposite interleaving still clears correctly.

A lock rather than volatile fields, because the invariant spans two fields
rather than the visibility of either. Dispatch stays outside it: firing
listeners or touching ShieldSignals while holding a lock is the deadlock
ShieldSignals documents at length, and the event already carries its own value,
so it does not need the state to still be current when it is delivered.

Two tests cover both orderings. The suite's baseline policy is now BLOCK, since
a port only reports while detecting and a test reporting under OFF was
exercising a state no port can produce.

Also drops a test that built a TestCodenameOneImplementation to check the
default: its constructor assigns the static singleton, so constructing one
repointed UITestBase's implementation away from the instance Display holds and
desynchronized every later test in the class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7fac02e199

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java Outdated
The previous fix moved the policy under a lock but still applied the retraction
through a follow-up notifyScreenObscured(false) after that lock was released,
which opened the mirror-image race. One thread switches to OFF; a second
re-enables a detecting policy and reports a fresh sighting; the first thread's
retraction finally lands and wipes it, leaving BLOCK or STRICT active over a
state claiming the screen is clear, and a listener whose last word was a stale
clearing event.

The state is now cleared in the same critical section as the policy, so the pair
moves together or it is not an invariant. What happens after the lock is pure
announcement: the new fireTapjackingState mutates nothing, and drops itself when
the value it would announce is no longer the one the API reports -- the same rule
ShieldSignals applies to its own bus, for the same reason. A superseded
announcement was already wrong when it was queued.

reEnablingDetectionAfterOffKeepsTheNewSighting covers the ordering, asserting
both that the newer sighting survives and that a listener does not end on the
stale clear.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9067ed160d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Pushing back on a review comment rather than changing behaviour, with the
reasoning next to the code so it survives.

The claim was that these statements land after super.createStartInvocation() and
so run after the application's start(), leaving the first screen unguarded. They
do not. They are emitted by createOnCreateCode, whose output is the tail of the
generated onCreate; createStartInvocation emits i.start() into the generated
run() method that onResume reaches, and Android has returned from onCreate long
before that. Display is usually uninitialized that early, which is deliberate:
Display.setProperty parks both values and Display.init() applies them, and that
init runs in onResume ahead of the start call.

So the policy and the overlay request are both in force before application code
can show a form. Moving the emissions into the start path, as suggested, is what
would actually open the window this warns about.

The same note is mirrored into the BuildDaemon copy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ec1a6a499

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java Outdated
The supersession check ran before handing the event to EventDispatcher, which is
too early: an off-EDT report is delivered through callSerially, and in the gap
the EDT can clear the state and announce that synchronously. The older event then
arrived last, leaving a listener holding true while the policy was OFF and the
API reported false.

The check now happens inside the delivered runnable, where the listener actually
runs, so a transition superseded in flight is dropped instead of announced late.
That is the rule ShieldSignals already applies to its own bus -- which the
comment here cited while the code checked in the wrong place.

aTransitionSupersededBeforeDeliveryIsDropped covers it, reporting and switching
off from off the EDT exactly as a port's input thread would.

eachEventCarriesTheStateItAnnouncedNotTheLatestOne asserted the old behaviour --
two rapid transitions both delivered -- which this deliberately stops doing. It
is now eachDeliveredEventCarriesTheStateItAnnounced, draining between the two, so
it still pins the contract that matters: an event that is delivered carries its
own value rather than the current global state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64918004a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java Outdated
shai-almog and others added 2 commits August 16, 2026 19:00
… currency

The delivery-time recheck from the previous commit fixed stale ordering by
discarding, which throws away real history. An overlay that appears and
withdraws before the EDT drains -- precisely what a malicious overlay does after
a blocked ACTION_DOWN -- had its obscured callback dropped because the state was
already clear again, so the app was told only that the screen was clear and
never that a gesture had been blocked. That is the one thing this listener
exists to tell it.

Ordering was always the real requirement, not currency. Announcements are now
queued inside the same critical section that writes the state, so delivery order
equals state-change order on any number of threads: callSerially appends to one
FIFO whoever calls it, the EDT included, so nothing can overtake anything. Every
transition is delivered, each carrying its own value, and the last one delivered
is by construction the current state -- which is what the earlier stale-ordering
report actually needed.

Queuing happens under the lock deliberately, guarded on Display.isInitialized()
because that is what makes callSerially queue instead of running the task
inline, and running it inline there would execute application code under the
lock. It costs nothing: a listener can only be registered through Display, so
before init there is nobody to tell.

The dispatch is a named static class rather than an anonymous one; it captures
nothing from the implementation instance, and SpotBugs' SIC_INNER_SHOULD_BE_STATIC_ANON
is a forbidden pattern in this repo.

Tests follow the corrected contract: queued transitions arrive in order and end
on the current state, and an overlay that withdraws before the EDT drains is
still reported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build-test (8) failed on the previous commit with MissingOverride against the
anonymous Runnable's run(). Replacing it with a named static class for SpotBugs
carried the same gap across, so this fixes the actual violation rather than
relocating it.

MissingOverride is a forbidden rule here and applies to everything, interface
implementations included -- Runnable.run() among them.

Worth recording why CI caught this and the local build did not: the Maven PMD
plugin is report-only and prints BUILD SUCCESS with violations sitting in
target/pmd.xml. The authoritative gate is
.github/scripts/generate-quality-report.py, which is what build-test (8) runs,
and it now passes locally along with SpotBugs 0 and Checkstyle 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06051dd670

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java Outdated
The dispatch held the EventDispatcher, so the listener set was read when the
runnable finally ran. A listener registering during an EDT backlog was therefore
told about a change that predated it -- and whether it heard that depended on
whether some unrelated listener happened to exist when the transition was
queued, since an empty set queues nothing at all. That conditional history is
the part that makes it a bug rather than a preference.

The listeners are now captured at queue time, copied under the dispatcher's own
monitor because getListenerCollection hands back the live list. A listener
removed inside the same window still receives that one in-flight event, which is
the same trade ShieldSignals already makes by snapshotting its array at the same
point; matching the sibling bus is worth more here than splitting the
difference.

aListenerRegisteredAfterATransitionDoesNotHearIt covers it, and asserts the
other half too -- the listener that was registered when the change happened
still gets it.

Verified with the authoritative gate this time, not just SpotBugs: PMD 0,
Checkstyle 0, SpotBugs 0, 4874 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almog merged commit 2c0220b into master Aug 16, 2026
46 checks passed
@shai-almog
shai-almog deleted the tapjacking-detection branch August 16, 2026 17:56
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.

1 participant