fix(screensaver): keep both screen saver activities out of the app's task
The settings screen Android's screen saver list opens gets a task of its own and stays out of recents, so it never lands in or clears the app's task, and its bars follow the app's theme. The on-demand screen saver is noHistory, so leaving it by Home does not bring the app back to it later, and it only touches the keep-screen-on flag when charging starts or stops. The next-alarm line uses the same 12/24-hour rule as the big readout, joined to the day by a translatable string; the minute's crossfade shows the old minute fading out rather than two copies of the new one.
This commit is contained in:
@@ -295,18 +295,25 @@
|
||||
<!-- The screen saver's settings, opened by the gear beside it in the
|
||||
system's screen saver list. Exported because only an exported
|
||||
activity can be started by the Settings app; it reads no extras
|
||||
and changes nothing a user could not change in-app. -->
|
||||
and changes nothing a user could not change in-app. A task of its
|
||||
own and out of recents, so opening it from Settings never lands
|
||||
in, or clears, the app's own task. -->
|
||||
<activity
|
||||
android:name=".ui.screensaver.ScreenSaverSettingsActivity"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:label="@string/screen_saver_title"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/Theme.Clockula" />
|
||||
|
||||
<!-- The same screen saver on demand, from the tabs' overflow menu: an
|
||||
app cannot start the system's dream itself. Not exported. -->
|
||||
app cannot start the system's dream itself. Not exported, and
|
||||
no history: leaving it by Home or Recents finishes it, so the app
|
||||
never comes back to a dim screen saver instead of its tabs. -->
|
||||
<activity
|
||||
android:name=".ui.screensaver.ScreenSaverActivity"
|
||||
android:exported="false"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/Theme.Clockula.ScreenSaver" />
|
||||
|
||||
<!-- Persists the per-app language on API < 33, where the platform
|
||||
|
||||
@@ -41,6 +41,9 @@ class ScreenSaverActivity : ComponentActivity() {
|
||||
override fun onReceive(context: Context, intent: Intent) = applyKeepScreenOn(intent)
|
||||
}
|
||||
|
||||
/** The flag last applied; null until the first battery state. Changing a window flag costs a relayout, and the battery reports far more often than the plug changes. */
|
||||
private var keepingScreenOn: Boolean? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setShowWhenLocked(true)
|
||||
@@ -79,6 +82,8 @@ class ScreenSaverActivity : ComponentActivity() {
|
||||
|
||||
private fun applyKeepScreenOn(battery: Intent) {
|
||||
val charging = battery.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
|
||||
if (charging == keepingScreenOn) return
|
||||
keepingScreenOn = charging
|
||||
if (charging) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
|
||||
@@ -59,7 +59,8 @@ import kotlin.math.roundToInt
|
||||
* world clock's dial — the date and the next alarm, on black, at a spot that
|
||||
* moves once a minute ([ScreenSaverDrift]) so nothing stays lit in one place.
|
||||
* The move is a slow crossfade rather than a slide, as DeskClock's is, and
|
||||
* a cut under reduce motion.
|
||||
* a cut under reduce motion: the old minute fades out where it was as the
|
||||
* new one fades in at its new spot.
|
||||
*/
|
||||
@Composable
|
||||
internal fun ScreenSaverContent(state: ScreenSaverUiState, modifier: Modifier = Modifier) {
|
||||
@@ -75,11 +76,11 @@ internal fun ScreenSaverContent(state: ScreenSaverUiState, modifier: Modifier =
|
||||
// landscape on a bedside dock as it is upright in the hand.
|
||||
val side = minOf(maxWidth, maxHeight)
|
||||
Crossfade(
|
||||
targetState = state.position,
|
||||
targetState = state,
|
||||
animationSpec = if (reduceMotion) snap() else tween(ScreenSaverDefaults.MOVE_MILLIS),
|
||||
label = "drift",
|
||||
) { position ->
|
||||
Drifting(position) { Clock(state, side) }
|
||||
) { shown ->
|
||||
Drifting(shown.position) { Clock(shown, side) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +180,8 @@ private fun AnalogTime(time: LocalTime, use24Hour: Boolean, locale: Locale, side
|
||||
|
||||
@Composable
|
||||
private fun NextAlarm(model: NextAlarmWidgetModel, now: LocalDateTime, use24Hour: Boolean, locale: Locale) {
|
||||
val label = remember(model, now.toLocalDate(), use24Hour, locale) { nextAlarmLabel(model, now, use24Hour, locale) }
|
||||
val (day, time) = remember(model, now.toLocalDate(), use24Hour, locale) { nextAlarmParts(model, now, use24Hour, locale) }
|
||||
val label = stringResource(R.string.screen_saver_next_alarm_when, day, time)
|
||||
val description = stringResource(R.string.screen_saver_next_alarm, label)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -235,16 +237,17 @@ private fun dateLabel(now: LocalDateTime, locale: Locale): String =
|
||||
|
||||
/**
|
||||
* The next alarm as the platform's own screen saver writes it: the short
|
||||
* 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".
|
||||
* 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". The
|
||||
* time follows the app's one 12/24-hour rule, like the big readout above it;
|
||||
* the two are joined by a translatable string, so a language can reorder them.
|
||||
*/
|
||||
private fun nextAlarmLabel(model: NextAlarmWidgetModel, now: LocalDateTime, use24Hour: Boolean, locale: Locale): String {
|
||||
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 fun nextAlarmParts(model: NextAlarmWidgetModel, now: LocalDateTime, use24Hour: Boolean, locale: Locale): Pair<String, String> {
|
||||
val date = model.date ?: now.toLocalDate()
|
||||
val skeleton = if (ScreenSaverAlarmDay.weekdayIsEnough(date, now.toLocalDate())) "EEE" else "EEEMMMd"
|
||||
return localizedDateFormatter(locale, skeleton).format(date) to
|
||||
timeFormatter(use24Hour, locale).format(LocalTime.of(model.hour, model.minute))
|
||||
}
|
||||
|
||||
private object ScreenSaverDefaults {
|
||||
|
||||
+13
-5
@@ -1,10 +1,13 @@
|
||||
package de.jeanlucmakiola.clockula.ui.screensaver
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -33,13 +36,18 @@ class ScreenSaverSettingsActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
val appearance by settingsPrefs.appearance.collectAsStateWithLifecycle(initialValue = Appearance())
|
||||
ClockulaTheme(
|
||||
darkTheme = appearance.themeMode.isDark(isSystemInDarkTheme()),
|
||||
dynamicColor = appearance.dynamicColor,
|
||||
) {
|
||||
val darkTheme = appearance.themeMode.isDark(isSystemInDarkTheme())
|
||||
// The bars follow the app's resolved theme, not the device's
|
||||
// night flag, as MainActivity's do.
|
||||
LaunchedEffect(darkTheme) {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT) { darkTheme },
|
||||
navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT) { darkTheme },
|
||||
)
|
||||
}
|
||||
ClockulaTheme(darkTheme = darkTheme, dynamicColor = appearance.dynamicColor) {
|
||||
CollapsingScaffold(title = stringResource(R.string.screen_saver_title), onBack = ::finish) {
|
||||
ScreenSaverSettingsSection(inApp = false)
|
||||
}
|
||||
|
||||
@@ -315,6 +315,8 @@
|
||||
<string name="screen_saver_system_settings_hint">Pick Clockula as the screen saver in Android\'s settings</string>
|
||||
<!-- Read by a screen reader for the alarm line on the screen saver; %1$s is the day and time, "Tue 7:00 AM". -->
|
||||
<string name="screen_saver_next_alarm">Next alarm: %1$s</string>
|
||||
<!-- The screen saver's next-alarm line: %1$s is the day ("Tue", or "Thu, Oct 29" a week or more away), %2$s the time ("7:00 AM"). -->
|
||||
<string name="screen_saver_next_alarm_when">%1$s %2$s</string>
|
||||
<string name="menu_more_options">More options</string>
|
||||
<string name="settings_self_check">Why might my alarm not ring?</string>
|
||||
<string name="settings_self_check_title">Why might my alarm not ring?</string>
|
||||
|
||||
@@ -98,7 +98,7 @@ kit module: the kit's roadmap keeps schedulers app-local.
|
||||
| `domain/format/` | `ClockFormat` (`M:SS` / `H:MM:SS`, countdowns rounded up, elapsed truncated), `NextFire` (`NextFireLabel` + `NextFireFormat`), the alarm row's "in 9h 12m" as data, and `StopwatchFormat`, the hundredths readout, split into a major field and two digits so the fraction can be drawn smaller |
|
||||
| `domain/backup/` | `BackupContent` (M10): the plain Kotlin model between `data/backup`'s JSON codec and the repository/engine; decoupled from the wire DTOs the same way the data layer is decoupled from Room |
|
||||
| `domain/selfcheck/` | `SelfCheck` (M10): the "why might my alarm not ring?" rule table: pure functions over a `DeviceState` snapshot, each producing a `CheckResult` with a status and an optional deep-link `SelfCheckFix` |
|
||||
| `domain/screensaver/` | the screen saver's pure half (issue #25): `ScreenSaverSettings` (style and night mode) and `ScreenSaverDrift`, the once-a-minute burn-in position as fractions of the free space, derived from the minute alone |
|
||||
| `domain/screensaver/` | the screen saver's pure half (issue #25): `ScreenSaverSettings` (style and night mode), `ScreenSaverDrift`, the once-a-minute burn-in position as fractions of the free space, derived from the minute alone, and `ScreenSaverAlarmDay`, which dates a next alarm a week or more away rather than naming its weekday alone |
|
||||
| `domain/widget/` | the three widget presenters (M10), `NextAlarmWidgetModel`/`TimerWidgetModel`/`ClockWidgetModel`: selection and formatting only, so a widget's content is unit-tested without Glance |
|
||||
| `data/db/` | `ClockulaDatabase` (`@Database` v3, `exportSchema = true`) and `Migrations` |
|
||||
| `data/alarms/` | the alarm and ring-state entities, DAOs, mappers and repositories |
|
||||
|
||||
Reference in New Issue
Block a user