Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Single `:app` module. All source in `app/src/main/java/com/ah/taplock/`:
- **TapLockAccessibilityService.kt** — Core service: accessibility overlays, floating lock button, and double-tap detection on status bar and lock screen
- **TapLockWidgetProvider.kt** — RemoteViews-based 1x1 widget
- **TapLockTileService.kt** — Quick Settings tile
- **RootLock.kt** — Optional root lock method: detects rooted devices, verifies su access, and locks by injecting KEYCODE_POWER via a persistent root shell (no lock-screen flicker); callers fall back to the accessibility lock on failure
- **DoubleTapDetector.kt** — Time-window tap detection with injectable clock
- **VibrationHelper.kt** — API-level-aware vibration (Android 13+ vs older)
- **Utils.kt** — Accessibility state check utility
Expand All @@ -42,3 +43,4 @@ Kotlin official style (`kotlin.code.style=official`). No detekt/ktlint — use `
- Widget uses RemoteViews (no Compose). Custom icon requires manual cache invalidation via ACTION_APPWIDGET_UPDATE broadcast.
- VibrationHelper branches on Build.VERSION for Android 13+. Both paths must be maintained.
- SharedPreferences are synchronous throughout — changes immediately trigger recomposition via preference listener.
- Root lock: pref `lock_method` (ACCESSIBILITY|ROOT), prompt-once flag `root_mode_prompt_shown`. The su shell is reused across locks; widget/tile can lock via `RootLock.performRootLock` without a live service instance. Every root path must keep its accessibility fallback.
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
[![Download on Google Play](https://img.shields.io/badge/Download_It-Play_Store-green?logo=googleplay)](https://play.google.com/store/apps/details?id=com.ah.taplock)
![F-Droid Version](https://img.shields.io/f-droid/v/com.ah.taplock)


<img src="app/src/main/ic_launcher-playstore.png" width="256" style="display: block; margin: auto;" alt="TapLock Logo">

A minimalist Android widget that lets you lock your screen with a double tap. Perfect for one-handed use and quick screen locking.

## Why TapLock?

While there are several screen-locking apps available on the Play Store, they all share a concerning characteristic: they're closed source while requiring significant system permissions.
While there are several screen-locking apps available on the Play Store, they all share a concerning characteristic: they're closed source while requiring significant system permissions.

The Accessibility Service permission, which is necessary for screen locking functionality, is particularly powerful - it can:

Expand All @@ -25,12 +24,14 @@ The Accessibility Service permission, which is necessary for screen locking func
- Access sensitive information

With closed-source applications, you're essentially trusting unknown code with these extensive capabilities. You have no way to verify:

- What data they're actually collecting
- Where that data might be sent
- What other hidden functionalities might exist
- How securely they handle their permissions

TapLock solves this trust problem through radical transparency:

- 100% open source - every line of code is visible and auditable
- No internet permissions - physically cannot transmit data
- Minimal, focused codebase - easy to review and verify
Expand All @@ -46,13 +47,15 @@ You don't have to trust our privacy claims - you can verify them yourself in the
- 🎯 Customizable double-tap timeout
- 🔍 Transparent widget - doesn't interfere with your home screen aesthetics
- 🛡️ Minimal permissions - only uses accessibility service for screen locking
- 🪄 Optional root lock mode on rooted devices - simulates a real power button press, so the screen turns off instantly without the lock-screen flicker
- 📱 Resizable widget
- 🎨 Material Design 3 UI
- 🔒 Privacy focused - collects no personal data

## Getting Started

### Requirements

- Android 12 (API 31) or higher
- 1MB of free space

Expand All @@ -73,26 +76,50 @@ You don't have to trust our privacy claims - you can verify them yourself in the
2. Adjust the double-tap timeout in Settings (default: 300ms)
3. The widget is transparent by default and can be resized to your preference

#### Root lock mode (optional, rooted devices only)

If TapLock detects a rooted device, it offers a choice of lock method:

- **Accessibility** (default): uses Android's `GLOBAL_ACTION_LOCK_SCREEN`, which briefly shows the lock screen before the display turns off.
- **Root (power button)**: injects a power button key event through a root (`su`) shell, so the screen turns off instantly with no flicker - exactly like pressing the physical power button.

When you select root mode, TapLock requests superuser access once from your root manager (e.g. Magisk). If root access is denied or the shell fails, TapLock automatically falls back to the accessibility lock. With root mode enabled, the widget and Quick Settings tile can lock the screen even without the accessibility service, though the tap-based lock triggers (status bar, lock screen, floating button) still require it.

##### Root option not showing up?

TapLock detects root by looking for the `su` binary. Managers like Magisk mount `su` into an app's namespace **only when the app's process starts** — so if TapLock was already running (even in the background) before root became visible to it, the root option cannot appear until the process is restarted. TapLock re-checks on every resume, but the namespace limitation means a restart is usually what actually fixes it.

If you're rooted but don't see the lock method choice:

1. **Force-close TapLock and reopen it**: go to system Settings → Apps → TapLock → Force stop (or long-press the app icon → App info → Force stop), then open the app again. A device reboot also works.
2. **Check your root manager's deny/hide list**: make sure TapLock is _not_ in Magisk's DenyList (or your manager's equivalent root-hiding feature), otherwise `su` stays invisible to it permanently.
3. **Grant superuser access before first launch** (if your manager supports it): managers such as KernelSU or APatch let you pre-grant root to an app from their own UI. Doing this before opening TapLock for the first time ensures root is detected immediately. Magisk only lists apps after their first `su` request, so with Magisk just use the force-close approach and approve the prompt when TapLock asks.

When the superuser prompt appears, choose "Grant" (and "remember"/"forever" if offered) so subsequent locks don't re-prompt.

## Building from Source

1. Clone the repository:

```bash
git clone https://github.com/modelorona/TapLock.git
```

2. Open the project in Android Studio Hedgehog or later

3. Build the project:

```bash
./gradlew assembleDebug
```

## Privacy

TapLock is designed with privacy in mind:

- No internet connectivity required
- No data collection
- Minimal permissions (only Accessibility Service)
- Minimal permissions (only Accessibility Service; superuser access is optional, opt-in, and used solely to simulate the power button)
- Open source for transparency

## Contributing
Expand All @@ -101,4 +128,4 @@ Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details
This project is licensed under the Apache License 2.0 - see the LICENSE file for details
5 changes: 3 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
<uses-permission android:name="android.permission.ACCESSIBILITY_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- Detect Advanced Protection Mode (Android 16+), which blocks third-party accessibility services. -->
<!-- Detect Advanced Protection Mode (Android 16+), which blocks third-party accessibility
services. -->
<uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" />

<application
Expand Down Expand Up @@ -77,4 +78,4 @@

</application>

</manifest>
</manifest>
6 changes: 6 additions & 0 deletions app/src/main/java/com/ah/taplock/AppExclusionDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp

/**
* Dialog listing launchable [apps] with checkboxes to build [excludedPackages]. Filtering by
* [searchQuery] matches both label and package name; all state is hoisted to the caller so
* toggles persist immediately.
*/
@Composable
fun AppExclusionDialog(
apps: List<TapLockAppInfo>,
Expand Down Expand Up @@ -124,6 +129,7 @@ fun AppExclusionDialog(
)
}

/** Single app row: checkbox plus label and package name, toggled by tapping anywhere. */
@Composable
private fun AppExclusionRow(
app: TapLockAppInfo,
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/com/ah/taplock/DoubleTapDetector.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.ah.taplock

/**
* Time-window double-tap detector. [currentTimeMs] is injectable so tests can drive the clock
* deterministically instead of relying on real time.
*/
class DoubleTapDetector(
private val currentTimeMs: () -> Long = System::currentTimeMillis
) {
private var lastTapTime = 0L

/**
* Records a tap at [tapTimeMs] and returns true when it lands within [timeoutMs] of the
* previous one. A detected double tap consumes both taps, so a third tap starts a new window.
*/
fun onTap(timeoutMs: Int, tapTimeMs: Long = currentTimeMs()): Boolean {
if (lastTapTime != 0L && tapTimeMs - lastTapTime < timeoutMs) {
lastTapTime = 0L
Expand All @@ -14,6 +22,7 @@ class DoubleTapDetector(
return false
}

/** Forgets the pending tap, e.g. when the tap target moves or is resized mid-sequence. */
fun reset() {
lastTapTime = 0L
}
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/com/ah/taplock/EdgeZoneComponents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp

/**
* Miniature phone mockup shown in settings that mirrors the configured edge and corner zones.
* Dimensions are scaled to the fixed preview frame, not real screen pixels, so widths map the
* supported dp range onto a visually useful size.
*/
@Composable
fun EdgeZonePreview(
leftEnabled: Boolean,
Expand Down Expand Up @@ -162,6 +167,10 @@ fun EdgeZonePreview(
}
}

/**
* Full-screen translucent overlay drawn while the user drags an edge/corner slider, showing the
* real zone positions at true dp sizes on the actual screen.
*/
@Composable
fun EdgeZoneLiveOverlay(
leftEnabled: Boolean,
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/ah/taplock/LockScreenZoneComponents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp

/**
* Miniature phone mockup shown in settings that mirrors the configured lock-screen tap zone,
* scaled to a fixed preview frame rather than real screen pixels.
*/
@Composable
fun LockScreenZonePreview(
lockZonePercent: Int,
Expand Down Expand Up @@ -75,6 +79,10 @@ fun LockScreenZonePreview(
}
}

/**
* Full-screen translucent overlay drawn while the user drags the lock-zone sliders, showing the
* real zone extent on the actual screen.
*/
@Composable
fun LockScreenZoneLiveOverlay(
lockZonePercent: Int,
Expand Down
Loading