fix: bring back a swiped-away ring notification
From Android 14 a foreground service's notification can be swiped away on an unlocked phone, which left a sounding alarm with no controls. The ring notification now reposts itself when it is deleted, and tapping a ringing alarm in the list opens its ring screen.
This commit is contained in:
1 parent
9cd40d6813
commit
c99106321a
4 files changed
+53
-3
No files matched your search
@@ -25,6 +25,7 @@ object AlarmIntents {
|
||||
const val REQUEST_SHOW: Int = 3
|
||||
const val REQUEST_SNOOZE: Int = 4
|
||||
const val REQUEST_DISMISS: Int = 5
|
||||
const val REQUEST_REPOST: Int = 6
|
||||
|
||||
val ALL_REQUEST_CODES: List<Int> = listOf(
|
||||
REQUEST_FIRE,
|
||||
@@ -32,5 +33,6 @@ object AlarmIntents {
|
||||
REQUEST_SHOW,
|
||||
REQUEST_SNOOZE,
|
||||
REQUEST_DISMISS,
|
||||
REQUEST_REPOST,
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package de.jeanlucmakiola.clockula.alarm.ring
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -51,6 +53,9 @@ class AlarmRingService : Service() {
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var ringing: Job? = null
|
||||
|
||||
/** The notification last posted, put back as it was when the user swipes it away. */
|
||||
private var posted: Notification? = null
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -58,6 +63,16 @@ class AlarmRingService : Service() {
|
||||
stop()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
if (intent?.action == ACTION_REPOST) {
|
||||
// The ring is untouched; only its notification comes back. A
|
||||
// service the system recreated has nothing to repost yet, so it
|
||||
// starts the ring as any other start would.
|
||||
posted?.let {
|
||||
getSystemService(NotificationManager::class.java)
|
||||
.notify(RingNotifications.RING_NOTIFICATION_ID, it)
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
}
|
||||
|
||||
// The whole ring is released, not just the coroutine: cancelling the job
|
||||
// alone would orphan a MediaPlayer the previous start had already
|
||||
@@ -115,7 +130,7 @@ class AlarmRingService : Service() {
|
||||
* screen.
|
||||
*/
|
||||
private fun startForegroundPreparing() {
|
||||
startForeground(RingNotifications.preparing(this, presentation()))
|
||||
startForeground(RingNotifications.preparing(this, presentation(), repostPendingIntent()))
|
||||
}
|
||||
|
||||
private fun startForeground(session: RingSession) {
|
||||
@@ -124,13 +139,22 @@ class AlarmRingService : Service() {
|
||||
context = this,
|
||||
session = session,
|
||||
presentation = presentation(),
|
||||
repost = repostPendingIntent(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentation() = RingPresentationPolicy.forCapabilities(capabilities.snapshot())
|
||||
|
||||
private fun repostPendingIntent(): PendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
AlarmIntents.REQUEST_REPOST,
|
||||
Intent(this, AlarmRingService::class.java).setAction(ACTION_REPOST),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
|
||||
private fun startForeground(notification: Notification) {
|
||||
posted = notification
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
// systemExempted: Android's own guidance names an app holding an
|
||||
// exact-alarm permission and continuing an alarm in the background.
|
||||
@@ -164,12 +188,14 @@ class AlarmRingService : Service() {
|
||||
|
||||
private fun stop() {
|
||||
releaseRing()
|
||||
posted = null
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_STOP = AlarmIntents.ACTION_DISMISS + ".SERVICE_STOP"
|
||||
private const val ACTION_REPOST = AlarmIntents.ACTION_DISMISS + ".SERVICE_REPOST"
|
||||
private const val WAKE_LOCK_TAG = "clockula:alarm-ring"
|
||||
|
||||
/** A backstop on the backstop: the ring window is ten minutes. */
|
||||
|
||||
@@ -82,7 +82,7 @@ object RingNotifications {
|
||||
* closes when there is none, so a dismiss that lands before the service's
|
||||
* read costs one brief screen wake, never a ring screen left up.
|
||||
*/
|
||||
fun preparing(context: Context, presentation: RingPresentation): Notification {
|
||||
fun preparing(context: Context, presentation: RingPresentation, repost: PendingIntent): Notification {
|
||||
val show = showPendingIntent(context)
|
||||
val builder = NotificationCompat.Builder(context, RING_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
@@ -93,6 +93,7 @@ object RingNotifications {
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
.setContentIntent(show)
|
||||
.setDeleteIntent(repost)
|
||||
if (presentation.useFullScreenIntent) builder.setFullScreenIntent(show, true)
|
||||
return builder.build()
|
||||
}
|
||||
@@ -104,11 +105,16 @@ object RingNotifications {
|
||||
* while snoozes are left, since a refused snooze dismisses; and an alarm
|
||||
* with a dismiss challenge has its Dismiss open the ring screen, where the
|
||||
* challenge is, rather than end the alarm from a heads-up or the lock screen.
|
||||
*
|
||||
* From Android 14 a foreground service's notification can be swiped away
|
||||
* on an unlocked phone, ongoing or not. [repost] puts it back, so a
|
||||
* sounding alarm never loses its controls.
|
||||
*/
|
||||
fun ringing(
|
||||
context: Context,
|
||||
session: RingSession,
|
||||
presentation: RingPresentation,
|
||||
repost: PendingIntent,
|
||||
): Notification {
|
||||
val alarm = session.alarm
|
||||
val show = showPendingIntent(context)
|
||||
@@ -122,6 +128,7 @@ object RingNotifications {
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
.setContentIntent(show)
|
||||
.setDeleteIntent(repost)
|
||||
if (session.canSnooze) {
|
||||
builder.addAction(
|
||||
R.drawable.ic_notification,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.jeanlucmakiola.clockula.ui.alarms
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -19,11 +20,13 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.ui.ring.AlarmRingActivity
|
||||
import de.jeanlucmakiola.clockula.ui.shell.LocalLivePillInset
|
||||
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
||||
import de.jeanlucmakiola.floret.components.GroupedListInset
|
||||
@@ -53,6 +56,7 @@ fun AlarmsScreen(
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val rows = (state as? AlarmsUiState.Loaded)?.rows
|
||||
val context = LocalContext.current
|
||||
val pillInset = LocalLivePillInset.current
|
||||
val addAlarm = stringResource(R.string.alarms_add)
|
||||
|
||||
@@ -99,7 +103,18 @@ fun AlarmsScreen(
|
||||
items(rows.orEmpty(), key = { row -> row.id }) { row ->
|
||||
AlarmRow(
|
||||
row = row,
|
||||
onClick = { onEditAlarm(row.id) },
|
||||
// A ringing alarm's row leads back to its ring screen, the
|
||||
// way out if its notification was swiped away.
|
||||
onClick = {
|
||||
if (row.ringing) {
|
||||
context.startActivity(
|
||||
Intent(context, AlarmRingActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
} else {
|
||||
onEditAlarm(row.id)
|
||||
}
|
||||
},
|
||||
onToggle = { enabled -> viewModel.onToggleEnabled(row.id, enabled) },
|
||||
// An alarm whose time changed slides to its new place
|
||||
// instead of teleporting.
|
||||
|
||||
Reference in new issue
Block a user