Skip to content

Fix overlay flicker and removal race conditions#26

Merged
kasnder merged 2 commits into
mainfrom
claude/fix-overlay-artifacts-mdBQr
Apr 19, 2026
Merged

Fix overlay flicker and removal race conditions#26
kasnder merged 2 commits into
mainfrom
claude/fix-overlay-artifacts-mdBQr

Conversation

@kasnder
Copy link
Copy Markdown
Owner

@kasnder kasnder commented Apr 18, 2026

Summary

Addresses two reported overlay artifacts: (1) overlays briefly disappearing and reappearing while still in the target app, and (2) overlays staying visible after leaving the app.

Flicker — onAccessibilityEvent is too eager to clear

  • Only TYPE_WINDOW_STATE_CHANGED from a foreign package now counts as a leave signal. Content changes and scrolls from systemui (e.g. status bar updates from an arriving notification) or the launcher no longer trigger a clear while the target app is still the active window underneath.
  • Foreign window-state changes are debounced by 150ms; any subsequent event from the target package cancels the pending clear. This absorbs transient focus changes such as a briefly pulled notification shade.

Race — OverlayManager add/remove was partly async, partly sync

  • All OverlayManager operations are now synchronous on the caller's thread (always main for the accessibility service). A queued addView can no longer run after a clear and leave an orphan view attached but untracked.
  • clearOverlays can no longer empty the tracker list before removeView has actually detached the views.
  • forceClearAllOverlays collapses to a clearAllOverlays alias since both paths are now equivalent.

Cleanup

  • Pending clear runnable is cancelled in all teardown paths (reloadRulesFromSource, reevaluateBlockingState, screen off, onInterrupt, onDestroy).

Considered and rejected

  • Per-element grace period (e.g. keep an overlay alive 500ms past its node's last appearance): would pin overlays at stale positions while a feed scrolls beneath them (Instagram-style), which is a worse artifact than a brief flicker.
  • Widening info.packageNames = null to receive events for unmonitored apps: would fix the case where a switch never delivers an event to the service, but increases battery cost on every other app.

Test plan

  • Open WhatsApp with a rule active; pull and release the notification shade quickly. Overlay should not disappear.
  • Open WhatsApp with a rule active; let a notification arrive without pulling the shade. Overlay should not flicker.
  • Scroll the Instagram feed (or similar) with a rule active. Overlays should follow scrolled elements without sticking at stale positions.
  • Switch from a rule app to launcher. Overlays should clear within ~150ms.
  • Switch from a rule app to another rule app. Overlays for the first should clear, the second's should appear.
  • Lock screen, then unlock and return to the rule app. Overlays should re-evaluate correctly.

https://claude.ai/code/session_018ChatjS8SFHCh8w7ySjCte

claude added 2 commits April 18, 2026 11:57
- Only act on TYPE_WINDOW_STATE_CHANGED when a foreign package takes focus;
  content changes and scrolls from systemui or the launcher no longer clear
  overlays while the target app is still the active window below (e.g. a
  status bar update from an arriving notification).
- Debounce the clear by 150ms on foreign window-state changes; a subsequent
  event from the target package cancels the pending clear. This absorbs
  transient focus changes such as a briefly pulled notification shade.
- Make OverlayManager.clearOverlays consistent with forceClearOverlays: check
  getParent() before removeView and move the tracker removal inside the
  posted runnable so the list cannot diverge from the window state.
- Cancel the pending clear runnable from all teardown paths
  (reloadRulesFromSource, reevaluateBlockingState, screen off, onInterrupt,
  onDestroy).

https://claude.ai/code/session_018ChatjS8SFHCh8w7ySjCte
Window operations and tracker updates now happen together on the caller's
thread (always main for this service) instead of being posted. A queued
addView can no longer run after a clear and leave an orphan view attached
but untracked, and clearOverlays can no longer empty the tracker list
before removeView has actually detached the views.

Callers (BaseDistractionControlService) drop the Handler argument and
forceClearAllOverlays collapses to a clearAllOverlays alias, since the
two paths are now equivalent.

No per-element grace period: keeping an overlay past its node's
disappearance would leave it pinned at a stale position while the feed
scrolls beneath it (Instagram-style), which is a worse artifact than a
brief flicker.

https://claude.ai/code/session_018ChatjS8SFHCh8w7ySjCte
@kasnder kasnder merged commit 3cb7dce into main Apr 19, 2026
2 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.

2 participants