Back to Blog

Building Doomscroll Detox to interrupt endless scrolling

Emmanouil Athanasopoulos4 min read826 words
React NativeAccessibilityNative AndroidDigital Wellness

The Motivation Behind the Project

Doomscroll Detox is a digital wellness app built to interrupt the habit of endless scrolling. The challenge was not just blocking content, but doing it in a way that still lets the phone remain useful for messaging and daily communication.

Core Features and Design Goals

  • Bedtime scheduling: a routine-based control that makes the app useful beyond one-off blocking.
  • Instant shield toggle: a fast on/off protection layer when the user wants an immediate reset.
  • Targeted blocking: the app focuses on feeds and short-form content while keeping messaging accessible.
  • Breathing exercises and usage stats: tools that help the app support behavior change instead of only restriction.

Technical Implementation

  • Built a 1,700-line Android AccessibilityService in Java that uses getRootInActiveWindow() to scan the full UI tree of the foreground app and determine the exact screen the user is on. For each major app, it implements a dedicated detection strategy: TikTok — detects safe screens (Inbox, Profile, Settings) by activity class and tab selection state, then blocks by default on the main feed activity, with optional friend-reel passthrough; Instagram — only blocks the Reels tab and clips_viewer views while allowing Home, DMs, and Profile, with a 2.5-second redirect grace period to prevent re-blocking while the DM screen loads; YouTube — counts shorts/reel view IDs in the tree (threshold ≥3 to distinguish the Shorts player from the Home shelf), checks for shorts_player_container and reel_recycler resource IDs, and persists Shorts state until a safe tab is selected; Facebook — blocks Watch/Reels tabs while allowing the main feed.
  • Implemented two distinct blocking modes: full-block (GLOBAL_ACTION_BACK + GLOBAL_ACTION_HOME to immediately exit the app) and feed-block (redirectToSafeScreen that programmatically clicks the app's messaging/inbox tab using AccessibilityNodeInfo.performAction, falling back to GLOBAL_ACTION_BACK). For inline reels on the Instagram home feed, the service steals audio focus via AudioManager.requestAudioFocus() to pause video playback without navigating away.
  • Built an anti-scroll timer system that tracks per-app usage time while blocking is active, accumulating scroll session durations in a HashMap. When a configurable time limit is reached, the app is added to the full-block list and an overlay countdown is shown via WindowManager. The service also includes a watchdog ticker that runs every second to handle schedule transitions, re-launch the ForegroundService, and manage anti-scroll state.
  • Designed a JS-to-Native bridge via NativeModules that syncs blocked app lists (full/feed/anti-scroll), schedule configurations (weekday/weekend/custom-day in minute-precision), and usage stats queries — with full platform-safe stubs that return sensible defaults on non-Android or Expo Go environments so the app never crashes.
  • Implemented anti-backup reinstall protection: on hydration, the app queries native AccessibilityService and UsageStats permissions to detect if Android's auto-backup restored state on a fresh install where system permissions were wiped, automatically resetting onboarding if permissions are missing.

Challenges I Faced Along the Way

The biggest challenge was the moving target of social media app UIs. Instagram, TikTok, and YouTube frequently change their internal activity names, view hierarchies, and resource IDs with every app update. What worked last week might break tomorrow. I built the detection logic with multiple fallback strategies for each app — if the primary detection method (resource ID matching) fails, the service falls back to activity class name matching, and then to heuristic content analysis. Even so, maintaining compatibility requires periodic updates whenever a major social media app ships a new version. Another major challenge was the Google Play review process. Apps that use AccessibilityService face intense scrutiny from Google's review team, and I had to provide detailed documentation explaining why the app needs this permission and how it uses it responsibly.

The Technology Stack

Built with Expo Router, React Native, TypeScript, and a 1,700-line Java native module. The AccessibilityService intercepts TYPE_WINDOW_STATE_CHANGED, TYPE_WINDOW_CONTENT_CHANGED, TYPE_VIEW_CLICKED, and TYPE_VIEW_SCROLLED events, using full UI tree scanning (getRootInActiveWindow) for per-app feed detection. Schedule state lives in SharedPreferences (accessible without JS context), and a ForegroundService + AlarmManager-based PollReceiver keep the service alive across process kills. The JS bridge uses NativeModules with a complete stub fallback layer for safe degradation.

What I Would Do Differently Next Time

If I were starting over, I would build an automated UI testing pipeline that installs the latest versions of Instagram, TikTok, and YouTube in an emulator and verifies that the detection logic still works correctly. Right now this testing is manual, which is time-consuming and error-prone. I would also consider a cloud-based configuration system where detection rules can be updated without shipping a new app version — this would significantly reduce the turnaround time when a social media app changes its UI structure.

Final Reflections

Doomscroll Detox is a good example of building around the habit itself instead of only building a pretty interface. The technical depth required to intercept and redirect app behaviour at the accessibility service level was far beyond what I initially expected, and it taught me a great deal about Android's accessibility framework, process lifecycle management, and the practical realities of building software that needs to interact with other apps at a deep level.