Skip to content

Commit 84d3092

Browse files
committed
fix(ui): prevent aggressive brightness override on OLED panels
Replace fixed brightness threshold/target with a relative boost strategy that adds 0.3 to the current brightness (capped at 0.6) when the screen is genuinely near-off (<0.1). Tie keep-screen-on and brightness to the bill's lifecycle rather than a fixed 10s timeout, and properly restore on backgrounding via lifecycle observer. Guard against capturing boosted brightness as the original value on re-entry. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent ba33780 commit 84d3092

1 file changed

Lines changed: 86 additions & 42 deletions

File tree

ui/components/src/main/kotlin/com/getcode/ui/utils/KeepScreenOn.kt

Lines changed: 86 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,117 @@ import android.app.Activity
44
import android.provider.Settings
55
import android.view.WindowManager
66
import androidx.compose.runtime.Composable
7-
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.runtime.DisposableEffect
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableFloatStateOf
10+
import androidx.compose.runtime.mutableStateOf
11+
import androidx.compose.runtime.remember
12+
import androidx.compose.runtime.rememberUpdatedState
13+
import androidx.compose.runtime.setValue
814
import androidx.compose.ui.platform.LocalContext
9-
import kotlinx.coroutines.delay
15+
import androidx.compose.ui.platform.LocalLifecycleOwner
16+
import androidx.lifecycle.Lifecycle
17+
import androidx.lifecycle.LifecycleEventObserver
18+
import kotlin.math.min
1019

1120
/**
12-
* A composable function that keeps the screen on for a specified duration when enabled.
13-
* It can also optionally increase the screen brightness to a target level if it's below a minimum threshold.
21+
* A composable function that keeps the screen on while enabled and optionally adjusts brightness.
1422
*
1523
* This is useful for scenarios like displaying a QR code where the user needs the screen to
1624
* stay on and be bright enough to be scanned.
1725
*
18-
* The effect is launched whenever the `isEnabled` parameter changes to `true`. After the specified
19-
* `timeoutMs`, the screen-on flag and any brightness changes are reverted.
26+
* The keep-screen-on flag and brightness adjustment remain active for the entire duration that
27+
* [isEnabled] is `true` and the app is in the foreground. When [isEnabled] flips to `false`,
28+
* the composable leaves composition, or the app is backgrounded, the flag is cleared and
29+
* brightness is restored. Returning to the foreground re-applies the overrides.
30+
*
31+
* When [useBrightness] is enabled and the screen is very dim (below [minBrightness]), brightness
32+
* is boosted by [brightnessBoost] relative to the current level (capped at [maxBrightness]).
33+
* This avoids jarring jumps on OLED panels with non-linear brightness curves.
2034
*
2135
* @param isEnabled A boolean flag to enable or disable the keep-screen-on functionality.
22-
* @param timeoutMs The duration in milliseconds for which to keep the screen on. Defaults to 10 seconds.
2336
* @param useBrightness A boolean flag to control whether the screen brightness should be adjusted.
24-
* @param minBrightness The minimum brightness threshold. If the current brightness is below this value
25-
* (or is set to automatic), the brightness will be adjusted. Brightness is a value from 0.0 to 1.0.
26-
* @param targetBrightness The brightness level to set the screen to if adjustment is needed. Brightness is a value from 0.0 to 1.0.
37+
* @param minBrightness The minimum brightness threshold below which a boost is applied. 0.0 to 1.0.
38+
* @param brightnessBoost The amount to add to current brightness when boosting. 0.0 to 1.0.
39+
* @param maxBrightness The maximum brightness cap when boosting. 0.0 to 1.0.
2740
*/
2841
@Composable
2942
fun KeepScreenOn(
3043
isEnabled: Boolean,
31-
timeoutMs: Long = 10_000L,
3244
useBrightness: Boolean = false,
33-
minBrightness: Float = 0.4f,
34-
targetBrightness: Float = 0.6f
45+
minBrightness: Float = 0.1f,
46+
brightnessBoost: Float = 0.3f,
47+
maxBrightness: Float = 0.8f
3548
) {
3649
val context = LocalContext.current
50+
val lifecycleOwner = LocalLifecycleOwner.current
51+
52+
val currentIsEnabled by rememberUpdatedState(isEnabled)
53+
var originalBrightness by remember { mutableFloatStateOf(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) }
54+
var brightnessAdjusted by remember { mutableStateOf(false) }
3755

38-
LaunchedEffect(isEnabled) {
39-
if (!isEnabled) return@LaunchedEffect
56+
DisposableEffect(isEnabled, lifecycleOwner) {
57+
if (!isEnabled) return@DisposableEffect onDispose { }
4058

4159
val window = (context as Activity).window
42-
val layoutParams = window.attributes
43-
val originalBrightness = layoutParams.screenBrightness
4460

45-
// When brightness is system-managed (adaptive brightness), the window
46-
// reports BRIGHTNESS_OVERRIDE_NONE (-1.0). We must query the actual
47-
// system brightness rather than treating -1.0 as "below threshold" —
48-
// otherwise we unconditionally override to targetBrightness which on
49-
// modern Pixel panels with non-linear curves appears as near-max.
50-
val effectiveBrightness = if (originalBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) {
51-
Settings.System.getInt(
52-
context.contentResolver,
53-
Settings.System.SCREEN_BRIGHTNESS,
54-
128 // ~50% fallback if unreadable
55-
) / 255f
56-
} else {
57-
originalBrightness
61+
fun clearOverrides() {
62+
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
63+
if (brightnessAdjusted) {
64+
val layoutParams = window.attributes
65+
layoutParams.screenBrightness = originalBrightness
66+
window.attributes = layoutParams
67+
brightnessAdjusted = false
68+
}
5869
}
5970

60-
if (useBrightness && effectiveBrightness < minBrightness) {
61-
layoutParams.screenBrightness = targetBrightness
62-
window.attributes = layoutParams
63-
}
71+
fun applyOverrides() {
72+
if (!currentIsEnabled) {
73+
clearOverrides()
74+
return
75+
}
6476

65-
try {
66-
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
67-
delay(timeoutMs)
68-
} finally {
69-
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
70-
if (useBrightness) {
71-
layoutParams.screenBrightness = originalBrightness
77+
val layoutParams = window.attributes
78+
79+
// Only capture original brightness on the first apply — not on
80+
// re-apply from ON_RESUME — to avoid saving the boosted value.
81+
if (!brightnessAdjusted) {
82+
originalBrightness = layoutParams.screenBrightness
83+
}
84+
85+
val effectiveBrightness = if (originalBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) {
86+
Settings.System.getInt(
87+
context.contentResolver,
88+
Settings.System.SCREEN_BRIGHTNESS,
89+
128
90+
) / 255f
91+
} else {
92+
originalBrightness
93+
}
94+
95+
brightnessAdjusted = useBrightness && effectiveBrightness < minBrightness
96+
if (brightnessAdjusted) {
97+
layoutParams.screenBrightness = min(effectiveBrightness + brightnessBoost, maxBrightness)
7298
window.attributes = layoutParams
7399
}
100+
101+
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
102+
}
103+
104+
applyOverrides()
105+
106+
val observer = LifecycleEventObserver { _, event ->
107+
when (event) {
108+
Lifecycle.Event.ON_PAUSE -> clearOverrides()
109+
Lifecycle.Event.ON_RESUME -> applyOverrides()
110+
else -> Unit
111+
}
112+
}
113+
lifecycleOwner.lifecycle.addObserver(observer)
114+
115+
onDispose {
116+
lifecycleOwner.lifecycle.removeObserver(observer)
117+
clearOverrides()
74118
}
75119
}
76-
}
120+
}

0 commit comments

Comments
 (0)