Files
honeyDueKMP/composeApp/src/androidMain/kotlin/com/tt/honeyDue/notifications/SnoozeScheduler.kt
Trey T 19471d780d P2 Stream H: standalone TaskSuggestionsScreen
Port iOS TaskSuggestionsView as a standalone route reachable outside
onboarding. Uses shared suggestions API + accept/skip analytics in
non-onboarding variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:10:47 -05:00

107 lines
4.3 KiB
Kotlin

package com.tt.honeyDue.notifications
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
/**
* Schedules a "snooze" redelivery of a task notification using [AlarmManager].
*
* iOS achieves snooze via UNNotificationRequest with a 30-minute
* UNTimeIntervalNotificationTrigger (see
* `iosApp/iosApp/PushNotifications/NotificationCategories.swift`). Android
* has no equivalent, so we lean on AlarmManager to wake us in 30 minutes
* and rebroadcast to [NotificationActionReceiver], which re-posts the
* notification via [FcmService]'s builder path.
*
* Exact alarms (`setExactAndAllowWhileIdle`) require the `SCHEDULE_EXACT_ALARM`
* permission on Android 12+. Because P4 Stream O is scoped to receiver-only
* changes in the manifest, we probe [AlarmManager.canScheduleExactAlarms] at
* runtime and fall back to the inexact `setAndAllowWhileIdle` variant when
* the permission has not been granted — snooze fidelity in that case is
* "roughly 30 min" which is within Android's Doze tolerance and acceptable.
*/
object SnoozeScheduler {
private const val TAG = "SnoozeScheduler"
/**
* Schedule a snooze alarm [delayMs] into the future for the given [taskId].
* If an alarm already exists for this task id, it is replaced (pending
* intents are reused by request code = taskId).
*/
fun schedule(
context: Context,
taskId: Long,
delayMs: Long = NotificationActions.SNOOZE_DELAY_MS,
title: String? = null,
body: String? = null,
type: String? = null
) {
val alarm = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
if (alarm == null) {
Log.w(TAG, "AlarmManager unavailable; cannot schedule snooze for task=$taskId")
return
}
val triggerAt = System.currentTimeMillis() + delayMs
val pi = pendingIntent(context, taskId, title, body, type, create = true)
?: run {
Log.w(TAG, "Unable to build snooze PendingIntent for task=$taskId")
return
}
val useExact = Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
alarm.canScheduleExactAlarms()
if (useExact) {
alarm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pi)
} else {
// Fallback path when SCHEDULE_EXACT_ALARM is revoked. Inexact but
// still wakes the device from Doze.
alarm.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pi)
}
Log.d(TAG, "Scheduled snooze for task=$taskId at $triggerAt (exact=$useExact)")
}
/** Cancel a scheduled snooze alarm for [taskId], if any. */
fun cancel(context: Context, taskId: Long) {
val alarm = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager ?: return
val pi = pendingIntent(context, taskId, null, null, null, create = false) ?: return
alarm.cancel(pi)
pi.cancel()
}
private fun pendingIntent(
context: Context,
taskId: Long,
title: String?,
body: String?,
type: String?,
create: Boolean
): PendingIntent? {
val intent = Intent(context, NotificationActionReceiver::class.java).apply {
action = NotificationActions.SNOOZE_FIRE
putExtra(NotificationActions.EXTRA_TASK_ID, taskId)
title?.let { putExtra(NotificationActions.EXTRA_TITLE, it) }
body?.let { putExtra(NotificationActions.EXTRA_BODY, it) }
type?.let { putExtra(NotificationActions.EXTRA_TYPE, it) }
}
val baseFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
val lookupFlags = if (create) baseFlags else PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
val requestCode = requestCode(taskId)
return PendingIntent.getBroadcast(context, requestCode, intent, lookupFlags)
}
/** Stable request code for per-task snoozes so cancel() finds the same PI. */
private fun requestCode(taskId: Long): Int {
// Fold 64-bit task id into a stable 32-bit request code.
return (taskId xor (taskId ushr 32)).toInt() xor SNOOZE_REQUEST_SALT
}
private const val SNOOZE_REQUEST_SALT = 0x534E5A45.toInt() // "SNZE"
}