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
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
<a href="https://play.google.com/store/apps/details?id=dev.lexip.hecate&referrer=utm_source%3Dgithub%26utm_medium%3Dreadme_button">
<img
src="https://img.shields.io/endpoint?url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Ddev.lexip.hecate%26l%3DDownloads%26m%3D%24totalinstalls&color=brightgreen&logo=google-play&logoColor=white"
alt="Play Store Download Count (6k+)">
alt="Play Store Download Count >10k">
</a>
<a href="https://play.google.com/store/apps/details?id=dev.lexip.hecate">
<img
src="https://img.shields.io/endpoint?url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Ddev.lexip.hecate%26l%3DRating%26m%3D%24rating&color=brightgreen&logo=google-play&logoColor=white"
alt="Play Store Rating >4,9*">
</a>

[![Feature Graphic / Screenshot](.github/resources/feature-graphic.png)](https://play.google.com/store/apps/details?id=dev.lexip.hecate&referrer=utm_source%3Dgithub%26utm_medium%3Dreadme_banner)
Expand Down Expand Up @@ -78,9 +83,8 @@ methods to do so:
installation required (WebADB).
👉 **[lexip.dev/setup](https://lexip.dev/setup)**

* **Shizuku** – If you have [Shizuku](https://play.google.com/store/apps/details?id=moe.shizuku.privileged.api&referrer=utm_source%3Dgithub_xlexip) installed and
configured, you can
grant the permission directly within Adaptive Theme.
* **Shizuku** – If you have [Shizuku](https://play.google.com/store/apps/details?id=moe.shizuku.privileged.api&referrer=utm_source%3Dgithub_xlexip)
installed and configured, you can grant the permission directly within Adaptive Theme.

* **Root** – If your device is rooted, you can grant the permission directly in Adaptive Theme as
well.
Expand Down Expand Up @@ -186,6 +190,9 @@ broadcasts – ensuring zero unnecessary battery drain in the background.
* HowToMen – [**Top 15 Best Android Apps - February 2026**](https://www.youtube.com/watch?v=iY3FBMTA15A&t=98s)
* Mr. Android FHD – [**8 Incredible Apps That Every Android User Needs in 2026**](https://www.youtube.com/watch?v=CH_4E1LzGcU&t=459s)
* El Androide Feliz – [**15 nuevas apps para Shizuku que son bestiales**](https://www.youtube.com/watch?v=eMznsQhldEw&t=152s)
* TechTab – [**Top 10 Android Apps you need to try - March 2026**](https://www.youtube.com/watch?v=nSFYlenb_-U&t=298s)
* Gadget Geek – [**Top 10 Best Android Apps | March 2026**](https://www.youtube.com/watch?v=8zQmriP8wSg&t=306s)
* Tech Tricks – [**10 Best New Top Rated Android Apps | You Can't Miss**](https://www.youtube.com/watch?v=Ti4Pt6hNZzc&t=257s)
* Всё про Андроид – [**Светлая и тёмная тема по датчику освещённости**](https://www.youtube.com/watch?v=Oj-WHpc5vK8)
* Thanks to [AlbertCaro](https://github.com/xLexip/Adaptive-Theme/pull/107) for spanish translation
strings.
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
applicationId = "dev.lexip.hecate"
minSdk = 34
targetSdk = 35
versionCode = 113
versionName = "1.2.1"
versionCode = 115
versionName = "1.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ enum class AdaptiveThreshold(val labelRes: Int, val lux: Float) {
SOFT(R.string.adaptive_threshold_soft, 10f),
BRIGHT(R.string.adaptive_threshold_bright, 100f),
DAYLIGHT(R.string.adaptive_threshold_daylight, 1_000f),
SUNLIGHT(R.string.adaptive_threshold_sunlight, 10_000f);
SUNLIGHT(R.string.adaptive_threshold_direct_sunlight, 10_000f);

companion object {
fun fromIndex(index: Int): AdaptiveThreshold {
Expand Down
52 changes: 44 additions & 8 deletions app/src/main/kotlin/dev/lexip/hecate/ui/setup/SetupViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.hardware.usb.UsbManager
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
Expand Down Expand Up @@ -52,6 +53,7 @@ import java.util.concurrent.atomic.AtomicBoolean
private const val AUTO_ADVANCE_DELAY = 2
private const val SHIZUKU_PACKAGE = "moe.shizuku.privileged.api"
private const val REQUEST_CODE_SHIZUKU = 1001
private const val TAG = "SetupViewModel"

/**
* UI state for the setup flow, observed by setup screens.
Expand Down Expand Up @@ -602,6 +604,7 @@ class SetupViewModel(
}

is RootGrantResult.Failure -> {
Log.w(TAG, "Root setup commands failed: ${result.reason}")
Toast.makeText(
context,
R.string.setup_root_grant_failed,
Expand All @@ -624,22 +627,55 @@ class SetupViewModel(
}

private fun tryGrantViaRoot(): RootGrantResult {
val packageName = application.packageName
val commands = ShizukuManager.buildAllGrantCommands(packageName)

for ((index, command) in commands.withIndex()) {
val result = executeSingleRootCommand(command)
when (result) {
is RootCommandResult.Success -> Unit
is RootCommandResult.Failure -> {
return RootGrantResult.Failure(
"command_${index + 1}_exit_${result.exitCode}: ${result.command}"
)
}
is RootCommandResult.Unexpected -> {
return RootGrantResult.Failure(
"command_${index + 1}_${result.throwableName}: ${result.command}"
)
}
}
}

return RootGrantResult.Success
}

private sealed interface RootCommandResult {
data object Success : RootCommandResult
data class Failure(val command: String, val exitCode: Int) : RootCommandResult
data class Unexpected(val command: String, val throwableName: String) : RootCommandResult
}

private fun executeSingleRootCommand(command: String): RootCommandResult {
return try {
val process = Runtime.getRuntime().exec("su")
val os = java.io.DataOutputStream(process.outputStream)
os.writeBytes("pm grant dev.lexip.hecate android.permission.WRITE_SECURE_SETTINGS\n")
os.writeBytes("exit\n")
os.flush()
os.close()
java.io.DataOutputStream(process.outputStream).use { os ->
os.writeBytes("$command\n")
os.writeBytes("exit\n")
os.flush()
}

val exitCode = process.waitFor()
if (exitCode == 0) {
RootGrantResult.Success
RootCommandResult.Success
} else {
RootGrantResult.Failure("exit_code_$exitCode")
RootCommandResult.Failure(command = command, exitCode = exitCode)
}
} catch (e: Exception) {
RootGrantResult.Failure(e.javaClass.simpleName)
RootCommandResult.Unexpected(
command = command,
throwableName = e.javaClass.simpleName
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ private fun UsbDebuggingCard(
showAction = !isEnabled,
actionConfig = ActionConfig(
labelRes = R.string.setup_action_open_developer_settings,
toastResOne = R.string.setup_usb_debugging_toast,
toastResOne = R.string.setup_usb_debugging_enable_toast,
toastResTwo = null,
onAction = onOpenDeveloperSettings,
enabled = isDeveloperOptionsEnabled
Expand Down
33 changes: 30 additions & 3 deletions app/src/main/kotlin/dev/lexip/hecate/util/shizuku/GrantService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,21 @@ class GrantService : Binder() {

TRANSACTION_EXECUTE_CMD -> {
data.enforceInterface(DESCRIPTOR)
val cmd = data.readString()
Log.d(TAG, "Received command via Shizuku: $cmd")
val exitCode = runShell(cmd)

val first = data.readInt()
val commands: List<String?> = if (first == -1) {
listOf(data.readString())
} else {
val count = first
buildList {
repeat(count.coerceAtLeast(0)) {
add(data.readString())
}
}
}

val (failedIndex, exitCode) = runShellAll(commands)
reply?.writeInt(failedIndex)
reply?.writeInt(exitCode)
true
}
Expand All @@ -41,6 +53,21 @@ class GrantService : Binder() {
}
}

private fun runShellAll(commands: List<String?>): Pair<Int, Int> {
commands.forEachIndexed { index, cmd ->
if (cmd.isNullOrBlank()) {
Log.w(TAG, "Received blank command at index=$index")
return index to -1
}

Log.d(TAG, "Received command via Shizuku (index=$index): $cmd")
val code = runShell(cmd)
if (code != 0) return index to code
}

return -1 to 0
}

private fun runShell(cmd: String?): Int {
if (cmd.isNullOrBlank()) return -1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ object ShizukuManager {
object ServiceNotRunning : GrantResult()
object NotAuthorized : GrantResult()
object Unexpected : GrantResult()
data class ShellCommandFailed(val exitCode: Int) : GrantResult()
data class ShellCommandFailed(
val commandIndex: Int,
val command: String,
val exitCode: Int
) : GrantResult()
}

init {
Expand Down Expand Up @@ -90,12 +94,25 @@ object ShizukuManager {
fun buildGrantWriteSecureSettingsCommand(packageName: String): String =
"pm grant $packageName android.permission.WRITE_SECURE_SETTINGS"

private fun buildAllowRunAnyInBackgroundCommand(packageName: String): String =
"cmd appops set $packageName RUN_ANY_IN_BACKGROUND allow"

private fun buildDeviceIdleWhitelistCommand(packageName: String): String =
"dumpsys deviceidle whitelist +$packageName"

fun buildAllGrantCommands(packageName: String): List<String> =
listOf(
buildGrantWriteSecureSettingsCommand(packageName),
buildAllowRunAnyInBackgroundCommand(packageName),
buildDeviceIdleWhitelistCommand(packageName)
)

fun executeGrantViaShizuku(context: Context, packageName: String): GrantResult {
if (Shizuku.isPreV11()) return GrantResult.ServiceNotRunning
if (!binderReady) return GrantResult.ServiceNotRunning
if (!hasPermission(context)) return GrantResult.NotAuthorized

val cmd = buildGrantWriteSecureSettingsCommand(packageName)
val commands = buildAllGrantCommands(packageName)

return try {
val monitor = Object()
Expand All @@ -105,7 +122,7 @@ object ShizukuManager {
val connection = createGrantServiceConnection(
context,
args,
cmd,
commands,
monitor,
packageName
) { grantResult ->
Expand Down Expand Up @@ -142,7 +159,7 @@ object ShizukuManager {
private fun createGrantServiceConnection(
context: Context,
args: Shizuku.UserServiceArgs,
cmd: String,
commands: List<String>,
monitor: Object,
packageName: String,
onResult: (GrantResult) -> Unit
Expand All @@ -156,7 +173,7 @@ object ShizukuManager {
if (binder == null) {
GrantResult.ServiceNotRunning
} else {
executeGrantTransaction(binder, cmd)
executeGrantTransaction(binder, commands)
}
} catch (t: Throwable) {
when (t) {
Expand Down Expand Up @@ -203,21 +220,36 @@ object ShizukuManager {

private fun executeGrantTransaction(
binder: IBinder,
cmd: String
commands: List<String>
): GrantResult {
val data = Parcel.obtain()
val reply = Parcel.obtain()
return try {
data.writeInterfaceToken("dev.lexip.hecate.util.shizuku.GrantService")
data.writeString(cmd)
data.writeInt(commands.size)
commands.forEach { data.writeString(it) }
val transactionCode = Binder.FIRST_CALL_TRANSACTION + 1
val success = binder.transact(transactionCode, data, reply, 0)
if (!success) {
GrantResult.ServiceNotRunning
} else {
val failedIndex = reply.readInt()
val exitCode = reply.readInt()
if (exitCode == 0) GrantResult.Success
else GrantResult.ShellCommandFailed(exitCode)
if (failedIndex == -1 && exitCode == 0) {
GrantResult.Success
} else {
val safeIndex = failedIndex.coerceIn(0, (commands.size - 1).coerceAtLeast(0))
val cmd: String = if (safeIndex in commands.indices) {
commands[safeIndex]
} else {
"<unknown>"
}
GrantResult.ShellCommandFailed(
commandIndex = failedIndex,
command = cmd,
exitCode = exitCode
)
}
}
} finally {
data.recycle()
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<string name="adaptive_threshold_daylight">Tageslicht</string>
<string name="adaptive_threshold_dim">Schwach</string>
<string name="adaptive_threshold_soft">Leicht</string>
<string name="adaptive_threshold_sunlight">Sonnenlicht</string>
<string name="adaptive_threshold_direct_sunlight">Direktes Sonnenlicht</string>

<string name="hint_custom_threshold_value">Lux-Wert</string>

Expand All @@ -67,7 +67,7 @@
<string name="setup_permission_not_granted">Berechtigung noch nicht erteilt. Bitte schließe die Einrichtung mit dem anderen Gerät ab.</string>
<string name="setup_usb_debugging_disabled">USB-Debugging aktivieren</string>
<string name="setup_usb_debugging_enabled">USB-Debugging aktiviert</string>
<string name="setup_usb_debugging_toast">Aktiviere USB-Debugging</string>
<string name="setup_usb_debugging_enable_toast">Aktiviere USB-Debugging in den Entwickleroptionen.</string>
<string name="setup_usb_not_connected">Warte auf USB-Verbindung…</string>
<string name="setup_usb_connected">USB verbunden</string>
<string name="setup_why_other_device_title">Warum ist ein anderes Gerät nötig?</string>
Expand Down
Loading
Loading