Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@
local.properties
app/release
vendor
.bundle
fastlane/README.md
fastlane/report.xml
fastlane/metadata/android/screenshots.html
fastlane/metadata/android/**/images/screenshots/
.fdroid-submission
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.4.10
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source "https://rubygems.org"

gem "fastlane"
345 changes: 345 additions & 0 deletions Gemfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.fastlane.screengrab)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
Expand Down
95 changes: 95 additions & 0 deletions app/src/androidTest/java/com/ah/taplock/ScreenshotTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.ah.taplock

import android.content.Context
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performScrollTo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.ah.taplock.ui.theme.TapLockTheme
import org.junit.Before
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule

/**
* Drives [TapLockScreen] and captures store screenshots via fastlane screengrab.
* Run through the `fastlane screenshots` lane, not as part of the normal test suite —
* it produces images rather than assertions. The [LocaleTestRule] switches the device
* locale for each locale listed in Screengrabfile so descriptions render translated.
*/
@RunWith(AndroidJUnit4::class)
class ScreenshotTest {

@get:Rule
val composeTestRule = createComposeRule()

private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext

private fun setScreenContent() {
composeTestRule.setContent {
TapLockTheme {
TapLockScreen()
}
}
}

@Before
fun setup() {
// Seed a configured state so the widget and floating-button previews render richly:
// custom-looking icon shown, glass widget style, floating button enabled, and the
// lock-screen double-tap zone controls revealed. Onboarding/info are dismissed.
context.getSharedPreferences(
context.getString(R.string.shared_pref_name),
Context.MODE_PRIVATE
).edit().clear()
.putBoolean(context.getString(R.string.has_completed_onboarding), true)
.putBoolean(context.getString(R.string.has_seen_info), true)
.putBoolean(context.getString(R.string.show_widget_icon), true)
.putString(context.getString(R.string.widget_style), TapLockWidgetStyle.GLASS.name)
.putBoolean(context.getString(R.string.floating_button_enabled), true)
.putInt(context.getString(R.string.floating_button_size_dp), 72)
.putInt(context.getString(R.string.floating_button_opacity_percent), 90)
.putBoolean(context.getString(R.string.lock_screen_double_tap), true)
.commit()
}

@Test
fun captureScreenshots() {
setScreenContent()

// 1 — top of the settings screen (permissions + quick access).
composeTestRule.waitForIdle()
Screengrab.screenshot("01_settings")

// 2 — the live widget preview (custom icon + glass style) with the style chips above it.
composeTestRule.onNodeWithTag("widget_style_preview").performScrollTo()
composeTestRule.waitForIdle()
Screengrab.screenshot("02_widget_styles")

// 3 — Quick Settings tile setup.
composeTestRule.onNodeWithTag("button_quick_settings_tile").performScrollTo()
composeTestRule.waitForIdle()
Screengrab.screenshot("03_quick_tile")

// 4 — ambient triggers with the lock-screen zone controls revealed.
composeTestRule.onNodeWithTag("switch_lock_screen").performScrollTo()
composeTestRule.waitForIdle()
Screengrab.screenshot("04_ambient_triggers")

// 5 — behavior section (timeout, vibration, lock delay).
composeTestRule.onNodeWithTag("slider_timeout").performScrollTo()
composeTestRule.waitForIdle()
Screengrab.screenshot("05_behavior")
}

companion object {
@get:ClassRule
@JvmStatic
val localeTestRule = LocaleTestRule()
}
}
16 changes: 16 additions & 0 deletions app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Debug-only permissions required by fastlane screengrab to switch device
locales and write screenshots during automated capture. These are NOT part
of the release build (they live in the debug source set only).
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />

</manifest>
7 changes: 5 additions & 2 deletions app/src/main/java/com/ah/taplock/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,9 @@ fun TapLockScreen() {
style = widgetStyle,
showIcon = showIcon,
iconBitmap = widgetIconBitmap ?: defaultAppIconBitmap,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag("widget_style_preview")
)

if (showIcon) {
Expand Down Expand Up @@ -1055,7 +1057,8 @@ fun TapLockScreen() {
Button(
onClick = { requestQuickSettingsTilePrompt() },
enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
!isTileAdded
!isTileAdded,
modifier = Modifier.testTag("button_quick_settings_tile")
) {
Text(
if (isTileAdded) {
Expand Down
10 changes: 10 additions & 0 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
default_platform(:android)

platform :android do
desc "Build debug + test APKs and capture store screenshots (EN/ES) via screengrab"
lane :screenshots do
gradle(task: "assembleDebug")
gradle(task: "assembleDebugAndroidTest")
screengrab
end
end
25 changes: 25 additions & 0 deletions fastlane/Screengrabfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# fastlane screengrab configuration for TapLock.
# Run with: bundle exec fastlane screenshots
# (see fastlane/Fastfile). Requires a running emulator/device.

app_package_name('com.ah.taplock')

# Only run the dedicated screenshot test class, not the whole instrumented suite.
use_tests_in_classes(['com.ah.taplock.ScreenshotTest'])
test_instrumentation_runner('androidx.test.runner.AndroidJUnitRunner')

# Capture these locales. Must match fastlane metadata folder locales.
locales(['en-US', 'es-ES'])

# Clear previously generated screenshots before each run.
clear_previous_screenshots(true)

# Output into the fastlane metadata tree so F-Droid / stores pick them up.
output_directory('fastlane/metadata/android')

# Reinstall the app + test APKs each run.
reinstall_app(true)

# Explicit APK paths (built by the `screenshots` lane / assembleDebug tasks).
app_apk_path('app/build/outputs/apk/debug/app-debug.apk')
tests_apk_path('app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk')
3 changes: 3 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/17.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Detect Android Advanced Protection and clearly explain that it blocks TapLock's accessibility service
* Spanish language support
* Dependency and build updates
20 changes: 20 additions & 0 deletions fastlane/metadata/android/en-US/full_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
TapLock is a minimalist utility that lets you lock your screen with a double tap — perfect for one-handed use and quick screen locking without reaching for the power button.

Create a resizable, transparent home screen widget and double-tap it to lock your phone instantly. TapLock also offers several other ways to lock:

* Home screen widget (resizable, with optional icon and multiple styles)
* Quick Settings tile
* Draggable floating button
* Double-tap the status bar
* Double-tap the lock screen, edges, or corners

Additional features:

* Adjustable double-tap timeout and lock delay
* Optional vibration feedback with light, medium, and strong patterns
* Per-app exclusions so taps are ignored in apps you choose
* Available in English and Spanish

TapLock uses Android's accessibility service to power off the screen. This permission is required for the app to function, and TapLock does not collect any personal information or track any other interaction you have with your device. The full source code is available for transparency.

Note: On devices with Android's Advanced Protection enabled, the system blocks all third-party accessibility services, so TapLock cannot lock the screen while Advanced Protection is turned on.
Binary file added fastlane/metadata/android/en-US/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/short_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Lock your screen instantly with a double tap.
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/title.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TapLock
3 changes: 3 additions & 0 deletions fastlane/metadata/android/es-ES/changelogs/17.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Detecta la Protección avanzada de Android y explica claramente que bloquea el servicio de accesibilidad de TapLock
* Compatibilidad con el idioma español
* Actualizaciones de dependencias y de compilación
20 changes: 20 additions & 0 deletions fastlane/metadata/android/es-ES/full_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
TapLock es una utilidad minimalista que te permite bloquear la pantalla con un doble toque, ideal para el uso con una sola mano y para bloquear la pantalla rápidamente sin buscar el botón de encendido.

Crea un widget transparente y redimensionable para la pantalla de inicio y tócalo dos veces para bloquear el teléfono al instante. TapLock también ofrece otras formas de bloquear:

* Widget de pantalla de inicio (redimensionable, con icono opcional y varios estilos)
* Mosaico de Ajustes rápidos
* Botón flotante arrastrable
* Doble toque en la barra de estado
* Doble toque en la pantalla de bloqueo, los bordes o las esquinas

Funciones adicionales:

* Tiempo límite de doble toque y retardo de bloqueo ajustables
* Vibración opcional con patrones ligero, medio y fuerte
* Exclusiones por app para ignorar los toques en las apps que elijas
* Disponible en inglés y español

TapLock usa el servicio de accesibilidad de Android para apagar la pantalla. Este permiso es necesario para que la app funcione, y TapLock no recopila ninguna información personal ni registra ninguna otra interacción que tengas con el dispositivo. El código fuente completo está disponible para mayor transparencia.

Nota: en dispositivos con la Protección avanzada de Android activada, el sistema bloquea todos los servicios de accesibilidad de terceros, por lo que TapLock no puede bloquear la pantalla mientras la Protección avanzada esté activada.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions fastlane/metadata/android/es-ES/short_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bloquea la pantalla al instante con un doble toque.
1 change: 1 addition & 0 deletions fastlane/metadata/android/es-ES/title.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TapLock
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ coreKtx = "1.19.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
screengrab = "2.1.1"
lifecycleRuntimeKtx = "2.11.0"
activityCompose = "1.13.0"
composeBom = "2026.06.00"
Expand All @@ -14,6 +15,7 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
fastlane-screengrab = { group = "tools.fastlane", name = "screengrab", version.ref = "screengrab" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
Expand Down
92 changes: 92 additions & 0 deletions scripts/capture-home-widget.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
#
# Capture a "hero" screenshot of the TapLock widget on the launcher home screen.
#
# This is a MANUAL one-off helper, not part of the deterministic `fastlane screenshots`
# lane. It drives the real launcher's widget-pin flow via adb + UI Automator, which is
# inherently launcher- and timing-dependent (it matches button text and the Pixel/Nexus
# launcher pin dialog). Expect to eyeball the result and occasionally re-run.
#
# Prerequisites:
# - An emulator/device already booted and visible to `adb devices`.
# - The debug APK installed (e.g. run `./gradlew installDebug` or the screenshots lane first).
#
# Usage:
# ANDROID_HOME=~/Library/Android/sdk ./scripts/capture-home-widget.sh [output.png]
#
set -euo pipefail

PKG="com.ah.taplock"
OUT="${1:-fastlane/metadata/android/en-US/images/phoneScreenshots/00_home_widget.png}"
# In-app button label differs first-time vs. when a widget already exists.
ADD_WIDGET_LABEL="Add Widget to Home Screen"
ADD_ANOTHER_LABEL="Add Another Widget"
PIN_CONFIRM_LABEL="Add to home screen" # launcher pin-dialog confirm button

ADB="${ANDROID_HOME:-$HOME/Library/Android/sdk}/platform-tools/adb"
command -v "$ADB" >/dev/null 2>&1 || ADB="adb"

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

die() { echo "error: $*" >&2; exit 1; }

"$ADB" get-state >/dev/null 2>&1 || die "no device/emulator connected (check 'adb devices')"
"$ADB" shell pm path "$PKG" >/dev/null 2>&1 || die "$PKG not installed; build/install the debug APK first"

# Center coordinates of a uiautomator node whose text matches $1, from the dumped XML in $2.
tap_text() {
local label="$2" xml="$3"
local bounds
bounds="$("$ADB" pull "$xml" "$TMP/ui.xml" >/dev/null 2>&1; \
grep -oE "text=\"$label\"[^>]*bounds=\"\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]\"" "$TMP/ui.xml" \
| grep -oE '[0-9]+' | tr '\n' ' ' || true)"
[ -n "$bounds" ] || return 1
# shellcheck disable=SC2086
set -- $bounds
local cx=$(( ($1 + $3) / 2 )) cy=$(( ($2 + $4) / 2 ))
echo " tapping '$label' at $cx,$cy"
"$ADB" shell input tap "$cx" "$cy"
}

echo "==> Allowing $PKG to bind app widgets"
"$ADB" shell appwidget grantbind --package "$PKG" --user current >/dev/null 2>&1 || true

# Dim the wallpaper to a clean solid dark backdrop. Besides looking tidier, this works
# around an emulator GPU bug that smears the gradient wallpaper on some home pages.
# Restored on exit.
echo "==> Dimming wallpaper for a clean backdrop"
"$ADB" shell cmd wallpaper set-dim-amount 1.0 >/dev/null 2>&1 || true
trap '"$ADB" shell cmd wallpaper set-dim-amount 0.0 >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT

echo "==> Launching app (fresh, scrolled to top)"
"$ADB" shell am force-stop "$PKG" >/dev/null 2>&1 || true
sleep 1
"$ADB" shell am start -n "$PKG/.MainActivity" >/dev/null
sleep 3

echo "==> Locating the in-app add-widget button"
"$ADB" shell uiautomator dump /sdcard/ui.xml >/dev/null 2>&1
tap_text tap "$ADD_WIDGET_LABEL" /sdcard/ui.xml \
|| tap_text tap "$ADD_ANOTHER_LABEL" /sdcard/ui.xml \
|| die "could not find the add-widget button ('$ADD_WIDGET_LABEL' / '$ADD_ANOTHER_LABEL') — did the label change?"
sleep 3

echo "==> Confirming the launcher pin dialog"
"$ADB" shell uiautomator dump /sdcard/ui2.xml >/dev/null 2>&1
tap_text tap "$PIN_CONFIRM_LABEL" /sdcard/ui2.xml \
|| die "could not find '$PIN_CONFIRM_LABEL' pin-dialog button"
sleep 2

echo "==> Going to the home screen and repainting"
# Restart the launcher so it repaints cleanly with the dimmed wallpaper before capture.
"$ADB" shell input keyevent KEYCODE_HOME; sleep 1
"$ADB" shell am force-stop com.google.android.apps.nexuslauncher >/dev/null 2>&1 || true
sleep 2
"$ADB" shell input keyevent KEYCODE_HOME; sleep 4

echo "==> Capturing $OUT"
mkdir -p "$(dirname "$OUT")"
"$ADB" exec-out screencap -p > "$OUT"
echo "Done: $OUT ($(wc -c < "$OUT" | tr -d ' ') bytes)"
echo "Review the image — the widget should appear top-left on the home screen."
Loading