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: 0 additions & 22 deletions .github/workflows/build_and_local_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,3 @@ jobs:
with:
name: build-reports
path: ./app/build/reports

- name: Clean before running customizer
run: git clean -fx .

- name: Run customizer script
run: bash customizer.sh com.android.blah MyNewModel MyNewApplication

- name: "Check that customizer ran correctly"
uses: andstor/file-existence-action@v3
with:
files: "app/src/main/java/com/android/blah/MyNewApplication.kt"
fail: false

- name: "Check that customizer removed all unnecessary files"
id: customizer_rm
uses: andstor/file-existence-action@v3
with:
files: ".git/config"
fail: false
- name: "Fail if unnecessary files were not deleted"
if: steps.customizer_rm.outputs.files_exists == 'true'
run: exit 1
1 change: 1 addition & 0 deletions .github/workflows/instrumented_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
with:
java-version: 17
distribution: 'zulu'
cache: gradle

- name: Run instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@
.externalNativeBuild
.cxx
local.properties

# Signing artifacts and local release secrets
*.keystore
*.jks
app/release.keystore
signing.properties
keystore.properties
app/signing.properties
218 changes: 193 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,209 @@
Architecture starter template (single module)
==================
# FocusBlocker

This template is compatible with the latest **stable** version of Android Studio.
FocusBlocker is an Android productivity application that enforces app-usage limits using a foreground monitoring service, policy-based blocking rules, and a dedicated block screen experience.

## Screenshots
![Screenshot](https://github.com/android/architecture-templates/raw/main/screenshots.png)
It is built with modern Android tooling and a security-first baseline suitable for personal hardening and extension into a production deployment pipeline.

## Features
## Table of Contents

* Room Database
* Hilt
* ViewModel, read+write
* UI in Compose, list + write (Material3)
* Navigation
* Repository and data source
* Kotlin Coroutines and Flow
* Unit tests
* UI tests using fake data with Hilt
1. [Overview](#overview)
2. [Core Capabilities](#core-capabilities)
3. [Tech Stack](#tech-stack)
4. [Architecture](#architecture)
5. [Permission and Runtime Model](#permission-and-runtime-model)
6. [Security Model](#security-model)
7. [Getting Started](#getting-started)
8. [Build and Release](#build-and-release)
9. [Testing](#testing)
10. [Project Conventions](#project-conventions)
11. [Contributing](#contributing)
12. [License](#license)

## Usage
## Overview

1. Clone this branch
FocusBlocker tracks foreground app usage and applies configurable daily quotas per package. Once quota is exhausted, the app presents a full-screen block activity and keeps monitoring in the background through a foreground service.

The app is designed around:

- Deterministic quota tracking by logical day.
- Continuous monitoring with restart resilience (boot/package-updated receiver).
- Secure preference storage for sensitive settings.
- A Compose-first UI with permission gateway and focused workflows.

## Core Capabilities

- Foreground app monitoring via `UsageStatsManager` in `AppMonitorService`.
- App-level policy persistence with Room (`AppPolicy`, `Task`, DAOs, and `AppDatabase`).
- Quota accumulation and automatic daily reset based on a configurable day-start hour.
- Full-screen blocking UX using `BlockScreenActivity` when a policy limit is reached.
- Boot and package-replaced recovery through `BootReceiver`.
- Permission gateway flow for usage access, overlay permission, and battery optimization exemption.
- Encrypted sensitive preferences through `EncryptedSharedPreferences`.

## Tech Stack

- Language: Kotlin
- UI: Jetpack Compose + Material 3
- Architecture primitives: ViewModel, Navigation Compose, Kotlin Coroutines/Flow
- Dependency Injection: Hilt
- Local persistence: Room + KSP
- Security: Android Keystore + AndroidX Security Crypto
- Build system: Gradle (Kotlin DSL)

Current project baselines:

- Android Gradle Plugin: `9.0.1`
- Kotlin: `2.3.10`
- Compile SDK: `36`
- Target SDK: `35`
- Min SDK: `26`

## Architecture

The project currently uses a single app module (`:app`) with clean package boundaries.

```text
app/src/main/java/com/focusblocker/app/
data/
local/
dao/
entity/
AppDatabase.kt
di/
security/
service/
ui/
screens/
block/
theme/
```
git clone https://github.com/android/architecture-templates.git --branch base

High-level flow:

1. User grants required special permissions in the permission gateway.
2. `AppMonitorService` starts as foreground service.
3. Service polls recent usage events and resolves current foreground package.
4. Policy and quota state are read/updated in Room.
5. If quota is exhausted, `BlockScreenActivity` is launched.

## Permission and Runtime Model

FocusBlocker requires the following permissions/special access for enforcement:

- `PACKAGE_USAGE_STATS` for foreground usage events.
- `SYSTEM_ALERT_WINDOW` for overlay/blocking UX compatibility.
- `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` to reduce service kill risk.
- `RECEIVE_BOOT_COMPLETED` for automatic restart after reboot.
- Foreground service permissions for persistent monitoring.

At runtime, the app gates entry through a dedicated permission screen and starts the monitor service only after required access is granted.

## Security Model

Sensitive state is stored in encrypted preferences:

- Backend: `EncryptedSharedPreferences`
- Key management: Android Keystore (`MasterKey` AES-256-GCM)
- Preference file: `focusblocker_secure_prefs`

Additional anti-tamper logic exists in `SecurityManager`:

- Logical-day computation using configurable day-start hour.
- Forward clock-drift detection using `SystemClock.elapsedRealtime()` baseline.

## Getting Started

### Prerequisites

- Android Studio (latest stable recommended)
- JDK 17+
- Android SDK with platform level 35+ installed

### Clone and Open

```bash
git clone <your-fork-or-repo-url>
cd FocusBlocker
```

2. Run the customizer script:
Open the project in Android Studio and allow Gradle sync to complete.

### Build Debug APK

```bash
./gradlew :app:assembleDebug
```
./customizer.sh your.package.name DataItemType [MyApplication]

### Install on Connected Device

```bash
./gradlew :app:installDebug
```

## Build and Release

### Release Signing Configuration

Release signing values are read from Gradle properties or environment variables:

- `RELEASE_STORE_FILE`
- `RELEASE_STORE_PASSWORD`
- `RELEASE_KEY_ALIAS`
- `RELEASE_KEY_PASSWORD`

You can start from `signing.properties.template` and place values in user-level Gradle properties or exported environment variables.

### Build Release APK

```bash
./gradlew :app:assembleRelease
```

If release signing values are missing, the build fails fast with explicit missing keys.

## Testing

### Unit Tests

```bash
./gradlew :app:testDebugUnitTest
```

Where `your.package.name` is your app ID (should be lowercase) and `DataItemType` is used for the
name of the screen, exposed state and data base entity (should be PascalCase). You can add an optional application name.
### Instrumented Tests

```bash
./gradlew :app:connectedDebugAndroidTest
```

### Full Verification Pass

```bash
./gradlew :app:lint :app:testDebugUnitTest
```

## Project Conventions

- Base package must remain `com.focusblocker.app`.
- Keep Room schemas exported under `app/schemas/`.
- Use Hilt for dependency wiring in app scope and Android components.
- Prefer immutable UI state and coroutine-based asynchronous flows.
- Keep release-signing secrets out of version control.

## Contributing

Contributions are welcome. Please review [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.

Recommended pull request quality bar:

- Focused, single-purpose changes.
- Updated tests for behavioral changes.
- No debug logging or temporary scaffolding left in final diff.
- Clear migration notes for permission, schema, or signing behavior changes.

## License

This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE).

# License
## Acknowledgments

Now in Android is distributed under the terms of the Apache License (Version 2.0). See the
[license](LICENSE) for more information.
This repository originated from Android architecture template foundations and has been adapted into the FocusBlocker application domain.
50 changes: 50 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,50 @@ android {
namespace = "com.focusblocker.app"
compileSdk = 36

val releaseStoreFilePathProvider = providers.gradleProperty("RELEASE_STORE_FILE")
.orElse(providers.environmentVariable("RELEASE_STORE_FILE"))
val releaseStorePasswordProvider = providers.gradleProperty("RELEASE_STORE_PASSWORD")
.orElse(providers.environmentVariable("RELEASE_STORE_PASSWORD"))
val releaseKeyAliasProvider = providers.gradleProperty("RELEASE_KEY_ALIAS")
.orElse(providers.environmentVariable("RELEASE_KEY_ALIAS"))
val releaseKeyPasswordProvider = providers.gradleProperty("RELEASE_KEY_PASSWORD")
.orElse(providers.environmentVariable("RELEASE_KEY_PASSWORD"))

val isReleaseBuildRequested = gradle.startParameter.taskNames.any {
it.contains("release", ignoreCase = true)
}

signingConfigs {
create("release") {
val storeFilePath = releaseStoreFilePathProvider.orNull
val storePasswordValue = releaseStorePasswordProvider.orNull
val keyAliasValue = releaseKeyAliasProvider.orNull
val keyPasswordValue = releaseKeyPasswordProvider.orNull

if (!storeFilePath.isNullOrBlank()) {
storeFile = rootProject.file(storeFilePath)
}
storePassword = storePasswordValue
keyAlias = keyAliasValue
keyPassword = keyPasswordValue

if (isReleaseBuildRequested) {
val missing = mutableListOf<String>()
if (storeFilePath.isNullOrBlank()) missing += "RELEASE_STORE_FILE"
if (storePasswordValue.isNullOrBlank()) missing += "RELEASE_STORE_PASSWORD"
if (keyAliasValue.isNullOrBlank()) missing += "RELEASE_KEY_ALIAS"
if (keyPasswordValue.isNullOrBlank()) missing += "RELEASE_KEY_PASSWORD"

if (missing.isNotEmpty()) {
throw GradleException(
"Missing release signing values: ${missing.joinToString()}. " +
"Provide them via Gradle properties or environment variables.",
)
}
}
}
}

defaultConfig {
applicationId = "com.focusblocker.app"
minSdk = 26 // API 26+ recommended: covers ~97% of devices + guarantees JobScheduler
Expand All @@ -43,6 +87,7 @@ android {

buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true // Enable R8 shrinking for release
isShrinkResources = true
proguardFiles(
Expand All @@ -59,6 +104,7 @@ android {

buildFeatures {
compose = true
buildConfig = true
}
}

Expand All @@ -70,6 +116,7 @@ dependencies {

// Core Android dependencies
implementation(libs.androidx.core.ktx)
implementation("androidx.core:core-splashscreen:1.0.1")
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)

Expand All @@ -92,6 +139,7 @@ dependencies {
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
// Tooling
debugImplementation(libs.androidx.compose.ui.tooling)
// Instrumented tests
Expand All @@ -112,6 +160,8 @@ dependencies {
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)

// ════════════════════════════════════════
// ROOM DATABASE
Expand Down
Loading
Loading