A focus timer with teeth. Holdfast covers the apps you choose until the clock runs out. Pick a length, build a blocklist, and — when you want no wiggle room — turn on Strict mode, which removes the early-exit button for the whole session.
Holdfast is an original Android app written from scratch in Kotlin and Jetpack Compose. Package: app.holdfast. Version: 1.1.0 (debug build, versionCode 2). It is not affiliated with Regain or any other app; the name, code, copy, and visual design are original.
- Onboarding that explains the Accessibility permission, lets you pick apps, and checks notifications before the first session.
- A blocklist built from installed launcher apps, with search. Settings, launchers, and system/permission UI are deliberately excluded so you can never lock yourself out of the phone.
- Presets of 15, 25, 45, and 60 minutes, plus a custom slider from 1 minute to 3 hours.
- Scheduled sessions: pick days, a start time, a length and whether the session is strict. Holdfast arms a single alarm for the soonest occurrence, re-arms it after reboot or a timezone change, and can start the session with the app closed. Exact alarms are used when Android allows them; otherwise the UI warns that a start may drift by a few minutes.
- Feed guard (BETA): keep YouTube, Instagram, or Facebook usable but cover the Shorts/Reels player while a session runs. Detection looks only at the player's view id, so a redesign causes a missed block, never a false block of the normal app.
- Strict mode: no early-exit control in the app or on the blocker for the duration of the session.
- A full-screen blocker over each blocked app, with the app's icon, a live countdown, a progress bar, and a short line of encouragement.
- A foreground timer service with a persistent countdown notification, plus an
AlarmManagerbackstop so the session ends even if the UI is gone. - Completion notifications and a local history of completed and early-ended sessions, with daily totals, a seven-day chart, and a streak.
- A weekly report at the top of History: focused time, session count and finish rate for the last seven days, compared with the seven days before, plus the best day of the week.
- Resume after process death or reinstall: an active session is restored from disk, and a session that expired while the app was dead is banked as complete.
- No network permission, no account, no analytics. Everything stays in app-private
SharedPreferences.
AppWatchAccessibilityServicereceivesTYPE_WINDOW_STATE_CHANGED/TYPE_WINDOWS_CHANGEDevents and reads only the foreground package name. For feed guard it additionally reads view identifiers to recognise the Shorts/Reels player. It never reads screen text, captions, keystrokes, or messages.- If the active session's blocklist contains that package, the service adds a full-screen
TYPE_ACCESSIBILITY_OVERLAYwindow (BlockerOverlay). This needs no "draw over other apps" grant. - For feed-guarded apps the service also listens to
TYPE_WINDOW_CONTENT_CHANGED, debounced to 400 ms, and checks for the Shorts/Reels pager's view id. A match raises the same overlay in feed mode, with a "Leave Shorts/Reels" button and a Back handler that leaves the feed. The rest of the app stays usable. - The overlay consumes Back and keeps the screen awake. In Strict mode it has no end control at all.
FocusServiceruns as aspecialUseforeground service and owns the countdown notification. AnAlarmManageralarm is scheduled at the session's end as a safety net.- On expiry,
FocusControllercloses the session, tears down the overlay, stops the service, posts the completion notification, and appends aSessionRecordto history. FocusStoreis the single in-process source of truth. Writes go toSharedPreferencessynchronously before the new state is published, so the UI, foreground service, and accessibility service cannot disagree.
The overlay is intentionally skipped for com.android.systemui, android, and permission-controller packages, so the notification shade, power menu, and runtime permission dialogs remain usable. Events from Holdfast's own overlay window are ignored unless they come from MainActivity, which prevents a show/hide feedback loop.
| Onboarding | Blocklist | Blocked app |
|---|---|---|
| Active session | History | Completion notification |
| Schedules | Feed guard | Weekly report |
More screenshots, including the back-button trap and automatic expiry, are in demo/. The full test record is in TEST-REPORT.md.
Requirements: JDK 17 or newer, Android SDK Platform 35, and a local.properties with sdk.dir pointing at the SDK.
export JAVA_HOME=/opt/homebrew/opt/openjdk@21 # or your JDK 17+ path
./gradlew assembleDebugThe APK lands at app/build/outputs/apk/debug/app-debug.apk (about 17 MB in debug).
Tests and lint:
./gradlew testDebugUnitTest lintDebugLast result: 14/14 unit tests pass; 0 lint errors, 41 warnings (warnings are mostly SharedPreferences.commit() being intentional, dependency update notices, and hard-coded overlay strings).
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n app.holdfast/.MainActivityWalk through onboarding, then enable the watcher:
Settings → Accessibility → Downloaded apps → Holdfast → Use Holdfast.
On an emulator or test device this can be scripted:
adb shell settings put secure enabled_accessibility_services \
app.holdfast/app.holdfast.service.AppWatchAccessibilityService
adb shell settings put secure accessibility_enabled 1Reinstalling the APK unbinds the accessibility service; re-enable it after every install. An active session survives reinstall because it is stored on disk.
- Accessibility is mandatory. Without it the timer still runs, but nothing is covered. The home screen warns when the watcher is off.
- Strict mode is a commitment, not a prison. Force-stopping Holdfast, clearing its data, uninstalling it, disabling Accessibility, or rebooting before the boot receiver runs can get around any third-party blocker. Holdfast says so in onboarding rather than pretending otherwise.
- One overlay covers the whole screen. System UI and permission dialogs remain reachable by design. A determined user can still use the notification shade.
- Feed guard is BETA and fail-open. It matches published view ids for YouTube Shorts, Instagram Reels, and Facebook Reels. If an app renames its player, the guard misses the feed; it will not block the normal app. Live Shorts detection could not be exercised on the test emulator because its YouTube client is stuck on an update screen, so detection is covered by unit tests and the negative on-device test only.
- Schedules depend on Android's alarm policy. With exact alarms allowed, starts landed on the second in testing. Without it Holdfast uses inexact alarms and tells the user starts may be a few minutes late.
- Tested against YouTube and Chrome on an Android 15 emulator. Other apps, launchers, and OEM skins may behave differently, especially around first-run dialogs or split-screen.
- Debug-signed. This is a working prototype, not a Play Store release.
- Unit tests cover the pure logic, not the device. 14 JVM tests cover schedule arithmetic and feed-marker matching. The blocking path is verified by the manual device pass in
TEST-REPORT.md; there are no instrumentation tests.
app/src/main/java/app/holdfast/
MainActivity.kt Compose entry point
HoldfastApp.kt Application init + session resume
data/AppRepository.kt Installed-app discovery and icons
data/FocusStore.kt Persisted session, blocklist, feed guard, schedules, history
data/Schedule.kt Schedule model + pure date planner
service/AppWatchAccessibilityService.kt Foreground + feed detection
service/BlockerOverlay.kt Full-screen accessibility overlay (block + feed modes)
service/FeedDetector.kt Shorts/Reels view-id matcher
service/FocusService.kt Foreground countdown service
service/FocusController.kt Session lifecycle and alarms
service/ScheduleScheduler.kt Single-alarm schedule arming
service/ScheduleReceiver.kt Scheduled start + re-arm
service/Notifications.kt Ongoing + completion + missed-schedule notifications
service/BootReceiver.kt Resume and re-arm after reboot / time change / update
service/SessionActionReceiver.kt End/expire broadcasts
ui/ Onboarding, home, apps, history, theme
app/src/test/java/app/holdfast/
data/SchedulePlannerTest.kt 8 schedule tests
service/FeedMarkersTest.kt 6 feed-marker tests
app/src/main/res/
layout/view_block_overlay.xml
xml/accessibility_service_config.xml
demo/ Test screenshots
Holdfast deliberately avoids Usage Access and SYSTEM_ALERT_WINDOW. Usage Access is batch-oriented and laggy for an immediate block; a draw-over-other-apps window requires a separate scary permission and can be disabled independently. The accessibility root window gives an immediate, event-driven answer to "which app is in front?", and the accessibility overlay layer is the correct place to cover it. The trade-off is the Accessibility permission itself, which onboarding explains plainly.