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
22 changes: 22 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Android CI

on:
pull_request:
push:
branches: [master]

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- uses: gradle/actions/setup-gradle@v4
- name: Test, lint, and assemble
run: ./gradlew :app:testDebugUnitTest :app:lintRelease :app:assembleDebug --console=plain
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
.externalNativeBuild
.cxx
local.properties
release-signing.properties
13 changes: 13 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Privacy Policy

Effective August 13, 2026

Screen Timeout Tile does not collect, store, share, or sell personal data.

The app has no internet permission, accounts, advertising, analytics, or tracking. It stores no user content and disables Android backup and device-transfer backup.

Android requires the **Modify system settings** permission before the app can read or change `Settings.System.SCREEN_OFF_TIMEOUT`. Screen Timeout Tile uses that access only to read the current screen timeout and switch it between 30 seconds and 10 minutes. The app does not use the permission to change any other system setting.

You can revoke this access at any time from Android Settings. You can also remove the Quick Settings tile or uninstall the app at any time.

Questions can be opened as an issue in the project’s public GitHub repository.
73 changes: 10 additions & 63 deletions PROJECT_GOAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,72 +51,19 @@ A minimal Android app that adds a Quick Settings tile. Tapping the tile toggles
---

## Manifest
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission
android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.ScreenTimeoutTile">

<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".TimeoutTileService"
android:label="Screen Timeout"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>

</application>
</manifest>
```
The production manifest is the source of truth. It declares only the protected
`WRITE_SETTINGS` permission, the exported launcher activity, and the exported
tile service guarded by `BIND_QUICK_SETTINGS_TILE`. App backup is disabled
because the app stores no user data.

---

## Example Implementation
```kotlin
@RequiresApi(Build.VERSION_CODES.N)
class TimeoutTileService : TileService() {

override fun onClick() {
if (!Settings.System.canWrite(this)) {
val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply {
data = Uri.parse("package:$packageName")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)
return
}

val current = Settings.System.getInt(
contentResolver,
Settings.System.SCREEN_OFF_TIMEOUT,
30000
)

val newTimeout = if (current > 30000) 30000 else 600000 // 30 s vs 10 min
Settings.System.putInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, newTimeout)

qsTile.state = if (newTimeout > 30000) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
```
## Implementation
`TimeoutTileService` owns the Android tile lifecycle, while
`ScreenTimeoutController`, `ScreenTimeoutSettingsStore`, and
`TimeoutTogglePolicy` keep settings access and toggle behavior independently
testable. Refer to the production Kotlin sources instead of duplicating an
implementation sample here.

---

Expand Down
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Screen Timeout Tile is a small Android app that adds a Quick Settings tile for s

It is built for the common Android workflow where you want the screen to stay awake while reading, debugging, cooking, presenting, or using your phone as a reference display, but you still want a quick way back to a short timeout afterward.

## Download

Download the signed APK from the [latest GitHub release](https://github.com/StoneHub/screen-timeout-tile/releases/latest). Android 7.0 or newer is required.

Because this app is distributed outside Google Play, Android may ask you to allow installs from the browser or file manager you used to download it. That permission can be turned off again immediately after installation.

## What It Does

- Adds a **Screen Timeout Tile** to Android Quick Settings.
Expand All @@ -13,6 +19,15 @@ It is built for the common Android workflow where you want the screen to stay aw
- Includes a setup screen for permission status, current timeout, and tile placement.
- Supports Android 13+ direct tile placement through the system prompt.
- Keeps Android 7-12 manual Quick Settings edit instructions available.
- Explains the hourglass, clock/bolt, and permission icons in the setup screen.

## What The Tile Means

- **Hourglass, highlighted:** 10-minute timeout.
- **Clock with bolt, dim:** 30-second timeout.
- **Lock:** Modify system settings permission is still needed.

The tile is never “off.” Each tap switches between the two timeout presets.

## Why This Exists

Expand All @@ -21,10 +36,10 @@ Android has the screen timeout setting buried in system settings. This project m
## Requirements

- Android 7.0 or newer, API 24+
- Android Studio Giraffe or newer recommended
- Android Studio Narwhal 4 Feature Drop or newer recommended
- Android Gradle Plugin 8.13.0
- Kotlin 2.0.21
- Java 11 toolchain
- JDK 17

## Install A Debug Build

Expand All @@ -36,9 +51,11 @@ Android has the screen timeout setting buried in system settings. This project m
If your shell cannot find the Android SDK, set the SDK path for the command:

```bash
ANDROID_HOME=/Users/monroe/Library/Android/sdk ANDROID_SDK_ROOT=/Users/monroe/Library/Android/sdk ./gradlew installDebug
ANDROID_HOME="$HOME/Library/Android/sdk" ANDROID_SDK_ROOT="$HOME/Library/Android/sdk" ./gradlew installDebug
```

Debug builds use the isolated package `com.stonecode.screentimeouttile.debug`, so a dogfood install cannot block a later production-signed update.

## Setup On A Device

1. Launch **Screen Timeout Tile**.
Expand All @@ -61,6 +78,8 @@ That permission is required because Android protects writes to `Settings.System.

This app does not declare internet access, does not create accounts, does not run analytics, and does not collect personal data.

See the full [privacy policy](PRIVACY.md).

## Development

Run focused JVM tests:
Expand Down Expand Up @@ -88,6 +107,8 @@ Before publishing a release build, also run:
./gradlew :app:bundleRelease
```

Release signing can use either the ignored `release-signing.properties` file or Keychain-backed environment variables. See [release signing](docs/release-signing.md). Without signing inputs, Gradle intentionally produces an unsigned release artifact for validation only.

## Project Structure

```text
Expand All @@ -111,15 +132,15 @@ app/src/androidTest/java/ Instrumentation smoke tests

## Current Status

The core tile behavior, setup screen, Android 13+ tile request flow, and JVM tests are in place. Release packaging and Play Store publishing are still in progress.
Version 1.1.0 is the first signed GitHub release. Play Store publishing remains a separate future distribution path.

## Contributing

Issues and pull requests are welcome. Useful contributions include:

- Testing tile behavior on different Android versions and OEM skins.
- Improving accessibility and large-font layout behavior.
- Adding release engineering for signed builds.
- Testing signed releases across additional Android devices and OEM skins.
- Tightening the privacy policy and store listing docs.

For code changes, keep behavior covered by JVM tests where possible and use instrumentation tests for device-specific setup or tile-placement flows.
Expand Down
72 changes: 66 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}

val releaseSigningPropertiesFile = rootProject.file("release-signing.properties")
val releaseSigningProperties = Properties().apply {
if (releaseSigningPropertiesFile.isFile) {
releaseSigningPropertiesFile.inputStream().use(::load)
}
}

fun releaseSigningValue(propertyName: String, environmentName: String): String? =
releaseSigningProperties.getProperty(propertyName)?.takeIf(String::isNotBlank)
?: providers.environmentVariable(environmentName).orNull?.takeIf(String::isNotBlank)

val releaseStoreFile = releaseSigningValue(
propertyName = "storeFile",
environmentName = "SCREEN_TIMEOUT_RELEASE_STORE_FILE",
)
val releaseStorePassword = releaseSigningValue(
propertyName = "storePassword",
environmentName = "SCREEN_TIMEOUT_RELEASE_STORE_PASSWORD",
)
val releaseKeyAlias = releaseSigningValue(
propertyName = "keyAlias",
environmentName = "SCREEN_TIMEOUT_RELEASE_KEY_ALIAS",
)
val releaseKeyPassword = releaseSigningValue(
propertyName = "keyPassword",
environmentName = "SCREEN_TIMEOUT_RELEASE_KEY_PASSWORD",
)
val releaseSigningValues = listOf(
releaseStoreFile,
releaseStorePassword,
releaseKeyAlias,
releaseKeyPassword,
)
val hasReleaseSigning = releaseSigningValues.all { it != null }

check(releaseSigningValues.all { it == null } || hasReleaseSigning) {
"Release signing is partially configured. Provide all four signing values."
}

android {
namespace = "com.stonecode.screentimeouttile"
compileSdk = 36
Expand All @@ -11,27 +52,46 @@ android {
applicationId = "com.stonecode.screentimeouttile"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionCode = 2
versionName = "1.1.0"
manifestPlaceholders["appLabel"] = "Screen Timeout Tile"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

signingConfigs {
if (hasReleaseSigning) {
create("release") {
storeFile = rootProject.file(requireNotNull(releaseStoreFile))
storePassword = requireNotNull(releaseStorePassword)
keyAlias = requireNotNull(releaseKeyAlias)
keyPassword = requireNotNull(releaseKeyPassword)
}
}
}

buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
manifestPlaceholders["appLabel"] = "Screen Timeout Tile (Debug)"
}
release {
isMinifyEnabled = false
isMinifyEnabled = true
isShrinkResources = true
signingConfig = signingConfigs.findByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "17"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class MainActivityInstrumentedTest {
fun shouldShowPermissionAndTileCtas_whenActivityLaunches() {
ActivityScenario.launch(MainActivity::class.java).use {
onView(withId(R.id.permissionButton)).check(matches(isDisplayed()))
onView(withId(R.id.permissionRationale)).check(matches(isDisplayed()))
onView(withId(R.id.tileButton)).check(matches(isDisplayed()))
onView(withId(R.id.nextAction)).check(matches(isDisplayed()))
}
Expand Down
13 changes: 10 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
tools:ignore="ProtectedPermissions" />

<application
android:allowBackup="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:label="${appLabel}"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ScreenTimeoutTile">
Expand All @@ -28,12 +28,19 @@

<service
android:name=".TimeoutTileService"
android:icon="@drawable/ic_qs_timeout_long"
android:exported="true"
android:label="@string/app_name"
android:label="@string/tile_label"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TILE_CATEGORY"
android:value="android.service.quicksettings.CATEGORY_DISPLAY" />
</service>

</application>
Expand Down
Loading
Loading