fix(tiles): redraw on clock and zone changes, and tighten the tile surfaces
The tiles' refresh now rides WidgetSync's collectors instead of a second copy of them, and SystemEventReceiver asks both tiles to redraw on a time or zone change, where the times they show move without any write. The quick-timer tile remembers its timer before starting it, so a failed start can't leave a timer it cannot see, and shows the timer's stored end rather than one re-derived from two clocks on every read. The tile preferences screen closes on anything but QS_TILE_PREFERENCES, runs in its own task, and asks for a redraw after saving a duration. The tiles and the next-alarm widget share one 12/24-hour time formatter and the widgets' launch intent.
This commit is contained in:
1 parent
da9854abf8
commit
beeca29f74
14 files changed
+99
-127
No files matched your search
@@ -444,7 +444,7 @@
|
||||
for System UI to bind it, and BIND_QUICK_SETTINGS_TILE — a
|
||||
signature permission only System UI holds — keeps anyone else
|
||||
from binding it. Both are active tiles: they are redrawn only
|
||||
when the app asks (`tile/TileSync.kt`), never on every panel
|
||||
when the app asks (`widget/WidgetSync.kt`), never on every panel
|
||||
open. -->
|
||||
<service
|
||||
android:name=".tile.NextAlarmTileService"
|
||||
@@ -478,12 +478,14 @@
|
||||
|
||||
<!-- What long-pressing either tile opens: the quick-timer tile's
|
||||
duration, or the Alarms tab for the next-alarm one. Exported
|
||||
because System UI starts it, and guarded to that one action, as
|
||||
the widget configurators are to theirs. -->
|
||||
because System UI starts it; it answers that one action and
|
||||
closes on any other. A task of its own, so finishing it returns
|
||||
to wherever the user was, not into an open Clockula. -->
|
||||
<activity
|
||||
android:name=".ui.tile.TilePreferencesActivity"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/Theme.Clockula">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
|
||||
@@ -6,7 +6,6 @@ import de.jeanlucmakiola.clockula.alarm.AlarmEngine
|
||||
import de.jeanlucmakiola.clockula.alarm.ring.RingNotifications
|
||||
import de.jeanlucmakiola.clockula.stopwatch.StopwatchEngine
|
||||
import de.jeanlucmakiola.clockula.stopwatch.service.StopwatchNotifications
|
||||
import de.jeanlucmakiola.clockula.tile.TileSync
|
||||
import de.jeanlucmakiola.clockula.timer.TimerEngine
|
||||
import de.jeanlucmakiola.clockula.timer.service.TimerNotifications
|
||||
import de.jeanlucmakiola.clockula.widget.WidgetSync
|
||||
@@ -42,9 +41,6 @@ class ClockulaApp : Application() {
|
||||
@Inject
|
||||
lateinit var widgetSync: WidgetSync
|
||||
|
||||
@Inject
|
||||
lateinit var tileSync: TileSync
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var scope: CoroutineScope
|
||||
@@ -86,10 +82,9 @@ class ClockulaApp : Application() {
|
||||
stopwatchEngine.onBootCompleted()
|
||||
}
|
||||
|
||||
// Never completes on its own — it is the widgets' one refresh trigger
|
||||
// for as long as the process lives (M10, docs/PLAN.md §9).
|
||||
// Never completes on its own — it is the widgets' and the Quick
|
||||
// Settings tiles' one refresh trigger for as long as the process
|
||||
// lives (M10, docs/PLAN.md §9; #26).
|
||||
scope.launch { widgetSync.run() }
|
||||
// And the Quick Settings tiles' (#26), the same way.
|
||||
scope.launch { tileSync.run() }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import androidx.glance.appwidget.updateAll
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.jeanlucmakiola.clockula.alarm.AlarmEngine
|
||||
import de.jeanlucmakiola.clockula.stopwatch.StopwatchEngine
|
||||
import de.jeanlucmakiola.clockula.tile.requestAllTileUpdates
|
||||
import de.jeanlucmakiola.clockula.timer.TimerEngine
|
||||
import de.jeanlucmakiola.clockula.widget.AnalogClockWidget
|
||||
import de.jeanlucmakiola.clockula.widget.MidnightRefresh
|
||||
@@ -95,6 +96,10 @@ class SystemEventReceiver : HiltBroadcastReceiver() {
|
||||
AnalogClockWidget().updateAll(context)
|
||||
NextAlarmWidget().updateAll(context)
|
||||
MidnightRefresh.schedule(context)
|
||||
// Both tiles show a wall-clock time in the zone they
|
||||
// were drawn in, and the instant behind it need not
|
||||
// have changed, so no write may reach `WidgetSync`.
|
||||
requestAllTileUpdates(context)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -65,7 +65,10 @@ object QuickTimerTilePresenter {
|
||||
val active = QuickTimerWidgetPresenter.present(duration, timerId, timers, elapsedRealtime, wallClock).active
|
||||
?: return QuickTimerTileModel.AtRest(duration)
|
||||
return when (active.mode) {
|
||||
TimerMode.RUNNING -> QuickTimerTileModel.Running(active.id, wallClock + active.remaining)
|
||||
// The stored end, which the engine re-anchors on a clock change,
|
||||
// rather than one re-derived per read: two clocks read a moment
|
||||
// apart would make every emission a "change".
|
||||
TimerMode.RUNNING -> QuickTimerTileModel.Running(active.id, active.endsAtWallClock ?: (wallClock + active.remaining))
|
||||
TimerMode.PAUSED -> QuickTimerTileModel.Paused(active.id, active.remaining)
|
||||
TimerMode.EXPIRED -> QuickTimerTileModel.Expired(active.id)
|
||||
// Never active, so never reached; at rest is the honest reading.
|
||||
|
||||
@@ -28,7 +28,8 @@ import javax.inject.Inject
|
||||
* It reads the next-alarm widget's own model ([NextAlarmWidgetPresenter]),
|
||||
* so the tile, the widget and the Alarms tab cannot disagree about which
|
||||
* alarm is next. The day is always the weekday's name, never "Today": an
|
||||
* active tile is redrawn only when an alarm changes (`tile/TileSync.kt`), and
|
||||
* active tile is redrawn only when an alarm changes (`widget/WidgetSync.kt`)
|
||||
* or the clock or zone does (`SystemEventReceiver`), and
|
||||
* "Today" would go stale at midnight with nothing to redraw it.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
|
||||
@@ -9,11 +9,9 @@ import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import android.text.format.DateFormat
|
||||
import de.jeanlucmakiola.clockula.MainActivity
|
||||
import de.jeanlucmakiola.clockula.ui.common.localTimeFormatter
|
||||
import de.jeanlucmakiola.clockula.ui.widget.openAppIntent
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
/**
|
||||
* Opens the app on the shell request [action] names (`ui/shell/ShellNavigation`)
|
||||
@@ -25,9 +23,8 @@ import java.time.format.FormatStyle
|
||||
* a `PendingIntent` instead; everything below it only has the `Intent` one.
|
||||
*/
|
||||
internal fun TileService.openApp(action: String) {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
.setAction(action)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
// The widgets' own launch intent, so a tile and a widget land the same way.
|
||||
val intent = openAppIntent(this, action)
|
||||
val launch = Runnable {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
val pending = PendingIntent.getActivity(
|
||||
@@ -68,27 +65,24 @@ internal fun TileService.render(
|
||||
tile.updateTile()
|
||||
}
|
||||
|
||||
/**
|
||||
* The device's 12/24-hour choice, then the locale's own short form — the same
|
||||
* two-step the next-alarm widget and the ring notification format with.
|
||||
*/
|
||||
internal fun Context.tileTime(time: LocalTime): String {
|
||||
val formatter = if (DateFormat.is24HourFormat(this)) {
|
||||
DateTimeFormatter.ofPattern("HH:mm")
|
||||
} else {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
}
|
||||
return formatter.withLocale(resources.configuration.locales[0]).format(time)
|
||||
}
|
||||
/** The app's one 12/24-hour time format, as the next-alarm widget shows it. */
|
||||
internal fun Context.tileTime(time: LocalTime): String = localTimeFormatter(this).format(time)
|
||||
|
||||
/**
|
||||
* Asks the system to bind [service] and let it redraw. Both tiles are
|
||||
* *active* tiles (`META_DATA_ACTIVE_TILE` in the manifest), so this is the
|
||||
* only way they are ever refreshed outside a click — which is exactly the
|
||||
* point: the panel opening costs the app nothing. A tile that is not in the
|
||||
* point: the panel opening costs the app nothing. `widget/WidgetSync.kt`
|
||||
* calls it on every change the tiles mirror. A tile that is not in the
|
||||
* panel makes this a no-op; a refusal is harmless for the same reason, so it
|
||||
* never throws.
|
||||
*/
|
||||
internal fun requestTileUpdate(context: Context, service: Class<out TileService>) {
|
||||
runCatching { TileService.requestListeningState(context, ComponentName(context, service)) }
|
||||
}
|
||||
|
||||
/** Both tiles, for a change that moves what their times mean rather than the state behind them. */
|
||||
internal fun requestAllTileUpdates(context: Context) {
|
||||
requestTileUpdate(context, NextAlarmTileService::class.java)
|
||||
requestTileUpdate(context, QuickTimerTileService::class.java)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package de.jeanlucmakiola.clockula.tile
|
||||
|
||||
import android.content.Context
|
||||
import android.service.quicksettings.TileService
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import de.jeanlucmakiola.clockula.alarm.AlarmEngine
|
||||
import de.jeanlucmakiola.clockula.timer.QuickTileTimer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Keeps the two Quick Settings tiles in step with the app's state (#26) —
|
||||
* `WidgetSync`'s counterpart, collecting the same read-only flows the tiles
|
||||
* render. Both are *active* tiles, so nothing else ever redraws them outside
|
||||
* a click: on every change this asks the system to bind the tile, which then
|
||||
* collects and draws for itself. The process is alive whenever an engine
|
||||
* writes, so this sees every change; and every process start emits once,
|
||||
* which is the redraw after a reboot or a process death.
|
||||
*
|
||||
* Started once, from [de.jeanlucmakiola.clockula.ClockulaApp.onCreate], in
|
||||
* the app's `@ApplicationScope`.
|
||||
*/
|
||||
@Singleton
|
||||
class TileSync @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val alarmEngine: AlarmEngine,
|
||||
private val quickTile: QuickTileTimer,
|
||||
) {
|
||||
|
||||
suspend fun run(): Unit = coroutineScope {
|
||||
sync(alarmEngine.upcoming(), NextAlarmTileService::class.java)
|
||||
sync(quickTile.model, QuickTimerTileService::class.java)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun CoroutineScope.sync(source: Flow<*>, tile: Class<out TileService>) = launch {
|
||||
source
|
||||
.distinctUntilChanged()
|
||||
// A burst of writes (an import's replace-all) is one redraw.
|
||||
.debounce(DEBOUNCE)
|
||||
.collect { requestTileUpdate(context, tile) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val DEBOUNCE = 150.milliseconds
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,9 @@ import javax.inject.Singleton
|
||||
* Android type in sight: `tile/QuickTimerTileService.kt` only renders [model]
|
||||
* and forwards the click here.
|
||||
*
|
||||
* At rest a tap creates a transient timer at the tile's duration and starts
|
||||
* it — the same two steps as `QuickTimerReceiver` and the Timers screen's
|
||||
* quick start — and remembers its id. On an active timer a tap resets it,
|
||||
* At rest a tap creates a transient timer at the tile's duration, remembers
|
||||
* its id and starts it — the same create-then-start as `QuickTimerReceiver`
|
||||
* and the Timers screen's quick start. On an active timer a tap resets it,
|
||||
* which for a transient timer means stop (or stop ringing) and delete, so the
|
||||
* tile is at rest again: the widget's second tap. Everything goes through the
|
||||
* [TimerEngine], so the AlarmManager slot and the foreground service follow
|
||||
@@ -56,8 +56,12 @@ class QuickTileTimer @Inject constructor(
|
||||
when (val current = model.first()) {
|
||||
is QuickTimerTileModel.AtRest -> {
|
||||
val id = timers.create(TimerDraft(duration = current.duration, deleteAfterUse = true))
|
||||
engine.start(id)
|
||||
// Remembered before it starts, so a start that throws, or a
|
||||
// process that dies in between, never leaves a running timer
|
||||
// the tile can neither see nor stop. It also makes the start
|
||||
// the last write, the one `WidgetSync` redraws the tile on.
|
||||
state.setTimerId(id)
|
||||
engine.start(id)
|
||||
}
|
||||
is QuickTimerTileModel.Running -> reset(current.timerId)
|
||||
is QuickTimerTileModel.Paused -> reset(current.timerId)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.jeanlucmakiola.clockula.ui.common
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateFormat
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -23,20 +24,25 @@ internal fun rememberLocalTimeFormatter(): (LocalTime) -> String {
|
||||
// The locale can change under a running activity, and the 12/24-hour
|
||||
// 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 formatter = remember(configuration) { localTimeFormatter(context) }
|
||||
return remember(formatter) {
|
||||
{ time -> formatter.format(time) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The device's 12/24-hour choice, then the locale's own short form — the same
|
||||
* two-step the ring notification already formats with. The non-Composable
|
||||
* half of [rememberLocalTimeFormatter], for the surfaces outside a
|
||||
* composition: the next-alarm widget and the Quick Settings tiles.
|
||||
*/
|
||||
internal fun localTimeFormatter(context: Context): DateTimeFormatter =
|
||||
if (DateFormat.is24HourFormat(context)) {
|
||||
DateTimeFormatter.ofPattern("HH:mm")
|
||||
} else {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
}
|
||||
|
||||
/** Delegates to [rememberLocalTimeFormatter]. Unchanged for every existing caller. */
|
||||
@Composable
|
||||
internal fun rememberAlarmTimeFormatter(): (TimeOfDay) -> String {
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.jeanlucmakiola.clockula.ui.tile
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@@ -17,13 +18,15 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.jeanlucmakiola.clockula.MainActivity
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.alarm.AlarmIntents
|
||||
import de.jeanlucmakiola.clockula.data.prefs.SettingsPrefs
|
||||
import de.jeanlucmakiola.clockula.tile.NextAlarmTileService
|
||||
import de.jeanlucmakiola.clockula.tile.QuickTimerTileService
|
||||
import de.jeanlucmakiola.clockula.tile.requestTileUpdate
|
||||
import de.jeanlucmakiola.clockula.ui.theme.ClockulaTheme
|
||||
import de.jeanlucmakiola.clockula.ui.widget.config.QuickTimerPicker
|
||||
import de.jeanlucmakiola.clockula.ui.widget.openAppIntent
|
||||
import de.jeanlucmakiola.floret.prefs.Appearance
|
||||
import de.jeanlucmakiola.floret.prefs.isDark
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
@@ -53,17 +56,19 @@ class TilePreferencesActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Exported for System UI, so anything else that starts it explicitly
|
||||
// is turned away rather than shown a settings screen.
|
||||
if (intent?.action != TileService.ACTION_QS_TILE_PREFERENCES) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
// Guarded like `MainActivity.requestFrom`: this activity is exported,
|
||||
// and reading an extra unparcels the whole bundle.
|
||||
val tile = runCatching {
|
||||
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java)
|
||||
}.getOrNull()
|
||||
if (tile?.className == NextAlarmTileService::class.java.name) {
|
||||
startActivity(
|
||||
Intent(this, MainActivity::class.java)
|
||||
.setAction(AlarmIntents.ACTION_SHOW_ALARMS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||
)
|
||||
startActivity(openAppIntent(this, AlarmIntents.ACTION_SHOW_ALARMS))
|
||||
finish()
|
||||
return
|
||||
}
|
||||
@@ -93,6 +98,8 @@ class TilePreferencesActivity : ComponentActivity() {
|
||||
saving = true
|
||||
lifecycleScope.launch {
|
||||
withContext(NonCancellable) { settingsPrefs.setQuickTileDuration(duration) }
|
||||
// The one write to the tile's state that `WidgetSync` doesn't watch.
|
||||
requestTileUpdate(this@TilePreferencesActivity, QuickTimerTileService::class.java)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.jeanlucmakiola.clockula.ui.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.format.DateFormat
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceModifier
|
||||
@@ -21,10 +20,9 @@ import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.alarm.AlarmIntents
|
||||
import de.jeanlucmakiola.clockula.ui.common.localTimeFormatter
|
||||
import de.jeanlucmakiola.clockula.domain.widget.NextAlarmWidgetModel
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
import java.util.Locale
|
||||
import de.jeanlucmakiola.clockula.widget.di.WidgetEntryPoint
|
||||
@@ -127,15 +125,8 @@ fun NextAlarmWidgetContent(model: NextAlarmWidgetModel) {
|
||||
|
||||
private fun showAlarmsIntent(context: Context): Intent = openAppIntent(context, AlarmIntents.ACTION_SHOW_ALARMS)
|
||||
|
||||
private fun timeLabel(context: Context, model: NextAlarmWidgetModel): String {
|
||||
val time = LocalTime.of(model.hour, model.minute)
|
||||
val formatter = if (DateFormat.is24HourFormat(context)) {
|
||||
DateTimeFormatter.ofPattern("HH:mm")
|
||||
} else {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
}
|
||||
return formatter.format(time)
|
||||
}
|
||||
private fun timeLabel(context: Context, model: NextAlarmWidgetModel): String =
|
||||
localTimeFormatter(context).format(LocalTime.of(model.hour, model.minute))
|
||||
|
||||
private fun dayLabel(context: Context, model: NextAlarmWidgetModel): String = when {
|
||||
model.isToday -> context.getString(R.string.widget_next_alarm_today)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package de.jeanlucmakiola.clockula.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.service.quicksettings.TileService
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import de.jeanlucmakiola.clockula.alarm.AlarmEngine
|
||||
import de.jeanlucmakiola.clockula.data.stopwatch.StopwatchRepository
|
||||
import de.jeanlucmakiola.clockula.data.timers.TimerRepository
|
||||
import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockRepository
|
||||
import de.jeanlucmakiola.clockula.tile.NextAlarmTileService
|
||||
import de.jeanlucmakiola.clockula.tile.QuickTimerTileService
|
||||
import de.jeanlucmakiola.clockula.tile.requestTileUpdate
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -35,9 +39,17 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
* (`widget/QuickTimerWidget.kt`). The analog clock reads none of them; its
|
||||
* date is kept current by [MidnightRefresh].
|
||||
*
|
||||
* The two Quick Settings tiles (#26) ride the same collectors: both are
|
||||
* *active* tiles, so nothing redraws them outside a click unless the app asks
|
||||
* the system to bind them, which is what a change here does. The quick-timer
|
||||
* tile follows the timers, as the quick-timer widget does; its duration is
|
||||
* written only by `ui/tile/TilePreferencesActivity.kt`, which asks for its
|
||||
* own redraw.
|
||||
*
|
||||
* Started once, from [de.jeanlucmakiola.clockula.ClockulaApp.onCreate], in the
|
||||
* app's `@ApplicationScope` — never driven by a `Ticker`, so an idle home
|
||||
* screen costs nothing between writes.
|
||||
* screen costs nothing between writes. Every process start emits once, which
|
||||
* is also the tiles' redraw after a reboot or a process death.
|
||||
*/
|
||||
@Singleton
|
||||
class WidgetSync @Inject constructor(
|
||||
@@ -49,21 +61,28 @@ class WidgetSync @Inject constructor(
|
||||
) {
|
||||
|
||||
suspend fun run(): Unit = coroutineScope {
|
||||
sync(alarmEngine.upcoming(), NextAlarmWidget())
|
||||
sync(timers.timers(), TimerWidget(), QuickTimerWidget())
|
||||
sync(alarmEngine.upcoming(), NextAlarmWidget(), tiles = listOf(NextAlarmTileService::class.java))
|
||||
sync(timers.timers(), TimerWidget(), QuickTimerWidget(), tiles = listOf(QuickTimerTileService::class.java))
|
||||
// A lap leaves the run untouched, so the lap count is watched too.
|
||||
sync(combine(stopwatch.run(), stopwatch.laps()) { run, laps -> run to laps.size }, StopwatchWidget())
|
||||
sync(worldClocks.worldClocks(), ClockWidget())
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun CoroutineScope.sync(source: Flow<*>, vararg widgets: GlanceAppWidget) = launch {
|
||||
private fun CoroutineScope.sync(
|
||||
source: Flow<*>,
|
||||
vararg widgets: GlanceAppWidget,
|
||||
tiles: List<Class<out TileService>> = emptyList(),
|
||||
) = launch {
|
||||
source
|
||||
.distinctUntilChanged()
|
||||
// A burst of writes (an import's replace-all, a reorder) collapses
|
||||
// to one redraw rather than one per row.
|
||||
.debounce(DEBOUNCE)
|
||||
.collect { widgets.forEach { it.updateAll(context) } }
|
||||
.collect {
|
||||
tiles.forEach { requestTileUpdate(context, it) }
|
||||
widgets.forEach { it.updateAll(context) }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -130,8 +130,8 @@ kit module: the kit's roadmap keeps schedulers app-local.
|
||||
| `interop/` | the `AlarmClock` boundary's Android half (M9): `AlarmClockActivity` (the one exported, permission-guarded, window-less door), `IntentExtrasReader` (`Bundle` → `IntentExtras`), `AlarmClockHandler` (the orchestrator; no `android.*` import, ever), `InteropIntents` (the internal transport to `MainActivity`) and `InteropLaunch` |
|
||||
| `system/` | `RebootRepair`, the boot-id gate |
|
||||
| `backup/` | `BackupEngine` (M10), the orchestrator (a ringing alarm dismissed and running timers deleted before a replace, `AlarmEngine.reschedule()`/`TimerEngine.resync()` after it), plus `BackupFiles`, the SAF seam, with its Android implementation in `backup/android/` |
|
||||
| `widget/` | the three `GlanceAppWidget`/`GlanceAppWidgetReceiver` pairs (M10) and `WidgetSync`, the event-driven collector (started from `ClockulaApp.onCreate`) that redraws all three on any change to the state they mirror. `widget/di/` holds the `@EntryPoint` Glance needs, since a `GlanceAppWidgetReceiver` is never `@AndroidEntryPoint`-constructed |
|
||||
| `tile/` | the two Quick Settings tiles (#26): `NextAlarmTileService` and `QuickTimerTileService`, which only render and forward a click, `TileKit` (opening the app, behind an unlock on a locked device, through the `PendingIntent` overload on API 34+), and `TileSync`, `WidgetSync`'s counterpart: both are *active* tiles, redrawn only when this asks the system to, never on every panel open |
|
||||
| `widget/` | the three `GlanceAppWidget`/`GlanceAppWidgetReceiver` pairs (M10) and `WidgetSync`, the event-driven collector (started from `ClockulaApp.onCreate`) that redraws all three, and since #26 the two Quick Settings tiles, on any change to the state they mirror. `widget/di/` holds the `@EntryPoint` Glance needs, since a `GlanceAppWidgetReceiver` is never `@AndroidEntryPoint`-constructed |
|
||||
| `tile/` | the two Quick Settings tiles (#26): `NextAlarmTileService` and `QuickTimerTileService`, which only render and forward a click, and `TileKit` (opening the app, behind an unlock on a locked device, through the `PendingIntent` overload on API 34+; asking for a redraw). Both are *active* tiles, redrawn only when `WidgetSync` (on a change they mirror) or `SystemEventReceiver` (on a clock or zone change) asks the system to, never on every panel open |
|
||||
| `ui/tile/` | `TilePreferencesActivity` (#26), what long-pressing a tile opens: the quick-timer tile's duration, through the quick-timer widget's own picker |
|
||||
| `ui/theme/`, `ui/crash/` | the M0/M1 theme and the crash-report surface |
|
||||
| `ui/shell/` | the navigation policy (`ShellNavigation`), the adaptive shell, the live pill and its source/ViewModel, the notification-permission ask. `EmptyTabScreen` is gone: its KDoc said it existed only until each of M5–M8 replaced its own tab, and the world clock was its last caller |
|
||||
|
||||
+1
-1
@@ -615,7 +615,7 @@ Deferred deliberately, in rough priority order:
|
||||
tab (after an unlock, on a locked device); "Quick timer" is the quick-timer
|
||||
widget's twin, with its own duration (long-press to choose; in the backup),
|
||||
and works on a locked device as the timer notification does. Both are active
|
||||
tiles, redrawn by `TileSync` on a write rather than bound on every panel
|
||||
tiles, redrawn by `WidgetSync` on a write rather than bound on every panel
|
||||
open. No new permission.
|
||||
- **Bedtime / sleep schedule**: wind-down and wake, with DND handoff. A feature
|
||||
in its own right, realistically v2.
|
||||
|
||||
Reference in new issue
Block a user