diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3ea4e3d..9488d6f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,6 +35,22 @@ android:exported="false" android:foregroundServiceType="mediaPlayback" /> + + + + + + + + + TileModel( + phase = timerState.phase, + minutes = when (timerState.phase) { + TimerPhase.IDLE -> settings.presetMinutes + else -> remainingMillisToDisplayMinutes(timerState.remainingMillis) + }, + ) + } + // remainingMillis changes every second but the displayed minutes only + // change once a minute — skip the redundant updateTile() calls. + .distinctUntilChanged() + .collect(::render) + } + } + + override fun onStopListening() { + listeningJob?.cancel() + listeningJob = null + super.onStopListening() + } + + override fun onDestroy() { + serviceScope.cancel() + super.onDestroy() + } + + override fun onClick() { + super.onClick() + when (timerRepository.timerState.value.phase) { + TimerPhase.IDLE -> startTimerWithPreset() + // RUNNING / FADING_OUT: cancel restores volume and stops the service; if + // the countdown ended in the meantime, the service's stale-intent guard + // turns this into a no-op stopSelf. + else -> startService(timerServiceIntent(SleepTimerService.ACTION_CANCEL)) + } + } + + private fun startTimerWithPreset() { + // The preset needs a DataStore read. The tap keeps the app in a state that + // may start a foreground service, and the read is local and fast, so doing + // it inside that window is fine. + serviceScope.launch { + val minutes = settingsRepository.settings.first().presetMinutes + val intent = timerServiceIntent(SleepTimerService.ACTION_START).apply { + putExtra(SleepTimerService.EXTRA_DURATION_MILLIS, minutes * 60_000L) + } + startForegroundService(intent) + } + } + + private fun render(model: TileModel) { + val tile = qsTile ?: return + tile.state = if (model.phase == TimerPhase.IDLE) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + tile.subtitle = when (model.phase) { + TimerPhase.FADING_OUT -> getString(R.string.qs_tile_fading_out) + // coerceAtLeast: remaining can round down to 0 in the brief window + // between expiry and teardown; never show "0 min". + else -> getString(R.string.qs_tile_minutes, model.minutes.coerceAtLeast(1)) + } + } + tile.updateTile() + } + + private fun timerServiceIntent(actionName: String): Intent = + Intent().apply { + action = actionName + setClassName(this@SleepTimerTileService, SleepTimerService::class.java.name) + } + + private data class TileModel(val phase: TimerPhase, val minutes: Int) +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bc8b66a..3125909 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -21,6 +21,11 @@ Haptisches Feedback Vibrieren bei Verwendung von Timer und Bedienelementen + + Schlaf-Timer + %d Min + Wird ausgeblendet + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2dbcd59..d8bf552 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,6 +21,11 @@ Haptic feedback Vibrate when using timer and controls + + Sleep Timer + %d min + Fading out +