app/src/main/java/dev/equwal/inkdim/Dim.kt (3572 bytes)
1 // SPDX-License-Identifier: GPL-3.0-or-later 2 // Copyright (c) 2026 equwal 3 package dev.equwal.inkdim 4 5 /** 6 * The decision and the shell commands. No Android types, so it is all testable. 7 * 8 * Measured on the Viwoods reader: the frontlight is an ordinary backlight LED. 9 * Its driver accepts 1 as the lowest lit value, but the framework will not go 10 * below 5. Ask for 4 through any official route and it snaps to zero. A write 11 * straight to the LED node gets under that floor, and the value then holds, 12 * even across sleep, until the system brightness is next set. 13 * 14 * The node belongs to `system`, so a plain shell user cannot write it. These 15 * readers ship a userdebug build, where the shell user may run `su 0`. So each 16 * write tries the plain write first and falls back to `su 0`. 17 */ 18 object Dim { 19 20 /** What the caller asked for. */ 21 enum class Action { TOGGLE, ON, OFF } 22 23 /** The light nodes to try, in order. The first one that exists wins. */ 24 val nodes = listOf( 25 "/sys/class/leds/lcd-backlight/brightness", 26 "/sys/class/backlight/panel0-backlight/brightness" 27 ) 28 29 /** The value to hold the light at. The lowest value the driver still lights. */ 30 const val ON_VALUE = 1 31 32 /** 33 * The state the light is in now. 34 * 35 * The node wins, because the user can change the brightness in the system 36 * at any time. [remembered] is the fallback when the node could not be read. 37 */ 38 fun isOn(nodeValue: Int?, remembered: Boolean): Boolean = 39 if (nodeValue == null) remembered else nodeValue <= ON_VALUE 40 41 /** The state to go to. */ 42 fun next(nodeValue: Int?, remembered: Boolean, action: Action): Boolean = when (action) { 43 Action.ON -> true 44 Action.OFF -> false 45 Action.TOGGLE -> !isOn(nodeValue, remembered) 46 } 47 48 // ---- shell commands ---------------------------------------------------- 49 50 /** 51 * Writes [value] to the first node that exists. A plain write first, then 52 * through `su 0`, then a read back to prove the value took. 53 * 54 * Exit 0 the light holds [value], 1 the write was refused, 2 there is no 55 * node on this device. 56 */ 57 fun writeCommand(nodes: List<String>, value: Int): String { 58 val list = nodes.joinToString(" ") { "'$it'" } 59 return "for n in $list; do [ -e \$n ] || continue; " + 60 "echo $value > \$n 2>/dev/null || su 0 sh -c \"echo $value > \$n\"; " + 61 "[ \"\$(cat \$n)\" = \"$value\" ] && exit 0; exit 1; done; exit 2" 62 } 63 64 /** 65 * Prints the value of the first node that exists. Exit 1 means this device 66 * has no light node that this app knows. 67 */ 68 fun readCommand(nodes: List<String>): String { 69 val list = nodes.joinToString(" ") { "'$it'" } 70 return "for n in $list; do [ -e \$n ] || continue; cat \$n; exit 0; done; exit 1" 71 } 72 73 /** 74 * Gives the light back to the system. 75 * 76 * The system brightness goes up by one and straight back. That makes the 77 * framework write the node again, with the value the user chose. So "off" 78 * always lands on the brightness the system holds, whatever it is. 79 */ 80 fun restoreCommand(): String = 81 "v=\$(settings get system screen_brightness); " + 82 "settings put system screen_brightness \$((v+1)); " + 83 "settings put system screen_brightness \$v" 84 85 /** The value the node holds, or null. Never throws. */ 86 fun parseRead(output: String): Int? = 87 output.lineSequence().map { it.trim() }.firstNotNullOfOrNull { it.toIntOrNull() } 88 }