From 8053390b8a40382cde5b64564c274c1772d16331 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 17:59:46 +0000 Subject: [PATCH] feat: add Quick Settings tile to start timer with last used duration Adds a Quick Settings tile that starts the sleep timer with the persisted preset (the last dial-committed duration) directly from the QS panel, without opening the app. While a timer is running the tile shows the remaining minutes and a tap cancels it, mirroring the notification's Cancel action. The tile observes the in-process TimerRepository StateFlow while the panel is open, so it stays in sync with the foreground service tick. Tile strings are localized in both en and de. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Kk4QRUoCmfQgD6xgUE9Phn --- app/src/main/AndroidManifest.xml | 16 +++ .../sleeptimer/tile/SleepTimerTileService.kt | 118 ++++++++++++++++++ app/src/main/res/values-de/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 4 files changed, 144 insertions(+) create mode 100644 app/src/main/kotlin/dev/xitee/sleeptimer/tile/SleepTimerTileService.kt 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 +