Per-app lock for Android. Pick any installed app; the next time it comes to the foreground, a full-screen gate asks for your fingerprint, face, or the phone's own PIN/pattern/password before the app is revealed.
Kotlin + Jetpack Compose, no third-party services, nothing leaves the device.
If AppLock is useful to you, a ⭐ helps other people find it.
| Pick what to lock | Finish setup | The gate |
|---|---|---|
![]() |
![]() |
![]() |
| Locked apps float to the top. | Only missing grants are listed. | Shown the instant a locked app opens. |
The gate normally raises the system biometric prompt over itself immediately. It
is absent above because the real build sets FLAG_SECURE, and both it and the
system prompt refuse to be screenshotted — so these were taken from a temporary
build with that flag and the auto-prompt removed.
Android has no "lock this app" API, so this does what every app locker does:
- Watch which package just came to the foreground.
- Cover it with our own activity the instant a locked package appears.
- Authenticate via the system biometric prompt. Pass → the gate finishes and the app is there. Back/cancel → the user is sent to the home screen.
The app is never actually blocked; the gate just lands on top of it faster than the user can interact with it.
AppWatchService (primary) |
LockGuardService (fallback mode) |
|
|---|---|---|
| Mechanism | AccessibilityService, TYPE_WINDOW_STATE_CHANGED events |
Polls UsageStatsManager every 300 ms |
| Latency | Effectively instant | 200–700 ms — the app flashes into view first |
| Battery | Negligible, event-driven | Noticeable; needs a foreground-service notification |
| Grant | Settings → Accessibility | Settings → Special access → Usage access |
The accessibility route is strongly preferred. Polling exists for users who would rather not enable an accessibility service, and as the takeover path when the accessibility service dies.
An accessibility service alone is not enough: when Android, an OEM battery manager or a recents swipe kills the process, nothing re-arms the watcher and every locked app opens freely until AppLock is launched again.
LockGuardService closes that. It runs as a foreground service whenever anything is
locked, checks on a 5-second heartbeat that a watcher is genuinely live, falls back to
polling if the accessibility service has gone away, and names the mode actually in
force in its notification. WatchdogJob, a persisted periodic job, restarts the guard
if the process is killed anyway. When nothing is protecting anything, a notification
and a card on the home screen say so, rather than failing silently.
None of this survives a force stop: that cancels our jobs along with everything else, and no unprivileged app can schedule its way out of it. Reaching the force-stop button means reaching Settings, so locking Settings itself is the only defence AppLock can offer there.
Full mechanism, and a map of the source, in docs/architecture.md.
SessionGate holds unlocks in memory only — a reboot or process death means everything
is locked again. An unlock ends when:
- the screen turns off, or
- the user leaves the app (when re-lock is set to Immediately), or
- the configured delay elapses (1 or 5 minutes).
A Quick Settings tile suspends locking for 15 minutes — for handing the phone to someone, or for testing something without the gate in the way. It comes back by itself, and tapping the tile again brings it back early.
Pausing asks for the same authentication the gate does. A tile that switched locking off on one tap would be a bypass of the entire app, reachable from the shade over a locked phone, so the tap only opens a prompt and the pause begins once that prompt passes. Resuming never asks: turning protection back on cannot need permission.
Like an unlock, the pause is held in memory only, so a process death comes back locking rather than paused. While it is running, the guard notification says AppLock is paused rather than claiming to watch.
Requires the Android SDK and JDK 21 — AGP does not run on JDK 26, so
gradle.properties pins org.gradle.java.home to /usr/lib/jvm/java-21-openjdk.
Change that line if your JDK lives elsewhere.
./gradlew assembleGithubDebug
adb install -r app/build/outputs/apk/github/debug/app-github-debug.apkgithub (default) |
play |
|
|---|---|---|
| Shipped as | APK on the Releases page | App Bundle on Google Play |
| Self-updater | Checks GitHub, downloads, offers to install | Absent |
INTERNET |
Declared | Not declared — no network access at all |
REQUEST_INSTALL_PACKAGES |
Declared | Not declared |
Uploading the play bundle is Console work; see
docs/releasing.md.
Google Play forbids an app that downloads and installs its own APK, so the
updater cannot ship in a Play build. Both flavours compile against the same
update API: src/play supplies stubs whose check() returns null, which
leaves the banner and the launch check inert, and the permissions and
FileProvider the real updater needs are declared only in
src/github/AndroidManifest.xml. So the Play build contains no update code at
all, rather than update code that is switched off.
.github/workflows/build.yml lints both flavours and builds the github APK on
every push to main, on pull requests, and on demand via Actions → Build APK
→ Run workflow. The APK lands as the applock-v<run number> artifact on the
run page, with the lint report beside it, and a signed Play bundle is built
alongside it.
The runner strips the org.gradle.java.home line from gradle.properties
first, since that path is local to a developer machine.
Every push to main publishes a GitHub Release by itself — build.yml creates
the tag v1.<run number> and attaches the APK, giving a permanent public
download link (Actions artifacts expire and need a login; release assets do
not). There is no tag to push by hand, and run_number only ever increases, so
the version never goes backwards either.
Pull requests build and lint but publish nothing.
Releases are signed from v1.8 on; earlier ones were debug-signed. Keystore and secret setup is in docs/releasing.md.
The app shows a checklist for whatever is missing:
- Accessibility → AppLock app watcher — how the app knows what you opened.
- Display over other apps — required, or Android 10+ blocks the gate from launching from the background.
- Battery optimisation off — optional, but Android may otherwise kill the watcher.
The home screen also recommends locking the Settings app once, dismissibly. Force stop, the accessibility toggle and uninstall all live in Settings, so locking it means each of those needs authentication first — the closest an unprivileged app gets to tamper protection.
These are inherent to doing this without system privileges:
- A user can disable the accessibility service in Settings, force stop the app, or uninstall it. Blocking that needs Device Admin, which is heavily restricted. Locking the Settings app raises the bar, since all three routes go through it.
- Aggressive OEM task-killers (Xiaomi, Oppo, Huawei, Samsung) kill background
services;
LockGuardServiceandWatchdogJobrecover from that within a job tick, but those devices still want manual whitelisting to avoid the gap entirely. - There is a brief window where the locked app is visible before the gate lands — short with the accessibility watcher, clearly visible with the poller.
- If no biometric is enrolled and no screen lock is set, the gate opens rather than refusing, since there would be nothing to check against.
- Recents-screen previews of locked apps are not suppressed; only the gate itself sets
FLAG_SECURE.
- AppLock's own PIN, separate from the phone's
- Intruder photo after failed attempts
- Auto-lock newly installed apps
- Hide locked apps from the recents switcher
- Per-app re-lock delay
- Backup and restore the locked list
- Uninstall protection (Device Admin)
- Publishing the
playflavour on Google Play
Unresolved, as opposed to the inherent limits above:
- Releases before v1.8 are debug-signed, so the signed builds since cannot upgrade one of those in place — Android refuses an update signed by a different key, so anyone who installed early has to uninstall first.
- The fingerprint path has never run on real hardware — only the emulator's PIN.
- OEM task-killers may stop the watcher; untested.
- CI versions are
1.<run_number>, so a counter reset would send versions backward.
All pull requests, feedback and contributions are welcome! Please read the Contributing guidelines for more about how to contribute and how to build and run the project.
The most useful contribution needs no code at all: a real-device report. The fingerprint path has only ever run against an emulator's PIN, and OEM background-killing is untested — if you run AppLock on a real phone, especially a Xiaomi, Oppo, Huawei or Samsung, open a device report saying what it did.
If AppLock is useful to you, a ⭐ helps other people find it.
- Architecture — staying alive, the source map, theme and session internals.
- Contributing — requirements, build, run, and what CI checks.
- Releasing — signing keys, and publishing the
playbuild to Google Play. - Privacy policy


