Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cbb6339
chore(ui): Hide the pause action in the service notification for now
xLexip Dec 7, 2025
9d6e56d
fix: Always restart the service onResume when adaptive theme is enabled
xLexip Dec 7, 2025
5b8ebf5
chore: Implement flexible update prompts and easy prompt intrusiveness
xLexip Dec 8, 2025
916c0b7
feat(ui): Show the live lux measurement when the service is enabled
xLexip Dec 8, 2025
36b57e1
refactor(ui): Outsource ForExpertsSection into own file
xLexip Dec 8, 2025
76adf6b
feat(setup): Implement Shizuku support
xLexip Dec 8, 2025
2a19718
refactor(ui): Optimize setup modularization
xLexip Dec 8, 2025
9c8aed1
feat(setup): Implemented root support
xLexip Dec 9, 2025
b1ad3b6
refactor(setup): Optimize the look of the setup expert card
xLexip Dec 9, 2025
0efc6e4
feat(setup): Add shake animation for the setup required card
xLexip Dec 9, 2025
c6881c7
feat(setup): Show USB pending card in step 3 if not connected
xLexip Dec 9, 2025
5d6783b
feat(setup): Show expert section in step 2 as well
xLexip Dec 9, 2025
e8e5aea
feat(setup): Collapse FAQ cards by default to save space
xLexip Dec 9, 2025
759fbcf
refactor(setup): Minor code optimizations and lint fixes
xLexip Dec 9, 2025
ee79850
refactor(util): Reduce cognitive complexity of the Shizuku manager
xLexip Dec 9, 2025
da4d9ce
fix(res): Hardcode "Star on GitHub" string
xLexip Dec 9, 2025
074a69f
refactor(ui): Address sonar findings
xLexip Dec 9, 2025
fd9cf7a
chore: Tweak min. screen height for top bar collapsing
xLexip Dec 9, 2025
b0b90eb
style: Increase three-dot menu color contrast and round corners
xLexip Dec 9, 2025
1eb3724
refactor(ui): Change "Star on GitHub" to "Support the project"
xLexip Dec 9, 2025
5833af4
fix: Add proguard rules for the Shizuku implementation
xLexip Dec 9, 2025
304b925
chore(analytics): Implement Shizuku logs
xLexip Dec 9, 2025
9e57c9a
docs: Update README
xLexip Dec 9, 2025
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
144 changes: 123 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,144 @@
[![GooglePlay](https://upload.wikimedia.org/wikipedia/commons/7/78/Google_Play_Store_badge_EN.svg)](https://lexip.dev/hecate/play)
# Adaptive Theme: Smart Dark Mode

![feature_graphic](https://i.ibb.co/G38P9b9V/adaptive-theme.jpg)
Adaptive Theme intelligently automates your device's theme settings, switching between **Light and
Dark mode** based on your environment's **ambient light** — not just the time of day.

## Adaptive Theme
Get the readability of Light mode in bright daylight and the eye-comfort of Dark mode in low light.
This allows for a true auto dark mode experience that native Android doesn't offer.

Adaptive Theme intelligently switches your device between Light and Dark mode based on your
environment.
## Closed Beta

Get the readability of Light mode in bright daylight and the comfort of Dark mode in low light —
going easy on your eyes and your battery.
Join this [Google Group](https://groups.google.com/g/apphive-testers) before clicking on
this [Google Play](https://play.google.com/apps/testing/dev.lexip.hecate) link to install the beta.


---

## 📋 Table of Contents

- [💡 Why use Adaptive Theme?](#-why-use-adaptive-theme)
- [✨ Key Highlights](#-key-highlights)
- [🛠️ One-Time Setup](#%EF%B8%8F-one-time-setup)
- [⚙️ How it Works](#%EF%B8%8F-how-it-works)
- [✅ Safety](#-safety)
- [❓ FAQ](#-faq)
- [❤️ Support the Project](#%EF%B8%8F-support-the-project)
- [📱 Screenshots](#-screenshots)

---

## 💡 Why use Adaptive Theme?

Most Android phones only switch themes at sunset or based on a fixed schedule. Adaptive Theme uses
your **light sensor** to switch intelligently, optimizing both **eye comfort** and **battery life**.

## ✨ Key Highlights

* 🌤️ **Smart Ambient Detection:** Uses your device's physical light sensor to toggle the system
theme.
* ⚙️ **Full Customization:** Set your specific lux threshold (brightness level) and use the Quick
Settings tile to quickly pause/resume the service.
* 🚀 **Modern & Native:** Built with **Jetpack Compose** and **Material You** for a smooth,
crash-free experience.
* 🔋 **Battery Friendly:** The app is passive. It only checks the sensor when you turn the screen
on — zero battery drain in the background.
* 🔒 **Privacy First:** Open Source, completely free, and no ads at all.
* 🗝️ **No Root Required:** Root access is not required (but is supported as an alternative setup
method).
* 🐱 **Optional Shizuku Support:** One of multiple setup options is
using [Shizuku](https://github.com/RikkaApps/Shizuku).

---

### Highlights
## 🛠️ One-Time Setup

Android restricts apps from changing system themes by default. To unlock this feature, a specific
permission (`WRITE_SECURE_SETTINGS`) is needed. After installing the app, you can choose any of the
following methods:

#### Method 1: Web Tool (Recommended)

Use our browser-based setup tool on a secondary device (Computer, Tablet, or Phone). No code or ADB
installation required (WebADB).
👉 **[lexip.dev/setup](https://lexip.dev/setup)**

#### Method 2: Shizuku (No PC)

🌤️ **Smart Detection**: Uses your ambient light sensor to switch themes automatically.
If you have **Shizuku** installed and configured (via Wireless Debugging or Root), you can grant the
permission directly within the Adaptive Theme app.

⚙️ **Full Control**: Fully customizable brightness threshold and a Quick Settings tile to
pause/resume the service.
#### Method 3: Root

🔒 **Free & Open**: Free to use, no ads and open source.
If your device is rooted, you can grant the permission with one click inside the app.

🚀 **Native Design**: Modern architecture, built with Jetpack Compose and Material You for a seamless
Android experience.
#### Method 4: Manual ADB

🚫 **No Flickering**: The theme only changes when you turn on screen and the device is uncovered.
If you have ADB installed on your computer, you can run the ADB grant command manually via your
terminal.

---

### One-Time Setup
## ⚙️ How it Works

**Why didn't the theme change immediately?**

To prevent unnecessary battery drain and screen flickering, Adaptive Theme obeys the following
rules:

1. It checks the light sensor only **immediately after the screen turns on**.
2. It verifies that the light sensor is **not covered**.
3. It switches the theme **instantly** before you start interacting with the UI.

---

## ✅ Safety

The required permission does **not** grant root access or read any user data. It only allows the app
to change settings such as "Dark Mode" in the system settings. This is absolutely safe and
completely reversible by uninstalling the app.

---

## ❓ FAQ

**1. Does this require Root?**
No. It works on stock devices. However, if you have Root, it can optionally be used to set up the
service faster.

**2. Does it work with custom skins (MIUI, OneUI)?**
In most cases, yes. It works with any system that respects the native Android Dark Mode
implementation.

**Support & Feedback:** If Adaptive Theme not work for you or if you have any questions, please
create an Issue or send feedback via the app.

---

## ❤️ Support the Project

Adaptive Theme is **completely free**, **ad-free**, **open source**, and developed in my free time.

If you enjoy using the app, there are three simple ways you can support the project:

⭐ **Star on GitHub:** Give this repository a star to help others find it.

🌟 **Rate on Google Play:**
A [5-star rating](https://play.google.com/store/apps/details?id=dev.lexip.hecate)
is the best way to boost the ranking.

☕ **Buy me a Coffee:** If you are feeling generous, you can
also [buy me a coffee](https://buymeacoffee.com/lexip).

📣 **Spread the Word:** Share the app to help the project grow.

---

To toggle the system theme, Android requires the permission `WRITE_SECURE_SETTINGS`. This is safe,
transparent and fully reversible. The app will guide you through the setup process.
**🇩🇪 Made in Germany** – Engineered with precision (and 🥨 🍺).

---

That’s it! Set your preference, and never worry about your light/dark mode again.
## 📱 Screenshots

🇩🇪 Made with 🥨 🍺 in Germany.
[![Adaptive Theme Screenshot](https://i.ibb.co/6cngXDnx/Adaptive-Theme-Screenshot.webp)](https://ibb.co/gbjz4tjp)

[![SonarCloud](https://sonarcloud.io/api/project_badges/quality_gate?project=xLexip_Hecate)](https://sonarcloud.io/summary/new_code?id=xLexip_Hecate)
#### [More Screenshots](https://play.google.com/store/apps/details?id=dev.lexip.hecate)
6 changes: 4 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ android {
applicationId = "dev.lexip.hecate"
minSdk = 31
targetSdk = 36
versionCode = 36
versionName = "0.7.0"
versionCode = 46
versionName = "0.9.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down Expand Up @@ -100,6 +100,8 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.app.update.ktx)
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
10 changes: 9 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile

# --- Shizuku integration ---
# Keep all Shizuku library classes used for binder communication
-keep class moe.shizuku.** { *; }
-keep class rikka.shizuku.** { *; }

# Keep Shizuku user service implementation and its members
-keep class dev.lexip.hecate.util.shizuku.GrantService { *; }
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="ProtectedPermissions">

<queries>
<package android:name="moe.shizuku.privileged.api" />
</queries>

<uses-permission
android:name="android.permission.FOREGROUND_SERVICE"
tools:ignore="ForegroundServicesPolicy" />
Expand Down Expand Up @@ -52,6 +56,15 @@
android:localeConfig="@xml/locales_config"
tools:targetApi="33">

<!-- Shizuku provider to acquire binder from Shizuku/Sui -->
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />

<!-- Explicitly disable AdID collection for Firebase Analytics -->
<meta-data
android:name="google_analytics_adid_collection_enabled"
Expand Down
47 changes: 45 additions & 2 deletions app/src/main/java/dev/lexip/hecate/analytics/AnalyticsLogger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,11 @@ object AnalyticsLogger {
}
}

fun logSetupFinished(context: Context) {
fun logSetupComplete(context: Context, source: String? = null) {
ifAllowed {
analytics(context).logEvent("setup_finished") { }
analytics(context).logEvent("setup_finished") {
if (source != null) param("source", source)
}
}
}

Expand All @@ -136,4 +138,45 @@ object AnalyticsLogger {
analytics(context).logEvent("in_app_update_installed") { }
}
}

fun logUnexpectedShizukuError(
context: Context,
operation: String,
stage: String,
throwable: Throwable,
binderReady: Boolean,
packageName: String? = null
) {
ifAllowed {
analytics(context).logEvent("shizuku_unexpected_error") {
param("operation", operation)
param("stage", stage)
param("exception_type", throwable.javaClass.simpleName)
param("message", throwable.message ?: "no_message")
param("binder_ready", if (binderReady) 1L else 0L)
if (packageName != null) param("package_name", packageName)
}
}
}

fun logShizukuGrantResult(
context: Context,
result: dev.lexip.hecate.util.shizuku.ShizukuManager.GrantResult,
packageName: String
) {
ifAllowed {
analytics(context).logEvent("shizuku_grant_result") {
val (resultType, exitCode) = when (result) {
is dev.lexip.hecate.util.shizuku.ShizukuManager.GrantResult.Success -> "success" to null
is dev.lexip.hecate.util.shizuku.ShizukuManager.GrantResult.ServiceNotRunning -> "service_not_running" to null
is dev.lexip.hecate.util.shizuku.ShizukuManager.GrantResult.NotAuthorized -> "not_authorized" to null
is dev.lexip.hecate.util.shizuku.ShizukuManager.GrantResult.ShellCommandFailed -> "shell_command_failed" to result.exitCode
is dev.lexip.hecate.util.shizuku.ShizukuManager.GrantResult.Unexpected -> "unexpected" to null
}
param("result_type", resultType)
exitCode?.let { param("exit_code", it.toLong()) }
param("package_name", packageName)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import dev.lexip.hecate.HecateApplication
import dev.lexip.hecate.R
import dev.lexip.hecate.analytics.AnalyticsLogger
import dev.lexip.hecate.broadcasts.ScreenOnReceiver
import dev.lexip.hecate.data.UserPreferencesRepository
import dev.lexip.hecate.util.DarkThemeHandler
Expand Down Expand Up @@ -148,22 +147,6 @@ class BroadcastReceiverService : Service() {
pendingIntent
).build()

// Create action to pause/kill the service. The service will start again on next boot or app open.
val stopIntent = Intent(this, BroadcastReceiverService::class.java).apply {
action = ACTION_PAUSE_SERVICE
}
val pausePendingIntent = PendingIntent.getService(
this,
0,
stopIntent,
PendingIntent.FLAG_IMMUTABLE
)
val pauseAction = NotificationCompat.Action.Builder(
0,
getString(R.string.action_pause_service),
pausePendingIntent
).build()

// Build notification
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
Expand All @@ -173,7 +156,6 @@ class BroadcastReceiverService : Service() {
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent)
.addAction(disableAction)
.addAction(pauseAction)
.setOngoing(true)


Expand Down
Loading