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>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user