commit 4d0fbdf2a413720fdcc68c9c74079c98b52cc213 truex <truex@equwal.com> 2026-09-21 00:27:37 -0700 Fix Extra dim toggle; add a switch for the system Emergency SOS Extra dim toggle. On now goes to the lowest level. Off gives the light back to the brightness set in the system. Before, on used the level last in use, or a middle level when there was none. ExtraDim.toggled is the rule, with regression tests. The stored "last level" value is deleted. The three light actions get their name from the payload and not from the stored label, so a binding made by 0.0.6 shows the new name: Extra dim toggle, Extra dim darker, Extra dim brighter. Emergency SOS. Android starts Emergency SOS on five quick Power presses, which collides with a 5-tap binding. The Power screen has a switch for it (Settings.Secure emergency_gesture_enabled). The app never turns it off by itself. The app writes the setting directly when it holds WRITE_SECURE_SETTINGS, and through the shell otherwise. The switch shows only where one of the two ways is open. Tested on the reader: with the value 0, five presses are counted and no SOS starts.
CHANGELOG.md | 14 ++++++++++ .../java/dev/equwal/assistkey/display/ExtraDim.kt | 13 ++++----- .../java/dev/equwal/assistkey/model/ActionSpec.kt | 12 ++++++++- .../dev/equwal/assistkey/native/PowerNative.kt | 31 ++++++++++++++++++++++ .../equwal/assistkey/ui/ActionPickerActivity.kt | 11 ++++---- .../dev/equwal/assistkey/ui/DisplayActivity.kt | 2 +- .../java/dev/equwal/assistkey/ui/PowerActivity.kt | 21 +++++++++++++++ .../equwal/assistkey/display/ExtraDimStepTest.kt | 10 +++++++ .../equwal/assistkey/model/ActionSpecNameTest.kt | 22 +++++++++++++++ gradle.properties | 4 +-- 10 files changed, 124 insertions(+), 16 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md index 98bb3a9..0c054e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.0.7-alpha - 2026-09-21 + +Version code 7. + +- **Fix: Extra dim toggle.** On now goes to the lowest level (1). Off gives the + light back to the brightness set in the system. Before, on used the level + that was last in use, or a middle level. Regression tests added. +- **Emergency SOS switch.** Android starts Emergency SOS on five quick Power + presses, which collides with a 5-tap binding. The Power screen now has a + switch for it (`emergency_gesture_enabled`). The app never turns it off by + itself. The switch shows only where the app can write the setting. +- The three light actions are now named Extra dim toggle, Extra dim darker and + Extra dim brighter. A binding made by 0.0.6 shows the new name with no change. + ## 0.0.6-alpha - 2026-09-21 Version code 6. diff --git a/app/src/main/java/dev/equwal/assistkey/display/ExtraDim.kt b/app/src/main/java/dev/equwal/assistkey/display/ExtraDim.kt index 41c9806..ad18802 100644 --- a/app/src/main/java/dev/equwal/assistkey/display/ExtraDim.kt +++ b/app/src/main/java/dev/equwal/assistkey/display/ExtraDim.kt @@ -43,8 +43,6 @@ object ExtraDim { // ---- from a key --------------------------------------------------------------------- - private const val K_LAST = "extra_dim_last" - /** * The order of brightness, darkest last: off (the system level), then the * levels under the floor from high to low. [darker] is one step down that @@ -61,17 +59,20 @@ object ExtraDim { return if (i <= 0) 0 else levels[i - 1] } + /** + * On and off. On is the lowest level there is. Off gives the light back to + * the brightness the user set in the system. + */ + fun toggled(level: Int, levels: List<Int>): Int = if (level > 0) 0 else levels.lastOrNull() ?: 0 + /** Runs a key action: "darker", "brighter" or "toggle". */ fun act(c: Context, what: String, done: (Boolean) -> Unit = {}) { - val p = c.getSharedPreferences(PREFS, Context.MODE_PRIVATE) val now = level(c) val next = when (what) { "darker" -> darker(now, levels()) "brighter" -> brighter(now, levels()) - // Toggle goes back to the level that was in use, not to a fixed one. - else -> if (now > 0) 0 else p.getInt(K_LAST, levels().getOrElse(levels().size / 2) { 0 }) + else -> toggled(now, levels()) } - if (next > 0) p.edit().putInt(K_LAST, next).apply() if (next == now) return done(true) set(c, next, done) } diff --git a/app/src/main/java/dev/equwal/assistkey/model/ActionSpec.kt b/app/src/main/java/dev/equwal/assistkey/model/ActionSpec.kt index fbe7861..700c6da 100644 --- a/app/src/main/java/dev/equwal/assistkey/model/ActionSpec.kt +++ b/app/src/main/java/dev/equwal/assistkey/model/ActionSpec.kt @@ -67,7 +67,17 @@ data class ActionSpec( .put("payload", payload) .put("label", label) - fun describe(): String = label.ifBlank { + /** + * The name of a light action comes from its payload, not from the stored + * label, so that a binding made by an older version shows the current name. + */ + private fun dimName(): String = when (payload) { + "darker" -> "Extra dim darker" + "brighter" -> "Extra dim brighter" + else -> "Extra dim toggle" + } + + fun describe(): String = if (kind == ActionKind.DIM) dimName() else label.ifBlank { when (kind) { ActionKind.NONE -> "Disabled" ActionKind.PASS_THROUGH -> "Default behaviour" diff --git a/app/src/main/java/dev/equwal/assistkey/native/PowerNative.kt b/app/src/main/java/dev/equwal/assistkey/native/PowerNative.kt index ffb5e3c..11216d5 100644 --- a/app/src/main/java/dev/equwal/assistkey/native/PowerNative.kt +++ b/app/src/main/java/dev/equwal/assistkey/native/PowerNative.kt @@ -69,6 +69,37 @@ object PowerNative { val onOff = listOf(Option(1, "On"), Option(0, "Off")) + // ---- Emergency SOS ---------------------------------------------------------------- + + private const val EMERGENCY_GESTURE = "emergency_gesture_enabled" + private const val PREFS = "assistkey_power" + private const val K_SOS = "system_sos" + + /** + * What the user chose here. The setting itself cannot be read by an app, so + * the app remembers its own last write. True, the system default, until then. + */ + fun emergencySos(c: Context): Boolean = + c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(K_SOS, true) + + /** + * Switches the system's "press Power five times" Emergency SOS. It writes + * the setting directly when the app holds WRITE_SECURE_SETTINGS, and asks + * the shell otherwise. [done] gets false when neither way is open. + */ + fun setEmergencySos(c: Context, on: Boolean, done: (Boolean) -> Unit) { + val value = if (on) "1" else "0" + fun remember(ok: Boolean) { + if (ok) c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(K_SOS, on).apply() + done(ok) + } + val direct = runCatching { Settings.Secure.putString(c.contentResolver, EMERGENCY_GESTURE, value) } + .getOrDefault(false) + if (direct) return remember(true) + if (!dev.equwal.assistkey.shell.Shell.ready) return remember(false) + dev.equwal.assistkey.shell.Shell.run("settings put secure $EMERGENCY_GESTURE $value") { remember(it.ok) } + } + fun canWriteSecure(c: Context): Boolean = c.checkSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) == PackageManager.PERMISSION_GRANTED diff --git a/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt index e231be6..5656406 100644 --- a/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt +++ b/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt @@ -59,12 +59,11 @@ class ActionPickerActivity : Activity() { navigation(col) if (dev.equwal.assistkey.shell.Shell.SUPPORTED) { col.header("Light") - listOf( - "Extra-dim: darker" to "darker", - "Extra-dim: brighter" to "brighter", - "Extra-dim: on and off" to "toggle" - ).forEach { (label, what) -> - col.row(label, null) { choose(ActionSpec(ActionKind.DIM, what, label)) } + listOf("toggle", "darker", "brighter").forEach { what -> + val spec = ActionSpec(ActionKind.DIM, what, "") + col.row(spec.describe(), if (what == "toggle") "On is the lowest level. Off is your system brightness." else null) { + choose(spec) + } } } col.header("Typing") diff --git a/app/src/main/java/dev/equwal/assistkey/ui/DisplayActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/DisplayActivity.kt index 2f4a3c6..f7062ef 100644 --- a/app/src/main/java/dev/equwal/assistkey/ui/DisplayActivity.kt +++ b/app/src/main/java/dev/equwal/assistkey/ui/DisplayActivity.kt @@ -56,7 +56,7 @@ class DisplayActivity : Activity() { "put back whenever the screen comes on. If this device does not let " + "the shell reach the light, choosing a level will say so." ) - col.note("To change the level from a key, bind one of the Light actions: darker, brighter, or on and off.") + col.note("To change the level from a key, bind a Light action: Extra dim toggle, darker or brighter. The toggle goes to the lowest level, and back to your system brightness.") if (Device.brightnessFloor == null) { col.note("This device's floor has not been measured, so these values are a guess.") } diff --git a/app/src/main/java/dev/equwal/assistkey/ui/PowerActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/PowerActivity.kt index 857decd..8a17205 100644 --- a/app/src/main/java/dev/equwal/assistkey/ui/PowerActivity.kt +++ b/app/src/main/java/dev/equwal/assistkey/ui/PowerActivity.kt @@ -57,6 +57,27 @@ class PowerActivity : Activity() { col.title("Power button") val direct = Shell.ready && PowerControl.wanted(this) if (direct) direct(col) else sideDoors(col) + emergencySos(col) + } + + /** + * Android starts Emergency SOS on five quick Power presses. That collides + * with a 5-tap binding, and with fast tapping in general. It is a safety + * feature, so the app never turns it off by itself: this is the user's switch. + */ + private fun emergencySos(col: LinearLayout) { + if (!Shell.ready && !PowerNative.canWriteSecure(this)) return + col.header("Emergency SOS") + col.check( + "System Emergency SOS on five presses", + "A safety feature of Android. Untick it if five quick presses start it by accident, or to use 5 taps for an action.", + PowerNative.emergencySos(this) + ) { on -> + PowerNative.setEmergencySos(this, on) { ok -> + if (!ok) Toast.makeText(this, "The system refused", Toast.LENGTH_LONG).show() + build() + } + } } private fun bindRow(col: LinearLayout, label: String, t: Trigger, enabled: Boolean = true) { diff --git a/app/src/test/java/dev/equwal/assistkey/display/ExtraDimStepTest.kt b/app/src/test/java/dev/equwal/assistkey/display/ExtraDimStepTest.kt index 7d95cd8..53fdba1 100644 --- a/app/src/test/java/dev/equwal/assistkey/display/ExtraDimStepTest.kt +++ b/app/src/test/java/dev/equwal/assistkey/display/ExtraDimStepTest.kt @@ -23,7 +23,17 @@ class ExtraDimStepTest { listOf(4, 3, 2).forEach { assertEquals(it, ExtraDim.brighter(ExtraDim.darker(it, levels), levels)) } } + // Regression: "on" used the last level in use (or a middle level), not the lowest. + @Test fun `on and off - on is the lowest level, from off`() { + assertEquals(1, ExtraDim.toggled(0, levels)) + } + + @Test fun `on and off - off is the system brightness, from any level`() { + levels.forEach { assertEquals(0, ExtraDim.toggled(it, levels)) } + } + @Test fun `a device with no levels stays off`() { + assertEquals(0, ExtraDim.toggled(0, emptyList())) assertEquals(0, ExtraDim.darker(0, emptyList())) assertEquals(0, ExtraDim.brighter(0, emptyList())) } diff --git a/app/src/test/java/dev/equwal/assistkey/model/ActionSpecNameTest.kt b/app/src/test/java/dev/equwal/assistkey/model/ActionSpecNameTest.kt new file mode 100644 index 0000000..cf334b0 --- /dev/null +++ b/app/src/test/java/dev/equwal/assistkey/model/ActionSpecNameTest.kt @@ -0,0 +1,22 @@ +package dev.equwal.assistkey.model + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ActionSpecNameTest { + + @Test fun `a light action is named from its payload`() { + assertEquals("Extra dim toggle", ActionSpec(ActionKind.DIM, "toggle", "").describe()) + assertEquals("Extra dim darker", ActionSpec(ActionKind.DIM, "darker", "").describe()) + assertEquals("Extra dim brighter", ActionSpec(ActionKind.DIM, "brighter", "").describe()) + } + + // A binding made by 0.0.6 stored the old label. It must show the new name. + @Test fun `an old stored label does not win over the current name`() { + assertEquals("Extra dim toggle", ActionSpec(ActionKind.DIM, "toggle", "Extra-dim: on and off").describe()) + } + + @Test fun `other actions keep their stored label`() { + assertEquals("Back", ActionSpec(ActionKind.GLOBAL, "BACK", "Back").describe()) + } +} diff --git a/gradle.properties b/gradle.properties index 77ad451..608e888 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,8 +6,8 @@ android.nonTransitiveRClass=true kotlin.code.style=official # Release identity. versionCode must rise with every upload to Play. -assistkey.versionName=0.0.6-alpha -assistkey.versionCode=6 +assistkey.versionName=0.0.7-alpha +assistkey.versionCode=7 # The free beta ends on this date (UTC) at the latest. It is the backstop for # installs that cannot reach Google Play; the real switch is the