diff --git a/README.md b/README.md index 99f9ec8..28b7e26 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Adaptive Theme: Auto Dark Mode by Ambient Light -Adaptive Theme automatically switches between Light and **Dark mode** +Adaptive Theme automatically switches between light and **dark mode** using the **ambient light sensor** — not a fixed schedule. It adapts to real lighting conditions to optimize **readability**, **eye comfort**, and **battery @@ -19,7 +19,7 @@ life**. ## 🚀 Quick Start (2 minutes) 1. **Install** Adaptive Theme. -2. **Grant the permission** with a [web-tool](https://lexip.dev/setup), Shizuku, or other methods +2. **Grant the permission** with the [web-tool](https://lexip.dev/setup), Shizuku, or other methods below. 3. **Pick your lux threshold** and you’re done. ✅ @@ -27,8 +27,8 @@ life**. - [✨ Features & Highlights](#-features--highlights) - [🛠️ One-Time Setup](#%EF%B8%8F-one-time-setup) -- [⚙️ How it Works](#%EF%B8%8F-how-it-works) - [✅ Safety](#-safety) +- [⚙️ How it Works](#%EF%B8%8F-how-it-works) - [❓ FAQ](#-faq) - [❤️ Support the Project](#%EF%B8%8F-support-the-project) - [🏗️ Architecture & Tech Stack](#%EF%B8%8F-architecture--tech-stack) @@ -37,15 +37,17 @@ life**. * 🌤️ **Smart Detection:** Uses your devices physical light sensor to switch the system theme. -* ⚙️ **Custom brightness threshold:** Choose exactly when Light ↔ Dark should flip. +* ⚙️ **Custom brightness threshold:** Choose exactly when the theme should flip or use a preset ( + indoor, outdoor, sunlight, etc.). * 🔋 **Battery Friendly:** The app is passive. Its event-driven architecture only checks the sensor when you turn on the screen — zero battery drain in the background. -* 🗝️ **No Root Required:** Root access is not required (but is supported as an alternative setup +* 🗝️ **No Root Required:** Root access is not required (but supported as an alternative setup method). * 🐱 **Shizuku Support:** One of multiple setup options is using [Shizuku](https://github.com/RikkaApps/Shizuku). * 🚀 **Modern & Native:** Built with best-practices using Kotlin, Jetpack Compose and Material You for a smooth and solid experience. +* 🌍 **50+ Languages:** Applied globalization at its best. * 🔒 **Transparent:** Free, open-source, no-ads. ## 🛠️ One-Time Setup @@ -56,19 +58,29 @@ permission (`WRITE_SECURE_SETTINGS`) has to be granted. The app comes with an easy step-by-step setup process, that lets you choose one of the following methods to do so: -* **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)** +* **Web Tool (Recommended)** – A 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)** * **Shizuku** – If you have **[Shizuku](https://github.com/RikkaApps/Shizuku)** installed and configured, you can - grant the permission directly within the Adaptive Theme app. + grant the permission directly within Adaptive Theme. + +* **Root** – If your device is rooted, you can grant the permission directly in Adaptive Theme as + well. + +* **Manual ADB** – If you have ADB installed on your computer, you can simply run the ADB command + manually: + ```adb shell pm grant dev.lexip.hecate android.permission.WRITE_SECURE_SETTINGS``` -* **Root** – If your device is rooted, you can grant the permission with one tap inside the app. +## ✅ Safety -* **Manual ADB** – If you have ADB installed on your computer, you can run the ADB command manually. +The required permission only allows the app to change system settings such as the dark mode. This is +absolutely safe and +completely reversible by uninstalling the app. It does **not** grant root access or read any user +data. ## ⚙️ How it Works @@ -76,24 +88,19 @@ installation required (WebADB). To avoid screen flicker and unnecessary background work, Adaptive Theme follows strict rules: -- **Event-driven:** It checks the light sensor only immediately after the screen turns on. +- **Event-driven:** It checks the light sensor only right after the screen turns on. Combined with + hysteresis, this prevents flicker, avoids interruptions while you’re using the phone, and saves + battery. - **Validity check:** It verifies that the sensor is not obstructed (e.g., by a hand or pocket). - **Seamless switch:** It switches the theme instantly, ensuring the UI is ready before you start interacting with it. -## ✅ Safety - -The required permission only allows the app to change system settings such as the dark mode. This is -absolutely safe and -completely reversible by uninstalling the app. It does **not** grant root access or read any user -data. - ## ❓ FAQ **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. +* No. It works on stock devices. However, if you have Root, it can be used as an alternative setup + method. **Does it work with custom Android skins (Xiaomi MIUI, Samsung OneUI, etc.)?** @@ -107,11 +114,12 @@ data. your device usage. - Check that your sensor isn’t covered when you turn the screen on. - Adjust your lux threshold and test in clearly bright/dim conditions. +- Check if the current lux value is shown correctly in the Adaptive Theme app. ### Support & Feedback -If Adaptive Theme doesn’t work for you — or if you have any questions or ideas — please open an -issue here or send feedback via the app. +If Adaptive Theme doesn’t work for you — or if you have any questions or ideas — please [open an +issue](https://github.com/xLexip/Adaptive-Theme/issues/new) here or send feedback via the app. ## ❤️ Support the Project @@ -142,7 +150,8 @@ also [buy me a coffee](https://buymeacoffee.com/lexip). Adaptive Theme is built with modern Android engineering standards to ensure a lightweight, maintainable, and production-ready codebase. -**Modern Codebase:** Written entirely in Kotlin with Jetpack Compose and Material 3 (Material You). +**Modern Codebase:** Written entirely in Kotlin with Jetpack Compose and Material 3 (Material You), +including haptic feedback. **Architecture:** Follows the MVVM pattern with a Single-Activity architecture. @@ -155,7 +164,8 @@ broadcasts – ensuring zero unnecessary battery drain in the background. ### **Made with 🥨 in Germany.** -> ~~> Keywords: theme switcher · android automation · night mode · dark sense · automatic dark +> ~~> Keywords: theme switcher · android automation · night mode · dark sense · automatic android +dark mode · brightness-based · light-based · based on lux · google pixel · auto dark theme · shizuku apps · android 14 · android diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 413df9c..bf21edb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "dev.lexip.hecate" minSdk = 34 targetSdk = 35 - versionCode = 97 - versionName = "1.0.0" + versionCode = 103 + versionName = "1.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -123,6 +123,8 @@ dependencies { "playImplementation"(libs.firebase.analytics) "playImplementation"(libs.firebase.crashlytics) "playImplementation"(libs.app.update.ktx) + "playImplementation"(libs.review) + "playImplementation"(libs.review.ktx) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/foss/kotlin/dev/lexip/hecate/util/InAppReviewHandler.kt b/app/src/foss/kotlin/dev/lexip/hecate/util/InAppReviewHandler.kt new file mode 100644 index 0000000..ae158a5 --- /dev/null +++ b/app/src/foss/kotlin/dev/lexip/hecate/util/InAppReviewHandler.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 xLexip + * + * Licensed under the GNU General Public License, Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/gpl-3.0 + * + * Please see the License for specific terms regarding permissions and limitations. + */ + +package dev.lexip.hecate.util + +import android.app.Activity + +object InAppReviewHandler { + + fun setReviewPending() { + // No-op for FOSS flavor + } + + fun checkAndTriggerReview(activity: Activity) { + // No-op for FOSS flavor + } +} + diff --git a/app/src/main/kotlin/dev/lexip/hecate/ui/MainActivity.kt b/app/src/main/kotlin/dev/lexip/hecate/ui/MainActivity.kt index 89f6433..9bf3796 100644 --- a/app/src/main/kotlin/dev/lexip/hecate/ui/MainActivity.kt +++ b/app/src/main/kotlin/dev/lexip/hecate/ui/MainActivity.kt @@ -25,6 +25,7 @@ import dev.lexip.hecate.services.BroadcastReceiverService import dev.lexip.hecate.ui.navigation.NavigationManager import dev.lexip.hecate.ui.theme.HecateTheme import dev.lexip.hecate.util.DarkThemeHandler +import dev.lexip.hecate.util.InAppReviewHandler import dev.lexip.hecate.util.InAppUpdateManager import dev.lexip.hecate.util.InstallSourceChecker @@ -74,6 +75,8 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() + InAppReviewHandler.checkAndTriggerReview(this) + inAppUpdateManager?.resumeImmediateUpdateIfNeeded() inAppUpdateManager?.resumeFlexibleUpdateIfNeeded() diff --git a/app/src/main/kotlin/dev/lexip/hecate/ui/MainScreen.kt b/app/src/main/kotlin/dev/lexip/hecate/ui/MainScreen.kt index d035173..cf7f969 100644 --- a/app/src/main/kotlin/dev/lexip/hecate/ui/MainScreen.kt +++ b/app/src/main/kotlin/dev/lexip/hecate/ui/MainScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -97,6 +98,8 @@ fun MainScreen( val internalUiState by mainViewModel.uiState.collectAsState() + val isSystemDark = isSystemInDarkTheme() + LaunchedEffect(Unit) { val installed = ShizukuAvailability.isShizukuInstalled(context) mainViewModel.setShizukuInstalled(installed) @@ -271,7 +274,11 @@ fun MainScreen( onValueChange = { index -> mainViewModel.setPendingCustomSliderLux(lux[index]) mainViewModel.onSliderValueCommitted(index) - textShakeKey.intValue += 1 + + // Shake the description text when the user could expect an immediate theme switch + if ((currentLux > lux[index]) == isSystemDark) { + textShakeKey.intValue += 1 + } }, enabled = uiState.adaptiveThemeEnabled, firstCard = true, diff --git a/app/src/main/kotlin/dev/lexip/hecate/ui/components/ThreeDotMenu.kt b/app/src/main/kotlin/dev/lexip/hecate/ui/components/ThreeDotMenu.kt index 2556c63..6fdca53 100644 --- a/app/src/main/kotlin/dev/lexip/hecate/ui/components/ThreeDotMenu.kt +++ b/app/src/main/kotlin/dev/lexip/hecate/ui/components/ThreeDotMenu.kt @@ -28,11 +28,11 @@ import androidx.core.net.toUri import dev.lexip.hecate.BuildConfig import dev.lexip.hecate.R import dev.lexip.hecate.logging.Logger +import dev.lexip.hecate.util.InAppReviewHandler import dev.lexip.hecate.util.InstallSourceChecker import java.net.URLEncoder import java.nio.charset.StandardCharsets - const val FEEDBACK_SUBJECT = "Adaptive Theme Feedback (v${BuildConfig.VERSION_NAME})" @Composable @@ -150,6 +150,11 @@ fun ThreeDotMenu( context, "support_project" ) + + if (isAdaptiveThemeEnabled) { + InAppReviewHandler.setReviewPending() + } + val supportUri = "https://github.com/xLexip/Adaptive-Theme?tab=readme-ov-file#%EF%B8%8F-support-the-project".toUri() val supportIntent = Intent(Intent.ACTION_VIEW, supportUri) diff --git a/app/src/main/kotlin/dev/lexip/hecate/ui/setup/SetupViewModel.kt b/app/src/main/kotlin/dev/lexip/hecate/ui/setup/SetupViewModel.kt index bbd0d5a..57fb9cd 100644 --- a/app/src/main/kotlin/dev/lexip/hecate/ui/setup/SetupViewModel.kt +++ b/app/src/main/kotlin/dev/lexip/hecate/ui/setup/SetupViewModel.kt @@ -586,7 +586,7 @@ class SetupViewModel( fun onGrantViaRootRequested() { val context = application.applicationContext - Toast.makeText(context, R.string.setup_root_grant_starting, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.setup_root_request, Toast.LENGTH_SHORT).show() viewModelScope.launch(ioDispatcher) { val result = tryGrantViaRoot() diff --git a/app/src/main/kotlin/dev/lexip/hecate/ui/setup/components/ForExpertsSection.kt b/app/src/main/kotlin/dev/lexip/hecate/ui/setup/components/ForExpertsSection.kt index bd28f04..6483a52 100644 --- a/app/src/main/kotlin/dev/lexip/hecate/ui/setup/components/ForExpertsSection.kt +++ b/app/src/main/kotlin/dev/lexip/hecate/ui/setup/components/ForExpertsSection.kt @@ -115,7 +115,7 @@ internal fun ForExpertsSectionCard( Column(modifier = Modifier.fillMaxWidth()) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(id = R.string.setup_manual_command), + text = stringResource(id = R.string.setup_alternatives), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 977fdb8..55d8912 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -62,7 +62,7 @@ Alternative Methoden Schritt 3: Berechtigung mit dem anderen Gerät erteilen Öffne den folgenden Link auf deinem anderen Gerät und folge den Anweisungen: - Alternativ kannst du diesen ADB-Command ausführen oder Root benutzen. Shizuku wird ebenfalls unterstützt. + Alternativ kannst du einen ADB-Command ausführen oder Root benutzen. Shizuku wird ebenfalls unterstützt. Berechtigung erteilt. Berechtigung noch nicht erteilt. Bitte schließe die Einrichtung mit dem anderen Gerät ab. USB-Debugging aktivieren @@ -73,12 +73,12 @@ Warum ist ein anderes Gerät nötig? Android verhindert, dass Apps sich die benötigte Berechtigung selbst erteilen. Es wird ein anderes Gerät mit einem Webbrowser oder ADB benötigt. Ist das sicher? - Ja. Es erlaubt der App nur, Einstellungen wie den Dunkelmodus zu ändern. Das ist absolut sicher und vollständig rückgängig zu machen. + Ja. Es erlaubt der App nur, Einstellungen wie den Dark Mode zu ändern. Das ist absolut sicher und vollständig rückgängig zu machen. Außerdem ist der Quellcode öffentlich auf GitHub einsehbar. Alternative: Shizuku Wenn du Shizuku bereits verwendest, benötigst du weder ein zweites Gerät noch ADB. Shizuku verwenden Root verwenden - Root-Zugriff angefragt… (beta) + Root-Zugriff angefragt… Root-Vergabe fehlgeschlagen. Ist das Gerät gerooted? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d490c6..f21fba5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,7 +67,7 @@ Alternative methods Step 3: Grant permission with the other device Open the following link on your other device and follow the instructions: - Alternatively, you can execute this ADB command or use root to grant the permission. Shizuku is also supported. + Alternatively, you can execute an ADB command yourself or use root to grant the permission. Shizuku is also supported. Permission granted. Permission not yet granted. Please complete the setup with the other device. Enable USB Debugging @@ -78,12 +78,12 @@ Why is another device required? Android prevents apps from granting the required permission themselves. It requires another device with either a web browser or ADB installed. Is this safe? - Yes. It just allows the app to modify settings like the dark mode. This is absolutely safe and completely reversible. + Yes. It just allows the app to modify settings like the dark mode. This is absolutely safe and completely reversible. Furthermore, the source code is publicly available on GitHub. Alternative: Shizuku If you already use Shizuku, you won’t need a second device or ADB. Use Shizuku instead Use Root - Requesting root access… (beta) + Requesting root access… Root grant failed. Is the device rooted? diff --git a/app/src/play/kotlin/dev/lexip/hecate/logging/Logger.kt b/app/src/play/kotlin/dev/lexip/hecate/logging/Logger.kt index a5b8965..5de682d 100644 --- a/app/src/play/kotlin/dev/lexip/hecate/logging/Logger.kt +++ b/app/src/play/kotlin/dev/lexip/hecate/logging/Logger.kt @@ -171,4 +171,11 @@ object Logger { } } } + + fun logInAppReviewFlowCompleted(context: Context) { + ifAllowed { + analytics(context).logEvent("review_flow_completed") { } + } + } + } diff --git a/app/src/play/kotlin/dev/lexip/hecate/util/InAppReviewHandler.kt b/app/src/play/kotlin/dev/lexip/hecate/util/InAppReviewHandler.kt new file mode 100644 index 0000000..75b6556 --- /dev/null +++ b/app/src/play/kotlin/dev/lexip/hecate/util/InAppReviewHandler.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025 xLexip + * + * Licensed under the GNU General Public License, Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.gnu.org/licenses/gpl-3.0 + * + * Please see the License for specific terms regarding permissions and limitations. + */ + +package dev.lexip.hecate.util + +import android.app.Activity +import android.util.Log +import android.widget.Toast +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.android.play.core.review.testing.FakeReviewManager +import dev.lexip.hecate.BuildConfig +import dev.lexip.hecate.logging.Logger + +object InAppReviewHandler { + + private const val TAG = "InAppReviewHandler" + private var isReviewPending = false + + fun setReviewPending() { + isReviewPending = true + } + + fun checkAndTriggerReview(activity: Activity) { + if (!isReviewPending) return + isReviewPending = false + + val manager = if (BuildConfig.DEBUG) { + FakeReviewManager(activity) + } else { + ReviewManagerFactory.create(activity) + } + + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + val reviewInfo = task.result + val flow = manager.launchReviewFlow(activity, reviewInfo) + flow.addOnCompleteListener { flowTask -> + if (BuildConfig.DEBUG) { + Toast.makeText(activity, "Fake Review Flow Completed", Toast.LENGTH_SHORT) + .show() + } + if (flowTask.isSuccessful) { + Logger.logInAppReviewFlowCompleted(activity) + } else { + val exception = flowTask.exception + Log.e(TAG, "Review flow failed", exception) + if (exception != null) Logger.logException(exception) + } + } + } else { + val exception = task.exception + Log.e(TAG, "Review manager request failed", exception) + if (exception != null) Logger.logException(exception) + } + } + } +} diff --git a/app/src/play/kotlin/dev/lexip/hecate/util/InAppUpdateManager.kt b/app/src/play/kotlin/dev/lexip/hecate/util/InAppUpdateManager.kt index ad36255..2afea6c 100644 --- a/app/src/play/kotlin/dev/lexip/hecate/util/InAppUpdateManager.kt +++ b/app/src/play/kotlin/dev/lexip/hecate/util/InAppUpdateManager.kt @@ -29,7 +29,7 @@ import dev.lexip.hecate.logging.Logger private const val TAG = "InAppUpdateManager" -private const val DAYS_FOR_IMMEDIATE_UPDATE = 3 +private const val DAYS_FOR_IMMEDIATE_UPDATE = 2 private const val MIN_PRIORITY_FOR_IMMEDIATE = 0 private const val DAYS_FOR_FLEXIBLE_UPDATE = 0 private const val MIN_PRIORITY_FOR_FLEXIBLE = 0 diff --git a/build.gradle.kts b/build.gradle.kts index 33777f3..20459a6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ plugins { sonar { properties { property("sonar.projectKey", "xLexip_Hecate") - property("sonar.projectVersion", "0.12.0") + property("sonar.projectVersion", "1.1.0") property("sonar.organization", "xlexip") property("sonar.host.url", "https://sonarcloud.io") property("sonar.androidLint.reportPaths", "app/build/reports/lint-results-debug.html") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ed3211..529a211 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ firebaseCrashlytics = "20.0.3" googleFirebaseCrashlytics = "3.0.6" junit = "4.13.2" junitVersion = "1.3.0" -kotlin = "2.2.21" +kotlin = "2.3.0" kotlinxSerializationJson = "1.9.0" lifecycleRuntimeKtx = "2.10.0" lifecycleViewmodelCompose = "2.10.0" @@ -22,6 +22,7 @@ localbroadcastmanager = "1.1.0" material = "1.13.0" navigationCompose = "2.9.6" preference = "1.2.1" +review = "2.0.2" shizuku = "13.1.5" [libraries] @@ -47,6 +48,8 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } +review = { group = "com.google.android.play", name = "review", version.ref = "review" } +review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "review" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } junit = { group = "junit", name = "junit", version.ref = "junit" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }