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" }