fix(ring): open the ring screen when an alarm or timer goes off in the app

A full-screen intent opens its activity only on a locked phone or a dark
display. While the user is in Clockula, SystemUI shows it as a heads-up
and the ring screen never comes up. Both ring services now start their
screen directly when the app has a visible window, which Android allows
from the background. Anywhere else the full-screen intent still decides.
This commit is contained in:
makiolaj committed 2026-10-28 11:55:12 +01:00
1 parent caa8c50db3
commit d3af7d5b7c
4 files changed
+49 -2

No files matched your search

+2 -1
View File
@@ -47,7 +47,8 @@ The first release of Clockula: alarms, timers, a stopwatch and a world clock.
default sound instead.
- A ring screen over the lock screen with snooze and dismiss. The dismiss
challenge always gives you a way out, works with a screen reader, and Back
can't silence an alarm.
can't silence an alarm. It also opens when the alarm or a timer goes off
while you're using Clockula.
- Choose what the volume buttons do while an alarm rings: change the volume
(as before), snooze, dismiss or nothing. Dismiss still respects the dismiss
challenge by snoozing instead, and a snooze press with no snoozes left
@@ -18,8 +18,10 @@ import de.jeanlucmakiola.clockula.domain.alarm.RingPresentationPolicy
import de.jeanlucmakiola.clockula.domain.alarm.RingSession
import de.jeanlucmakiola.clockula.domain.ring.RingFallbackPolicy
import de.jeanlucmakiola.clockula.domain.ring.SilenceAfter
import de.jeanlucmakiola.clockula.ring.ForegroundRingScreen
import de.jeanlucmakiola.clockula.ring.RingAudioPlayer
import de.jeanlucmakiola.clockula.ring.RingVibrator
import de.jeanlucmakiola.clockula.ui.ring.AlarmRingActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -128,10 +130,12 @@ class AlarmRingService : Service() {
* replaces it with the alarm's own notification as soon as the session is
* read. Same channel and same id, so the user sees one notification — and
* the placeholder is the first post, so it is the one that opens the ring
* screen.
* screen. Inside the app the full-screen intent only shows a heads-up,
* so the screen is opened directly there.
*/
private fun startForegroundPreparing() {
startForeground(RingNotifications.preparing(this, presentation(), repostPendingIntent()))
ForegroundRingScreen.launchIfVisible(this, AlarmRingActivity::class.java)
}
private fun startForeground(session: RingSession) {
@@ -0,0 +1,38 @@
package de.jeanlucmakiola.clockula.ring
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
/**
* Opens a ring screen directly while one of Clockula's own windows is on
* screen.
*
* A full-screen intent opens its activity only when the phone is locked or the
* display is off. On a phone in use, and so always while the user is inside
* Clockula, SystemUI shows it as a heads-up and the ring screen never comes up.
* An app with a visible window may start activities from the background, so
* the ring services start the screen themselves in that one case. Everywhere
* else the full-screen intent stays in charge, and a heads-up over another app
* is what the platform asks for.
*/
object ForegroundRingScreen {
fun launchIfVisible(context: Context, screen: Class<out Activity>) {
if (!appIsVisible()) return
// A refused start costs nothing: the notification is up either way.
runCatching {
context.startActivity(
Intent(context, screen).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
)
}
}
/** The process holds a visible activity, not only a foreground service. */
private fun appIsVisible(): Boolean {
val process = ActivityManager.RunningAppProcessInfo()
ActivityManager.getMyMemoryState(process)
return process.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
}
}
@@ -20,12 +20,14 @@ import de.jeanlucmakiola.clockula.domain.timer.TimerMode
import de.jeanlucmakiola.clockula.domain.timer.TimerRing
import de.jeanlucmakiola.clockula.domain.timer.TimerRingDecision
import de.jeanlucmakiola.clockula.domain.timer.TimerSettings
import de.jeanlucmakiola.clockula.ring.ForegroundRingScreen
import de.jeanlucmakiola.clockula.ring.RingAudioPlayer
import de.jeanlucmakiola.clockula.ring.RingVibrator
import de.jeanlucmakiola.clockula.timer.AlarmRingStatus
import de.jeanlucmakiola.clockula.timer.TimerEngine
import de.jeanlucmakiola.clockula.timer.TimerIntents
import de.jeanlucmakiola.clockula.timer.TimerServiceState
import de.jeanlucmakiola.clockula.ui.ring.TimerRingActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -286,6 +288,8 @@ class TimerService : Service() {
fullScreen = sounding && capabilities.snapshot().canUseFullScreenIntent
}
startForeground(TimerNotifications.timer(this, state, fullScreen))
// Inside the app the full-screen intent only shows a heads-up.
if (!state.alertOnce && sounding) ForegroundRingScreen.launchIfVisible(this, TimerRingActivity::class.java)
}
/**