A self-discipline companion that keeps you off the apps you can't stop opening — without ever feeling like parental control software.
English · 简体中文 → README.zh-CN.md
⚠️ Notice: This is a single-developer, Chinese-first project. English is provided for international contributors, but day-to-day commits and issue discussion are mostly in Chinese. You're welcome to contribute in either language.
SilentGuardian is an Android app that quietly limits the total time per day you can spend on apps you've flagged as problematic (short-video, social feeds, games). When your quota runs out, it doesn't yell at you or block the screen — it just cuts the network to those specific apps via a local VPN blackhole. The app stays installed, opens normally, shows the feed, just nothing loads. The psychological effect is more "ugh, boring" than "the system is fighting me" — which is the point.
It's designed for adults who want to self-regulate, not for parental control over children. There's no cloud account, no remote dashboard, no analytics on you. Everything runs locally on your device.
| Feature | What it does |
|---|---|
| ⏱️ Daily time quota | Set a max total time per app per day. Counted only while the app is actually in the foreground. |
| 🌙 Bedtime mode | Hard lockout window (e.g. 23:00–07:00) where guarded apps are blocked regardless of remaining quota. |
| 📅 Weekday / Weekend split | Different limits for weekdays vs. weekends. Finally let yourself breathe on Saturday. |
| 🕳️ Per-app network blackhole | When quota is exhausted, a local VpnService drops packets only for the targeted apps. Other apps and the rest of the system keep working normally. |
| 📞 Call-aware fail-safe | If you're on a call (TelephonyManager.callState != IDLE), the VPN immediately stops blocking and skips that poll cycle. No more missed calls because you ran out of TikTok time. |
| 🌒 Screen-off pause | ACTION_SCREEN_OFF suspends the polling coroutine. Quota only burns while the screen is on and the app is foreground. |
| 📅 Day-rollover reset | Doesn't trust system day-change broadcasts. Compares today's date string against the stored last-seen date every cycle, force-resets if mismatched. |
| 🛡️ Anti-uninstall + PIN lock | Optional Device Admin activation prevents uninstall; PIN gate prevents changing settings in a moment of weakness. |
| 🌐 Bilingual UI | Full Simplified Chinese and English string resources. System locale picks automatically. |
flowchart TB
subgraph UI["UI Layer (Activities + Fragments)"]
Splash[SplashActivity]
Main[MainActivity]
Home[HomeFragment]
Perm[PermissionFragment]
Other[OtherFragment - Settings]
AppSelect[AppSelectActivity]
Pin[PinLockActivity]
end
subgraph Core["Core Services"]
Monitor[MonitorService<br/>5s polling daemon<br/>foreground sticky]
VPN[BlackholeVpnService<br/>per-app packet filter]
end
subgraph Data["State & Storage"]
DM[DataManager<br/>MMKV wrapper]
UM[UpdateManager]
AR[AdminReceiver<br/>Device Admin]
end
subgraph System["Android APIs"]
USM[UsageStatsManager]
TM[TelephonyManager]
Screen[SCREEN_ON/OFF<br/>BroadcastReceiver]
end
Monitor -->|query MOVE_TO_FOREGROUND| USM
Monitor -->|callState check| TM
Monitor --> Screen
Monitor -->|exceeded?| VPN
Monitor --> DM
Main --> DM
UM --> DM
AR --> DM
Polling loop (every 5 seconds):
- Check
TelephonyManager.callState— if notIDLE, disable VPN block, skip cycle. - Compare today's date with stored date — if different, reset all counters.
- Query
UsageStatsManager.queryEvents()for most recentMOVE_TO_FOREGROUNDevent. - If foreground app is in the guarded list, increment its time counter by Δ.
- If any counter exceeds its limit (or current time is inside bedtime window), establish
BlackholeVpnServicetargeting that app's UID. - Otherwise, ensure VPN is stopped.
| Layer | Choice | Why |
|---|---|---|
| Language | Kotlin + Coroutines | Single-language codebase per project convention |
| UI | Material Components + ViewBinding-less (findViewById) | Stable, no Compose migration burden |
| Local storage | MMKV 1.3.x | mmap-based, survives crashes, 100× faster than SharedPreferences |
| Permissions | XXPermissions 18.x | Handles the messy cross-version permission matrix |
| Utilities | AndroidUtilCode 1.31.x | Package list, app icons, common helpers |
| Analytics | Microsoft Clarity | Anonymous session recordings (opt-in by installation) |
Build tooling: Gradle 9.x, AGP, JDK 17, minSdk=24, targetSdk=34.
- Android Studio Ladybug or newer (or just JDK 17 + Gradle 9.x CLI)
- An Android device or emulator running Android 7.0+ (API 24+)
# Debug build (no signing setup required)
gradle assembleDebug
# Release build (requires your own keystore — see below)
cp app/keystore.properties.example app/keystore.properties
# Edit keystore.properties to point to your keystore + password
gradle assembleReleaseNote on signing: The release
keystore.propertiesandrelease.keystoreare gitignored. The repository's build will produce an unsigned release APK unless you provide your own. The maintainer's production signing key is never in the repo — see Security notes.
adb install -r app/build/outputs/apk/debug/app-debug.apkOr grab the latest signed release APK from the official download endpoint.
The app walks you through a strict permission chain — this is intentional, each one is required and the order avoids dialog overlap:
- Notifications (Android 13+) — for the foreground service notification.
- Usage access (
PACKAGE_USAGE_STATS) — required to know which app is in the foreground. - Battery optimization exemption — so the OS doesn't kill the polling daemon.
- VPN consent — shown the first time the block triggers.
- Device Admin (optional) — for anti-uninstall protection.
| Permission | Why it's needed |
|---|---|
BIND_VPN_SERVICE |
The blocking mechanism itself — local VPN with no external server. |
FOREGROUND_SERVICE + FOREGROUND_SERVICE_SPECIAL_USE |
Keeps MonitorService alive. Android 14 requires the special-use subtype declaration. |
PACKAGE_USAGE_STATS |
The only reliable way to know which app is currently in the foreground. |
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS |
Without this, Doze mode suspends the 5s polling within minutes of screen-off. |
BIND_DEVICE_ADMIN |
Optional. Used only if you enable anti-uninstall protection. |
RECEIVE_BOOT_COMPLETED |
Auto-restart the daemon after device reboot. |
POST_NOTIFICATIONS |
Android 13+ requirement for the foreground notification. |
QUERY_ALL_PACKAGES |
To let you pick from all installed apps in the guarded-apps picker. |
INTERNET, MODIFY_AUDIO_SETTINGS |
Reserved for the planned "AI voice companion" feature. |
This app controls the user's network. "Zero collateral damage to other phone functionality" is the single hardest constraint. Every one of these is in code, not aspirational:
- 📞 Call protection: checked at the start of every polling cycle. Off-hook = unblock immediately, skip counting. Never miss a call because of quota.
- 🔌 Lifecycle hardening:
MonitorService.onDestroy()is guaranteed to tear down the VPN. Even on force-stop. - 💥 Crash self-heal:
Thread.setDefaultUncaughtExceptionHandleris registered inApp.kt. First action on any uncaught crash: stop the VPN, restore network. - 📅 Day-rollover: system day-change broadcasts are unreliable across OEMs. We re-derive "today" from
LocalDate.now()on every cycle. - 🌒 Screen-off suspend:
ACTION_SCREEN_OFFpauses the polling coroutine. No phantom drain while in your pocket. - 💤 Doze jump detection: if a polling interval measures >30s of perceived time, the timestamp is reset rather than counting it as one tick (which would otherwise let you rack up "5 seconds" per cycle while actually playing for 30).
Comments in code are tagged [Failsafe] or [Hack] so future maintainers (human or AI) know not to "clean up" what looks redundant.
- Core polling + per-app VPN block
- Daily quota + per-session limit + cooldown
- Weekday/weekend split
- Bedtime mode
- PIN lock + Device Admin anti-uninstall
- In-app self-update (signed APK from configured URL)
- AI voice companion — when blocked, the mic-based assistant offers a 60-second voice interaction as a "soft exit ramp" away from the timed-out app. (Tracked in docs/mic_test_demo.md.)
- Per-app granularity for bedtime window
- Widget showing remaining quota today
- Companion WearOS tile
This is a small project, mostly maintained by one person. PRs are welcome, especially for:
- Bug fixes that maintain the zero-collateral-damage invariant
- Localization to more languages (
values-<locale>/strings.xml) - OEM-specific compatibility fixes (Xiaomi, Huawei, Oppo, Vivo boot-time quirks)
Please read CLAUDE.md and AGENTS.md first — they encode the architectural constraints that aren't obvious from the code alone.
Conventional Commits (feat:, fix:, refactor:, chore:). See git log for examples.
- The maintainer's production signing keystore was rotated before open-sourcing. The old one is scrubbed from git history. Anyone who cloned before rotation still has a stale copy — this is unavoidable.
- If you find a security issue, please email [email protected] before opening a public issue.
Copyright © 2025–present Hangzhou Qiantang Yuesi Software Studio.
Distributed under the GNU General Public License v3.0.
Any derivative distribution must:
- Keep the source open under the same license
- Disclose modifications clearly
- Include the original copyright and license
This is intentionally not MIT/Apache. If you want to embed SilentGuardian into a closed-source product, contact the maintainer for a commercial license.
- Tencent MMKV — fast local storage
- XXPermissions — sane permission handling
- AndroidUtilCode — utility belt
- Microsoft Clarity — anonymous UX telemetry








