fix(screensaver): start dim, lock on time and keep room to drift
Both hosts start at night-mode brightness and only brighten once the settings say so, and the screen stays black until the settings and the appearance are both read, so a dark room is never lit by a loading frame. The on-demand screen saver lets the phone lock on its usual timeout and stays up over the lock screen, so a phone left charging is not left unlocked. The digital readout takes 70% of the shorter side, leaving room to drift across it. The next alarm is one localized date-time skeleton instead of a hand-joined day and time, the date uses floret's localizedDateFormatter, and the 12/24-hour pattern is shared with rememberLocalTimeFormatter.
This commit is contained in:
1 parent
d744fa8d98
commit
23f2ee94ea
5 files changed
+64
-45
No files matched your search
@@ -7,8 +7,11 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import de.jeanlucmakiola.clockula.domain.TimeOfDay
|
||||
import java.time.LocalTime
|
||||
import java.time.chrono.IsoChronology
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeFormatterBuilder
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Honours the user's 12/24-hour setting. Deliberately not unit-tested: it is
|
||||
@@ -24,19 +27,27 @@ internal fun rememberLocalTimeFormatter(): (LocalTime) -> String {
|
||||
// choice rides the same configuration, so both are re-read with it.
|
||||
val configuration = LocalConfiguration.current
|
||||
val formatter = remember(configuration) {
|
||||
// The device's 12/24-hour choice, then the locale's own short form —
|
||||
// the same two-step the ring notification already formats with.
|
||||
if (DateFormat.is24HourFormat(context)) {
|
||||
DateTimeFormatter.ofPattern("HH:mm")
|
||||
} else {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
}
|
||||
val locale = Locale.getDefault(Locale.Category.FORMAT)
|
||||
DateTimeFormatter.ofPattern(localTimePattern(DateFormat.is24HourFormat(context), locale), locale)
|
||||
}
|
||||
return remember(formatter) {
|
||||
{ time -> formatter.format(time) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The rule itself, as a pattern: the device's 12/24-hour choice, then the
|
||||
* locale's own short form — the same two-step the ring notification already
|
||||
* formats with. A pattern rather than a formatter for the screen saver, which
|
||||
* splits it around the AM/PM marker to draw that smaller.
|
||||
*/
|
||||
internal fun localTimePattern(is24Hour: Boolean, locale: Locale): String =
|
||||
if (is24Hour) {
|
||||
"HH:mm"
|
||||
} else {
|
||||
DateTimeFormatterBuilder.getLocalizedDateTimePattern(null, FormatStyle.SHORT, IsoChronology.INSTANCE, locale)
|
||||
}
|
||||
|
||||
/** Delegates to [rememberLocalTimeFormatter]. Unchanged for every existing caller. */
|
||||
@Composable
|
||||
internal fun rememberAlarmTimeFormatter(): (TimeOfDay) -> String {
|
||||
|
||||
@@ -30,6 +30,8 @@ class ClockulaDreamService : DreamService() {
|
||||
super.onAttachedToWindow()
|
||||
isInteractive = false
|
||||
isFullscreen = true
|
||||
// Dim until the settings say otherwise (ScreenSaverRoot).
|
||||
isScreenBright = false
|
||||
|
||||
val owner = DreamViewOwner().also { owner = it }
|
||||
owner.create(window.decorView)
|
||||
|
||||
@@ -25,7 +25,12 @@ import javax.inject.Inject
|
||||
* Any touch closes it, as it would end a dream. It keeps the screen on only
|
||||
* while the phone is charging: on battery the display times out as it
|
||||
* normally would, so a screen saver left open in a bag does not drain it.
|
||||
* Night mode turns the window's own brightness down to almost nothing.
|
||||
* Kept on or not, the phone still locks on its usual timeout
|
||||
* (`FLAG_ALLOW_LOCK_WHILE_SCREEN_ON`, as DeskClock's screen saver does) and
|
||||
* the screen saver carries on over the lock screen, so a phone left on the
|
||||
* charger overnight is not left unlocked; a touch then lands on the keyguard.
|
||||
* Night mode turns the window's own brightness down to almost nothing; the
|
||||
* window starts that way, until the settings are known.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ScreenSaverActivity : ComponentActivity() {
|
||||
@@ -38,6 +43,9 @@ class ScreenSaverActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setShowWhenLocked(true)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
|
||||
applyNightBrightness(true)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
hide(WindowInsetsCompat.Type.systemBars())
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.jeanlucmakiola.clockula.ui.screensaver
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
@@ -44,15 +43,14 @@ import de.jeanlucmakiola.clockula.domain.screensaver.ScreenSaverDrift
|
||||
import de.jeanlucmakiola.clockula.domain.screensaver.ScreenSaverStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.NextAlarmWidgetModel
|
||||
import de.jeanlucmakiola.clockula.domain.worldclock.DayNight
|
||||
import de.jeanlucmakiola.clockula.ui.common.localTimePattern
|
||||
import de.jeanlucmakiola.clockula.ui.worldclock.WorldClockFace
|
||||
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
||||
import de.jeanlucmakiola.floret.locale.currentLocale
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.chrono.IsoChronology
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeFormatterBuilder
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -131,10 +129,12 @@ private fun Clock(state: ScreenSaverUiState, side: Dp) {
|
||||
private fun DigitalTime(time: LocalTime, use24Hour: Boolean, locale: Locale, side: Dp) {
|
||||
val parts = remember(time, use24Hour, locale) { timeParts(time, use24Hour, locale) }
|
||||
// About 0.6em per tabular digit or colon, the marker at its own smaller
|
||||
// size, so the readout fills ~90 % of the shorter side at any length.
|
||||
// size, so the readout spans DIGITS_FRACTION of the shorter side at any
|
||||
// length — never all of it, or upright on a phone there would be no room
|
||||
// left across the screen to drift into.
|
||||
val ems = (parts.before.length + parts.after.length + parts.marker.length * MARKER_SCALE)
|
||||
.coerceAtLeast(4f) * 0.6f
|
||||
val size = minOf(side * 0.9f / ems, side * 0.4f)
|
||||
val size = minOf(side * DIGITS_FRACTION / ems, side * 0.32f)
|
||||
val fontSize = with(LocalDensity.current) { size.toSp() }
|
||||
val text = buildAnnotatedString {
|
||||
append(parts.before)
|
||||
@@ -203,27 +203,18 @@ private fun NextAlarm(model: NextAlarmWidgetModel, now: LocalDateTime, use24Hour
|
||||
/** The AM/PM marker's size against the digits'. */
|
||||
private const val MARKER_SCALE = 0.3f
|
||||
|
||||
/**
|
||||
* The app's one 12/24-hour rule, as `rememberLocalTimeFormatter` applies it:
|
||||
* `HH:mm`, or the locale's own short time. Spelled as a pattern here because
|
||||
* the numerals need it split around the AM/PM marker; the same pattern, so the
|
||||
* screen saver never writes a time the alarm list writes differently.
|
||||
*/
|
||||
private fun timePattern(use24Hour: Boolean, locale: Locale): String =
|
||||
if (use24Hour) {
|
||||
"HH:mm"
|
||||
} else {
|
||||
DateTimeFormatterBuilder.getLocalizedDateTimePattern(null, FormatStyle.SHORT, IsoChronology.INSTANCE, locale)
|
||||
}
|
||||
/** The digital readout's share of the screen's shorter side. */
|
||||
private const val DIGITS_FRACTION = 0.7f
|
||||
|
||||
/** The app's one 12/24-hour rule ([localTimePattern]), so the screen saver never writes a time the alarm list writes differently. */
|
||||
private fun timeFormatter(use24Hour: Boolean, locale: Locale): DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern(timePattern(use24Hour, locale), locale)
|
||||
DateTimeFormatter.ofPattern(localTimePattern(use24Hour, locale), locale)
|
||||
|
||||
/** A time split around its AM/PM marker; [marker] is empty in 24-hour time. */
|
||||
private data class TimeParts(val before: String, val marker: String, val after: String)
|
||||
|
||||
private fun timeParts(time: LocalTime, use24Hour: Boolean, locale: Locale): TimeParts {
|
||||
val pattern = timePattern(use24Hour, locale)
|
||||
val pattern = localTimePattern(use24Hour, locale)
|
||||
val markerAt = pattern.indexOf('a')
|
||||
if (markerAt < 0) return TimeParts(format(time, pattern, locale), "", "")
|
||||
return runCatching {
|
||||
@@ -240,20 +231,20 @@ private fun format(time: LocalTime, pattern: String, locale: Locale): String =
|
||||
|
||||
/** The locale's own full weekday-and-date form, "Thursday, October 22". */
|
||||
private fun dateLabel(now: LocalDateTime, locale: Locale): String =
|
||||
DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(locale, "EEEEMMMMd"), locale).format(now)
|
||||
localizedDateFormatter(locale, "EEEEMMMMd").format(now)
|
||||
|
||||
/**
|
||||
* The next alarm as the platform's own screen saver writes it: the short
|
||||
* weekday and the time, "Tue 7:00 AM". The weekday is always there, so a
|
||||
* time alone can never be read as today's, and an alarm a week or more away
|
||||
* carries its date too ([ScreenSaverAlarmDay]), "Thu, Oct 29 7:00 AM".
|
||||
* weekday and the time, "Tue 7:00 AM", laid out in the locale's own order.
|
||||
* The weekday is always there, so a time alone can never be read as today's,
|
||||
* and an alarm a week or more away carries its date too
|
||||
* ([ScreenSaverAlarmDay]), "Thu, Oct 29, 7:00 AM".
|
||||
*/
|
||||
private fun nextAlarmLabel(model: NextAlarmWidgetModel, now: LocalDateTime, use24Hour: Boolean, locale: Locale): String {
|
||||
val date = model.date ?: now.toLocalDate()
|
||||
val daySkeleton = if (ScreenSaverAlarmDay.weekdayIsEnough(date, now.toLocalDate())) "EEE" else "EEEMMMd"
|
||||
val day = DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(locale, daySkeleton), locale).format(date)
|
||||
val time = timeFormatter(use24Hour, locale).format(LocalTime.of(model.hour, model.minute))
|
||||
return "$day $time"
|
||||
val date = model.date ?: return ""
|
||||
val day = if (ScreenSaverAlarmDay.weekdayIsEnough(date, now.toLocalDate())) "EEE" else "EEEMMMd"
|
||||
val time = if (use24Hour) "HHmm" else "hmma"
|
||||
return localizedDateFormatter(locale, day + time).format(date.atTime(model.hour, model.minute))
|
||||
}
|
||||
|
||||
private object ScreenSaverDefaults {
|
||||
|
||||
@@ -19,18 +19,25 @@ import de.jeanlucmakiola.floret.prefs.Appearance
|
||||
* a backgrounded activity stops waking for the minute as well.
|
||||
*
|
||||
* [onNightModeChanged] hands the night-mode choice back to the host, which
|
||||
* owns the window and so the display's brightness.
|
||||
* owns the window and so the display's brightness — only once it is known:
|
||||
* both hosts start dim, so a night-mode screen saver never lights a dark room
|
||||
* for the moment its settings take to load. Until the settings and the
|
||||
* appearance have both arrived the screen stays black, rather than showing a
|
||||
* frame in colours that are about to change.
|
||||
*/
|
||||
@Composable
|
||||
internal fun ScreenSaverRoot(source: ScreenSaverSource, onNightModeChanged: (Boolean) -> Unit) {
|
||||
val appearance by source.appearance.collectAsStateWithLifecycle(initialValue = Appearance())
|
||||
val appearance by source.appearance.collectAsStateWithLifecycle<Appearance?>(initialValue = null)
|
||||
val state by remember(source) { source.state() }.collectAsStateWithLifecycle(initialValue = null)
|
||||
val nightMode = state?.settings?.nightMode ?: false
|
||||
LaunchedEffect(nightMode) { onNightModeChanged(nightMode) }
|
||||
val nightMode = state?.settings?.nightMode
|
||||
LaunchedEffect(nightMode) { nightMode?.let(onNightModeChanged) }
|
||||
|
||||
ScreenSaverTheme(nightMode = nightMode, dynamicColor = appearance.dynamicColor) {
|
||||
// Black until the first state lands, rather than a frame of anything else.
|
||||
state?.let { ScreenSaverContent(it) }
|
||||
?: Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background))
|
||||
ScreenSaverTheme(nightMode = nightMode ?: true, dynamicColor = appearance?.dynamicColor ?: false) {
|
||||
val ready = state
|
||||
if (ready != null && appearance != null) {
|
||||
ScreenSaverContent(ready)
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user