Merge branch 'master' into ios-2
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -64,4 +64,5 @@ logs/
|
|||||||
.cache/
|
.cache/
|
||||||
*.lock
|
*.lock
|
||||||
*.bin
|
*.bin
|
||||||
workflow/
|
workflow/
|
||||||
|
screenshots/
|
||||||
@@ -3,6 +3,7 @@ package com.timesafari.dailynotification
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -22,15 +23,28 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
|
when (intent?.action) {
|
||||||
Log.i(TAG, "Boot completed, rescheduling notifications")
|
Intent.ACTION_BOOT_COMPLETED,
|
||||||
|
Intent.ACTION_LOCKED_BOOT_COMPLETED -> {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
Log.i(TAG, "Boot completed, setting boot flag and starting recovery")
|
||||||
try {
|
|
||||||
rescheduleNotifications(context)
|
// Phase 2: Set boot flag for scenario detection
|
||||||
} catch (e: Exception) {
|
// This allows ReactivationManager to detect boot scenario on next app launch
|
||||||
Log.e(TAG, "Failed to reschedule notifications after boot", e)
|
// Only set flag for actual boot events, not MY_PACKAGE_REPLACED
|
||||||
}
|
val prefs = context.getSharedPreferences("dailynotification_recovery", Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().putLong("last_boot_at", System.currentTimeMillis()).apply()
|
||||||
|
|
||||||
|
// Phase 3: Use ReactivationManager for boot recovery
|
||||||
|
ReactivationManager.runBootRecovery(context)
|
||||||
|
}
|
||||||
|
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||||
|
// App was updated - don't set boot flag, just run recovery
|
||||||
|
// This prevents false BOOT detection when app is reinstalled during testing
|
||||||
|
Log.i(TAG, "Package replaced, running recovery without setting boot flag")
|
||||||
|
ReactivationManager.runBootRecovery(context)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Unhandled intent action: ${intent?.action}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +85,13 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
vibration = true,
|
vibration = true,
|
||||||
priority = "normal"
|
priority = "normal"
|
||||||
)
|
)
|
||||||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
NotifyReceiver.scheduleExactNotification(
|
||||||
|
context,
|
||||||
|
nextRunTime,
|
||||||
|
config,
|
||||||
|
scheduleId = schedule.id,
|
||||||
|
source = ScheduleSource.BOOT_RECOVERY
|
||||||
|
)
|
||||||
Log.i(TAG, "Rescheduled notification for schedule: ${schedule.id}")
|
Log.i(TAG, "Rescheduled notification for schedule: ${schedule.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,9 +97,14 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
}
|
}
|
||||||
db = DailyNotificationDatabase.getDatabase(context)
|
db = DailyNotificationDatabase.getDatabase(context)
|
||||||
Log.i(TAG, "Daily Notification Plugin loaded successfully")
|
Log.i(TAG, "Daily Notification Plugin loaded successfully")
|
||||||
|
|
||||||
|
// Phase 1: Perform app launch recovery (cold start only)
|
||||||
|
// Runs asynchronously, non-blocking, with timeout
|
||||||
|
val reactivationManager = ReactivationManager(context)
|
||||||
|
reactivationManager.performRecovery()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to initialize Daily Notification Plugin", e)
|
Log.e(TAG, "Failed to initialize Daily Notification Plugin", e)
|
||||||
// Don't throw - allow plugin to load but database operations will fail gracefully
|
// Don't throw - allow plugin to load even if recovery fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,7 +634,7 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
// Cancel alarm using the scheduled time (used for request code)
|
// Cancel alarm using the scheduled time (used for request code)
|
||||||
val nextRunAt = schedule.nextRunAt
|
val nextRunAt = schedule.nextRunAt
|
||||||
if (nextRunAt != null && nextRunAt > 0) {
|
if (nextRunAt != null && nextRunAt > 0) {
|
||||||
NotifyReceiver.cancelNotification(context, nextRunAt)
|
NotifyReceiver.cancelNotification(context, scheduleId = schedule.id, triggerAtMillis = nextRunAt)
|
||||||
cancelledAlarms++
|
cancelledAlarms++
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -727,109 +732,8 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun scheduleDailyReminder(call: PluginCall) {
|
fun scheduleDailyReminder(call: PluginCall) {
|
||||||
// Alias for scheduleDailyNotification for backward compatibility
|
// Alias for scheduleDailyNotification for backward compatibility
|
||||||
// scheduleDailyReminder accepts same parameters as scheduleDailyNotification
|
// This ensures both method names work the same way
|
||||||
try {
|
scheduleDailyNotification(call)
|
||||||
if (context == null) {
|
|
||||||
return call.reject("Context not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if exact alarms can be scheduled
|
|
||||||
if (!canScheduleExactAlarms(context)) {
|
|
||||||
// Permission not granted - request it
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && canRequestExactAlarmPermission(context)) {
|
|
||||||
try {
|
|
||||||
val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
|
|
||||||
data = android.net.Uri.parse("package:${context.packageName}")
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
Log.w(TAG, "Exact alarm permission required. Opened Settings for user to grant permission.")
|
|
||||||
call.reject(
|
|
||||||
"Exact alarm permission required. Please grant 'Alarms & reminders' permission in Settings, then try again.",
|
|
||||||
"EXACT_ALARM_PERMISSION_REQUIRED"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to open exact alarm settings", e)
|
|
||||||
call.reject("Failed to open exact alarm settings: ${e.message}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = android.net.Uri.parse("package:${context.packageName}")
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
Log.w(TAG, "Exact alarm permission denied. Directing user to app settings.")
|
|
||||||
call.reject(
|
|
||||||
"Exact alarm permission denied. Please enable 'Alarms & reminders' in app settings.",
|
|
||||||
"PERMISSION_DENIED"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to open app settings", e)
|
|
||||||
call.reject("Failed to open app settings: ${e.message}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission granted - proceed with scheduling
|
|
||||||
// Capacitor passes the object directly via call.data
|
|
||||||
val options = call.data ?: return call.reject("Options are required")
|
|
||||||
|
|
||||||
// Extract required fields, with defaults
|
|
||||||
val time = options.getString("time") ?: return call.reject("Time is required")
|
|
||||||
val title = options.getString("title") ?: "Daily Reminder"
|
|
||||||
val body = options.getString("body") ?: ""
|
|
||||||
val sound = options.getBoolean("sound") ?: true
|
|
||||||
val priority = options.getString("priority") ?: "default"
|
|
||||||
|
|
||||||
Log.i(TAG, "Scheduling daily reminder: time=$time, title=$title")
|
|
||||||
|
|
||||||
// Convert HH:mm time to cron expression (daily at specified time)
|
|
||||||
val cronExpression = convertTimeToCron(time)
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
val config = UserNotificationConfig(
|
|
||||||
enabled = true,
|
|
||||||
schedule = cronExpression,
|
|
||||||
title = title,
|
|
||||||
body = body,
|
|
||||||
sound = sound,
|
|
||||||
vibration = options.getBoolean("vibration") ?: true,
|
|
||||||
priority = priority
|
|
||||||
)
|
|
||||||
|
|
||||||
val nextRunTime = calculateNextRunTime(cronExpression)
|
|
||||||
|
|
||||||
// Schedule AlarmManager notification
|
|
||||||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
|
||||||
|
|
||||||
// Store schedule in database
|
|
||||||
val scheduleId = options.getString("id") ?: "daily_reminder_${System.currentTimeMillis()}"
|
|
||||||
val schedule = Schedule(
|
|
||||||
id = scheduleId,
|
|
||||||
kind = "notify",
|
|
||||||
cron = cronExpression,
|
|
||||||
clockTime = time,
|
|
||||||
enabled = true,
|
|
||||||
nextRunAt = nextRunTime
|
|
||||||
)
|
|
||||||
getDatabase().scheduleDao().upsert(schedule)
|
|
||||||
|
|
||||||
call.resolve()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to schedule daily reminder", e)
|
|
||||||
call.reject("Daily reminder scheduling failed: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Schedule daily reminder error", e)
|
|
||||||
call.reject("Daily reminder error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1367,6 +1271,54 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
|
// Use stable scheduleId for daily notifications to ensure "one per day" semantics
|
||||||
|
// If user provides an ID, use it; otherwise use stable "daily_notification"
|
||||||
|
val scheduleId = options.getString("id") ?: "daily_notification"
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: START - time=$time, scheduleId=$scheduleId")
|
||||||
|
|
||||||
|
// CRITICAL: Cancel and delete all existing notification schedules before creating new one
|
||||||
|
// This ensures "one per day" semantics - only one daily notification schedule exists
|
||||||
|
// This cleanup runs regardless of whether user provided an ID or not
|
||||||
|
val existingSchedules = getDatabase().scheduleDao().getByKind("notify")
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: Found ${existingSchedules.size} existing notification schedule(s) in database")
|
||||||
|
|
||||||
|
if (existingSchedules.isNotEmpty()) {
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: Existing schedule IDs: ${existingSchedules.map { it.id }.joinToString(", ")}")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleanedCount = 0
|
||||||
|
existingSchedules.forEach { existingSchedule ->
|
||||||
|
try {
|
||||||
|
// Skip if this is the same schedule we're about to create (will be upserted anyway)
|
||||||
|
if (existingSchedule.id == scheduleId) {
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: Skipping cleanup of schedule with same ID (will be updated): ${existingSchedule.id}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: Cleaning up existing schedule: id=${existingSchedule.id}, nextRunAt=${existingSchedule.nextRunAt}, enabled=${existingSchedule.enabled}")
|
||||||
|
|
||||||
|
// Cancel the alarm in AlarmManager
|
||||||
|
NotifyReceiver.cancelNotification(context, existingSchedule.id)
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: Cancelled alarm for schedule: ${existingSchedule.id}")
|
||||||
|
|
||||||
|
// Delete from database
|
||||||
|
getDatabase().scheduleDao().deleteById(existingSchedule.id)
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: Deleted schedule from database: ${existingSchedule.id}")
|
||||||
|
cleanedCount++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "scheduleDailyNotification: Failed to cancel/delete existing schedule: ${existingSchedule.id}", e)
|
||||||
|
// Continue with other schedules - don't fail entire operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanedCount > 0) {
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: ✅ Cleaned up $cleanedCount existing notification schedule(s) before creating new one (total found: ${existingSchedules.size})")
|
||||||
|
} else if (existingSchedules.isNotEmpty()) {
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: No cleanup needed - existing schedule will be updated via upsert: $scheduleId")
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "scheduleDailyNotification: No existing schedules found - creating first notification schedule")
|
||||||
|
}
|
||||||
|
|
||||||
val config = UserNotificationConfig(
|
val config = UserNotificationConfig(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
schedule = cronExpression,
|
schedule = cronExpression,
|
||||||
@@ -1381,13 +1333,14 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
|
|
||||||
// Schedule AlarmManager notification as static reminder
|
// Schedule AlarmManager notification as static reminder
|
||||||
// (doesn't require cached content)
|
// (doesn't require cached content)
|
||||||
val scheduleId = "daily_${System.currentTimeMillis()}"
|
|
||||||
NotifyReceiver.scheduleExactNotification(
|
NotifyReceiver.scheduleExactNotification(
|
||||||
context,
|
context,
|
||||||
nextRunTime,
|
nextRunTime,
|
||||||
config,
|
config,
|
||||||
isStaticReminder = true,
|
isStaticReminder = true,
|
||||||
reminderId = scheduleId
|
reminderId = scheduleId,
|
||||||
|
scheduleId = scheduleId,
|
||||||
|
source = ScheduleSource.INITIAL_SETUP
|
||||||
)
|
)
|
||||||
|
|
||||||
// Always schedule prefetch 2 minutes before notification
|
// Always schedule prefetch 2 minutes before notification
|
||||||
@@ -1465,7 +1418,7 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
val triggerAtMillis = options.getLong("triggerAtMillis") ?: return call.reject("triggerAtMillis is required")
|
val triggerAtMillis = options.getLong("triggerAtMillis") ?: return call.reject("triggerAtMillis is required")
|
||||||
|
|
||||||
val context = context ?: return call.reject("Context not available")
|
val context = context ?: return call.reject("Context not available")
|
||||||
val isScheduled = NotifyReceiver.isAlarmScheduled(context, triggerAtMillis)
|
val isScheduled = NotifyReceiver.isAlarmScheduled(context, triggerAtMillis = triggerAtMillis)
|
||||||
|
|
||||||
val result = JSObject().apply {
|
val result = JSObject().apply {
|
||||||
put("scheduled", isScheduled)
|
put("scheduled", isScheduled)
|
||||||
@@ -1534,6 +1487,112 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test method: Inject invalid data into database for recovery testing
|
||||||
|
*
|
||||||
|
* This method is used by TEST 4 to verify that recovery handles invalid
|
||||||
|
* data gracefully (empty IDs, null nextRunAt, etc.) without crashing.
|
||||||
|
*
|
||||||
|
* @param call Plugin call with optional parameters:
|
||||||
|
* - injectEmptyScheduleId: boolean (default: true) - inject schedule with empty ID
|
||||||
|
* - injectNullNextRunAt: boolean (default: true) - inject schedule with null nextRunAt
|
||||||
|
* - injectEmptyNotificationId: boolean (default: true) - inject notification with empty ID
|
||||||
|
*/
|
||||||
|
@PluginMethod
|
||||||
|
fun injectInvalidTestData(call: PluginCall) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val options = call.data
|
||||||
|
val injectEmptyScheduleId = options?.getBoolean("injectEmptyScheduleId") ?: true
|
||||||
|
val injectNullNextRunAt = options?.getBoolean("injectNullNextRunAt") ?: true
|
||||||
|
val injectEmptyNotificationId = options?.getBoolean("injectEmptyNotificationId") ?: true
|
||||||
|
|
||||||
|
val db = getDatabase()
|
||||||
|
val injected = mutableListOf<String>()
|
||||||
|
|
||||||
|
// Inject schedule with empty ID
|
||||||
|
if (injectEmptyScheduleId) {
|
||||||
|
try {
|
||||||
|
val invalidSchedule = Schedule(
|
||||||
|
id = "", // Empty ID - should be skipped by recovery
|
||||||
|
kind = "notify",
|
||||||
|
cron = "0 9 * * *",
|
||||||
|
clockTime = "09:00",
|
||||||
|
enabled = true,
|
||||||
|
nextRunAt = System.currentTimeMillis() + 86400000L
|
||||||
|
)
|
||||||
|
db.scheduleDao().upsert(invalidSchedule)
|
||||||
|
injected.add("empty_schedule_id")
|
||||||
|
Log.i(TAG, "TEST: Injected schedule with empty ID")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "TEST: Failed to inject empty schedule ID", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject schedule with null nextRunAt
|
||||||
|
if (injectNullNextRunAt) {
|
||||||
|
try {
|
||||||
|
val invalidSchedule = Schedule(
|
||||||
|
id = "test_null_nextrunat",
|
||||||
|
kind = "notify",
|
||||||
|
cron = "0 9 * * *",
|
||||||
|
clockTime = "09:00",
|
||||||
|
enabled = true,
|
||||||
|
nextRunAt = null // Null nextRunAt - should be skipped by recovery
|
||||||
|
)
|
||||||
|
db.scheduleDao().upsert(invalidSchedule)
|
||||||
|
injected.add("null_nextrunat")
|
||||||
|
Log.i(TAG, "TEST: Injected schedule with null nextRunAt")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "TEST: Failed to inject null nextRunAt", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject notification with empty ID
|
||||||
|
// Note: Room's @NonNull constraint may prevent this, but we try anyway
|
||||||
|
// If it fails, the other invalid data types (null nextRunAt) will still test recovery
|
||||||
|
if (injectEmptyNotificationId) {
|
||||||
|
try {
|
||||||
|
val invalidNotification =
|
||||||
|
com.timesafari.dailynotification.entities.NotificationContentEntity()
|
||||||
|
invalidNotification.id = "" // Empty ID - should be skipped by recovery
|
||||||
|
invalidNotification.title = "Test Invalid Notification"
|
||||||
|
invalidNotification.body = "This has an empty ID"
|
||||||
|
invalidNotification.scheduledTime = System.currentTimeMillis() - 3600000L // 1 hour ago
|
||||||
|
invalidNotification.deliveryStatus = "pending"
|
||||||
|
invalidNotification.deliveryAttempts = 0
|
||||||
|
invalidNotification.lastDeliveryAttempt = 0
|
||||||
|
invalidNotification.userInteractionCount = 0
|
||||||
|
invalidNotification.lastUserInteraction = 0
|
||||||
|
invalidNotification.ttlSeconds = 86400L
|
||||||
|
invalidNotification.createdAt = System.currentTimeMillis()
|
||||||
|
invalidNotification.updatedAt = System.currentTimeMillis()
|
||||||
|
|
||||||
|
db.notificationContentDao().insertNotification(invalidNotification)
|
||||||
|
injected.add("empty_notification_id")
|
||||||
|
Log.i(TAG, "TEST: Injected notification with empty ID")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "TEST: Failed to inject empty notification ID (Room @NonNull constraint may prevent this): ${e.message}")
|
||||||
|
Log.i(TAG, "TEST: Other invalid data types (null nextRunAt, empty schedule ID) will still test recovery")
|
||||||
|
// This is expected - Room may reject empty primary keys
|
||||||
|
// The other invalid data types will still test recovery handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = JSObject().apply {
|
||||||
|
put("success", true)
|
||||||
|
put("injected", JSONArray(injected))
|
||||||
|
put("message", "Invalid test data injected: ${injected.joinToString(", ")}")
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to inject invalid test data", e)
|
||||||
|
call.reject("Failed to inject invalid test data: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun scheduleUserNotification(call: PluginCall) {
|
fun scheduleUserNotification(call: PluginCall) {
|
||||||
try {
|
try {
|
||||||
@@ -1593,12 +1652,21 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
try {
|
try {
|
||||||
val nextRunTime = calculateNextRunTime(config.schedule)
|
val nextRunTime = calculateNextRunTime(config.schedule)
|
||||||
|
|
||||||
|
// Generate scheduleId before scheduling (needed for stable requestCode)
|
||||||
|
val scheduleId = "notify_${System.currentTimeMillis()}"
|
||||||
|
|
||||||
// Schedule AlarmManager notification
|
// Schedule AlarmManager notification
|
||||||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
NotifyReceiver.scheduleExactNotification(
|
||||||
|
context,
|
||||||
|
nextRunTime,
|
||||||
|
config,
|
||||||
|
scheduleId = scheduleId,
|
||||||
|
source = ScheduleSource.INITIAL_SETUP
|
||||||
|
)
|
||||||
|
|
||||||
// Store schedule in database
|
// Store schedule in database
|
||||||
val schedule = Schedule(
|
val schedule = Schedule(
|
||||||
id = "notify_${System.currentTimeMillis()}",
|
id = scheduleId,
|
||||||
kind = "notify",
|
kind = "notify",
|
||||||
cron = config.schedule,
|
cron = config.schedule,
|
||||||
enabled = config.enabled,
|
enabled = config.enabled,
|
||||||
@@ -1682,7 +1750,14 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
FetchWorker.scheduleFetch(context, contentFetchConfig)
|
FetchWorker.scheduleFetch(context, contentFetchConfig)
|
||||||
|
|
||||||
val nextRunTime = calculateNextRunTime(userNotificationConfig.schedule)
|
val nextRunTime = calculateNextRunTime(userNotificationConfig.schedule)
|
||||||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, userNotificationConfig)
|
val scheduleId = "notify_${System.currentTimeMillis()}"
|
||||||
|
NotifyReceiver.scheduleExactNotification(
|
||||||
|
context,
|
||||||
|
nextRunTime,
|
||||||
|
userNotificationConfig,
|
||||||
|
scheduleId = scheduleId,
|
||||||
|
source = ScheduleSource.INITIAL_SETUP
|
||||||
|
)
|
||||||
|
|
||||||
// Store both schedules
|
// Store both schedules
|
||||||
val fetchSchedule = Schedule(
|
val fetchSchedule = Schedule(
|
||||||
@@ -1859,6 +1934,57 @@ open class DailyNotificationPlugin : Plugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all schedules with their AlarmManager status
|
||||||
|
* Returns schedules from database with isActuallyScheduled flag for each
|
||||||
|
*/
|
||||||
|
@PluginMethod
|
||||||
|
fun getSchedulesWithStatus(call: PluginCall) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val options = call.getObject("options")
|
||||||
|
val kind = options?.getString("kind")
|
||||||
|
val enabled = options?.getBoolean("enabled")
|
||||||
|
|
||||||
|
val context = context ?: return@launch call.reject("Context not available")
|
||||||
|
|
||||||
|
val schedules = when {
|
||||||
|
kind != null && enabled != null ->
|
||||||
|
getDatabase().scheduleDao().getByKindAndEnabled(kind, enabled)
|
||||||
|
kind != null ->
|
||||||
|
getDatabase().scheduleDao().getByKind(kind)
|
||||||
|
enabled != null ->
|
||||||
|
if (enabled) getDatabase().scheduleDao().getEnabled() else getDatabase().scheduleDao().getAll().filter { !it.enabled }
|
||||||
|
else ->
|
||||||
|
getDatabase().scheduleDao().getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each schedule, check if it's actually scheduled in AlarmManager
|
||||||
|
val schedulesArray = org.json.JSONArray()
|
||||||
|
schedules.forEach { schedule ->
|
||||||
|
val scheduleJson = scheduleToJson(schedule)
|
||||||
|
|
||||||
|
// Only check AlarmManager status for "notify" schedules with nextRunAt
|
||||||
|
if (schedule.kind == "notify" && schedule.nextRunAt != null) {
|
||||||
|
val isScheduled = NotifyReceiver.isAlarmScheduled(context, scheduleId = schedule.id, triggerAtMillis = schedule.nextRunAt!!)
|
||||||
|
scheduleJson.put("isActuallyScheduled", isScheduled)
|
||||||
|
} else {
|
||||||
|
scheduleJson.put("isActuallyScheduled", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulesArray.put(scheduleJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve(JSObject().apply {
|
||||||
|
put("schedules", schedulesArray)
|
||||||
|
})
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get schedules with status", e)
|
||||||
|
call.reject("Failed to get schedules with status: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun createSchedule(call: PluginCall) {
|
fun createSchedule(call: PluginCall) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|||||||
@@ -386,6 +386,9 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
|
|||||||
/**
|
/**
|
||||||
* Schedule the next occurrence of this daily notification
|
* Schedule the next occurrence of this daily notification
|
||||||
*
|
*
|
||||||
|
* Uses centralized NotifyReceiver.scheduleExactNotification() with ROLLOVER_ON_FIRE source
|
||||||
|
* to ensure idempotence and proper logging
|
||||||
|
*
|
||||||
* @param context Application context
|
* @param context Application context
|
||||||
* @param content Current notification content
|
* @param content Current notification content
|
||||||
*/
|
*/
|
||||||
@@ -393,42 +396,114 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
Log.d(TAG, "Scheduling next notification for: " + content.getId());
|
Log.d(TAG, "Scheduling next notification for: " + content.getId());
|
||||||
|
|
||||||
// Calculate next occurrence (24 hours from now)
|
// Extract scheduleId from notificationId pattern or use fallback
|
||||||
|
// Notification IDs are often "daily_${scheduleId}"
|
||||||
|
String scheduleId = null;
|
||||||
|
String cronExpression = null;
|
||||||
long nextScheduledTime = content.getScheduledTime() + (24 * 60 * 60 * 1000);
|
long nextScheduledTime = content.getScheduledTime() + (24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
// Create new content for next occurrence
|
// Try to extract scheduleId from notificationId (e.g., "daily_1764578136269")
|
||||||
NotificationContent nextContent = new NotificationContent();
|
String notificationId = content.getId();
|
||||||
nextContent.setTitle(content.getTitle());
|
if (notificationId != null && notificationId.startsWith("daily_")) {
|
||||||
nextContent.setBody(content.getBody());
|
scheduleId = notificationId; // Use notificationId as scheduleId
|
||||||
nextContent.setScheduledTime(nextScheduledTime);
|
} else {
|
||||||
nextContent.setSound(content.isSound());
|
scheduleId = "daily_rollover_" + System.currentTimeMillis();
|
||||||
nextContent.setPriority(content.getPriority());
|
}
|
||||||
nextContent.setUrl(content.getUrl());
|
|
||||||
// fetchedAt is set in constructor, no need to set it again
|
|
||||||
|
|
||||||
// Save to storage
|
// Calculate cron from current scheduled time (extract hour:minute)
|
||||||
DailyNotificationStorage storage = new DailyNotificationStorage(context);
|
try {
|
||||||
storage.saveNotificationContent(nextContent);
|
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||||
|
cal.setTimeInMillis(content.getScheduledTime());
|
||||||
|
int hour = cal.get(java.util.Calendar.HOUR_OF_DAY);
|
||||||
|
int minute = cal.get(java.util.Calendar.MINUTE);
|
||||||
|
cronExpression = String.format("%d %d * * *", minute, hour);
|
||||||
|
|
||||||
|
// Recalculate next run time from cron (tomorrow at same time)
|
||||||
|
nextScheduledTime = calculateNextRunTimeFromCron(cronExpression);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Failed to calculate cron from scheduled time, using default", e);
|
||||||
|
cronExpression = "0 9 * * *"; // Default to 9 AM
|
||||||
|
}
|
||||||
|
|
||||||
// Schedule the notification
|
// Create config for next notification
|
||||||
DailyNotificationScheduler scheduler = new DailyNotificationScheduler(
|
com.timesafari.dailynotification.UserNotificationConfig config =
|
||||||
context,
|
new com.timesafari.dailynotification.UserNotificationConfig(
|
||||||
(android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE)
|
true, // enabled
|
||||||
|
cronExpression,
|
||||||
|
content.getTitle() != null ? content.getTitle() : "Daily Notification",
|
||||||
|
content.getBody(),
|
||||||
|
content.isSound(),
|
||||||
|
true, // vibration
|
||||||
|
content.getPriority() != null ? content.getPriority() : "normal"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use centralized scheduling function with ROLLOVER_ON_FIRE source
|
||||||
|
com.timesafari.dailynotification.NotifyReceiver.scheduleExactNotification(
|
||||||
|
context,
|
||||||
|
nextScheduledTime,
|
||||||
|
config,
|
||||||
|
false, // isStaticReminder
|
||||||
|
null, // reminderId
|
||||||
|
scheduleId,
|
||||||
|
com.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE
|
||||||
);
|
);
|
||||||
|
|
||||||
boolean scheduled = scheduler.scheduleNotification(nextContent);
|
Log.i(TAG, "Next notification scheduled via centralized function: scheduleId=" + scheduleId);
|
||||||
|
|
||||||
if (scheduled) {
|
|
||||||
Log.i(TAG, "Next notification scheduled successfully");
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Failed to schedule next notification");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error scheduling next notification", e);
|
Log.e(TAG, "Error scheduling next notification", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to convert HH:mm time to cron expression
|
||||||
|
*/
|
||||||
|
private String convertTimeToCron(String clockTime) {
|
||||||
|
try {
|
||||||
|
String[] parts = clockTime.split(":");
|
||||||
|
if (parts.length == 2) {
|
||||||
|
int hour = Integer.parseInt(parts[0]);
|
||||||
|
int minute = Integer.parseInt(parts[1]);
|
||||||
|
return String.format("%d %d * * *", minute, hour);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Failed to parse clockTime: " + clockTime, e);
|
||||||
|
}
|
||||||
|
return "0 9 * * *"; // Default to 9 AM
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to calculate next run time from cron expression
|
||||||
|
*/
|
||||||
|
private long calculateNextRunTimeFromCron(String cron) {
|
||||||
|
try {
|
||||||
|
String[] parts = cron.trim().split("\\s+");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
int minute = Integer.parseInt(parts[0]);
|
||||||
|
int hour = Integer.parseInt(parts[1]);
|
||||||
|
|
||||||
|
java.util.Calendar calendar = java.util.Calendar.getInstance();
|
||||||
|
long now = calendar.getTimeInMillis();
|
||||||
|
|
||||||
|
calendar.set(java.util.Calendar.HOUR_OF_DAY, hour);
|
||||||
|
calendar.set(java.util.Calendar.MINUTE, minute);
|
||||||
|
calendar.set(java.util.Calendar.SECOND, 0);
|
||||||
|
calendar.set(java.util.Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
long nextRun = calendar.getTimeInMillis();
|
||||||
|
if (nextRun <= now) {
|
||||||
|
calendar.add(java.util.Calendar.DAY_OF_YEAR, 1);
|
||||||
|
nextRun = calendar.getTimeInMillis();
|
||||||
|
}
|
||||||
|
return nextRun;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Failed to calculate next run time from cron: " + cron, e);
|
||||||
|
}
|
||||||
|
// Fallback: 24 hours from now
|
||||||
|
return System.currentTimeMillis() + (24 * 60 * 60 * 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get notification priority constant
|
* Get notification priority constant
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ public class DailyNotificationScheduler {
|
|||||||
*/
|
*/
|
||||||
private boolean scheduleExactAlarm(PendingIntent pendingIntent, long triggerTime) {
|
private boolean scheduleExactAlarm(PendingIntent pendingIntent, long triggerTime) {
|
||||||
try {
|
try {
|
||||||
|
// WARNING: This is the OLD scheduler - should be replaced with NotifyReceiver.scheduleExactNotification()
|
||||||
|
// Deep logging to identify if this path is still being called (should not be for daily notifications)
|
||||||
|
Log.w(TAG, "LEGACY SCHEDULER CALLED: Scheduling OS alarm: variant=LEGACY_SCHEDULER, triggerTime=" + triggerTime + ", pendingIntentHash=" + pendingIntent.hashCode());
|
||||||
|
Log.w(TAG, "This should NOT be called for daily notifications - use NotifyReceiver.scheduleExactNotification() instead");
|
||||||
|
|
||||||
// Enhanced exact alarm scheduling for Android 12+ and Doze mode
|
// Enhanced exact alarm scheduling for Android 12+ and Doze mode
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// Use setExactAndAllowWhileIdle for Doze mode compatibility
|
// Use setExactAndAllowWhileIdle for Doze mode compatibility
|
||||||
|
|||||||
@@ -540,65 +540,89 @@ public class DailyNotificationWorker extends Worker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new content for next occurrence
|
// Extract scheduleId from notificationId pattern or use fallback
|
||||||
NotificationContent nextContent = new NotificationContent();
|
// Notification IDs are often "daily_${scheduleId}"
|
||||||
nextContent.setTitle(content.getTitle());
|
String scheduleId = null;
|
||||||
nextContent.setBody(content.getBody());
|
String cronExpression = null;
|
||||||
nextContent.setScheduledTime(nextScheduledTime);
|
|
||||||
nextContent.setSound(content.isSound());
|
|
||||||
nextContent.setPriority(content.getPriority());
|
|
||||||
nextContent.setUrl(content.getUrl());
|
|
||||||
// fetchedAt is set in constructor, no need to set it again
|
|
||||||
|
|
||||||
// Save to Room (authoritative) and legacy storage (compat)
|
// Try to extract scheduleId from notificationId (e.g., "daily_1764578136269")
|
||||||
saveNextToRoom(nextContent);
|
String notificationId = content.getId();
|
||||||
DailyNotificationStorage legacyStorage2 = new DailyNotificationStorage(getApplicationContext());
|
if (notificationId != null && notificationId.startsWith("daily_")) {
|
||||||
legacyStorage2.saveNotificationContent(nextContent);
|
scheduleId = notificationId; // Use notificationId as scheduleId
|
||||||
|
} else {
|
||||||
|
scheduleId = "daily_rollover_" + System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
// Schedule the notification
|
// Calculate cron from current scheduled time (extract hour:minute)
|
||||||
DailyNotificationScheduler scheduler = new DailyNotificationScheduler(
|
try {
|
||||||
getApplicationContext(),
|
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||||
(android.app.AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE)
|
cal.setTimeInMillis(content.getScheduledTime());
|
||||||
|
int hour = cal.get(java.util.Calendar.HOUR_OF_DAY);
|
||||||
|
int minute = cal.get(java.util.Calendar.MINUTE);
|
||||||
|
cronExpression = String.format("%d %d * * *", minute, hour);
|
||||||
|
|
||||||
|
// Recalculate next run time from cron (tomorrow at same time)
|
||||||
|
nextScheduledTime = calculateNextRunTimeFromCron(cronExpression);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Failed to calculate cron from scheduled time, using default", e);
|
||||||
|
cronExpression = "0 9 * * *"; // Default to 9 AM
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create config for next notification
|
||||||
|
com.timesafari.dailynotification.UserNotificationConfig config =
|
||||||
|
new com.timesafari.dailynotification.UserNotificationConfig(
|
||||||
|
true, // enabled
|
||||||
|
cronExpression,
|
||||||
|
content.getTitle() != null ? content.getTitle() : "Daily Notification",
|
||||||
|
content.getBody(),
|
||||||
|
content.isSound(),
|
||||||
|
true, // vibration
|
||||||
|
content.getPriority() != null ? content.getPriority() : "normal"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use centralized scheduling function with ROLLOVER_ON_FIRE source
|
||||||
|
com.timesafari.dailynotification.NotifyReceiver.scheduleExactNotification(
|
||||||
|
getApplicationContext(),
|
||||||
|
nextScheduledTime,
|
||||||
|
config,
|
||||||
|
false, // isStaticReminder
|
||||||
|
null, // reminderId
|
||||||
|
scheduleId,
|
||||||
|
com.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE
|
||||||
);
|
);
|
||||||
|
|
||||||
boolean scheduled = scheduler.scheduleNotification(nextContent);
|
// Log next scheduled time in readable format
|
||||||
|
String nextTimeStr = formatScheduledTime(nextScheduledTime);
|
||||||
|
Log.i(TAG, "DN|RESCHEDULE_OK id=" + content.getId() + " next=" + nextTimeStr + " scheduleId=" + scheduleId);
|
||||||
|
|
||||||
if (scheduled) {
|
// Schedule background fetch for next notification (5 minutes before scheduled time)
|
||||||
// Log next scheduled time in readable format
|
try {
|
||||||
String nextTimeStr = formatScheduledTime(nextScheduledTime);
|
DailyNotificationStorage storageForFetcher = new DailyNotificationStorage(getApplicationContext());
|
||||||
Log.i(TAG, "DN|RESCHEDULE_OK id=" + content.getId() + " next=" + nextTimeStr);
|
DailyNotificationStorageRoom roomStorageForFetcher = new DailyNotificationStorageRoom(getApplicationContext());
|
||||||
|
DailyNotificationFetcher fetcher = new DailyNotificationFetcher(
|
||||||
|
getApplicationContext(),
|
||||||
|
storageForFetcher,
|
||||||
|
roomStorageForFetcher
|
||||||
|
);
|
||||||
|
|
||||||
// Schedule background fetch for next notification (5 minutes before scheduled time)
|
// Calculate fetch time (5 minutes before notification)
|
||||||
try {
|
long fetchTime = nextScheduledTime - TimeUnit.MINUTES.toMillis(5);
|
||||||
DailyNotificationStorage storageForFetcher = new DailyNotificationStorage(getApplicationContext());
|
long currentTime = System.currentTimeMillis();
|
||||||
DailyNotificationStorageRoom roomStorageForFetcher = new DailyNotificationStorageRoom(getApplicationContext());
|
|
||||||
DailyNotificationFetcher fetcher = new DailyNotificationFetcher(
|
if (fetchTime > currentTime) {
|
||||||
getApplicationContext(),
|
fetcher.scheduleFetch(fetchTime);
|
||||||
storageForFetcher,
|
Log.i(TAG, "DN|RESCHEDULE_PREFETCH_SCHEDULED id=" + content.getId() +
|
||||||
roomStorageForFetcher
|
" next_fetch=" + fetchTime +
|
||||||
);
|
" next_notification=" + nextScheduledTime);
|
||||||
|
} else {
|
||||||
// Calculate fetch time (5 minutes before notification)
|
Log.w(TAG, "DN|RESCHEDULE_PREFETCH_PAST id=" + content.getId() +
|
||||||
long fetchTime = nextScheduledTime - TimeUnit.MINUTES.toMillis(5);
|
" fetch_time=" + fetchTime +
|
||||||
long currentTime = System.currentTimeMillis();
|
" current=" + currentTime);
|
||||||
|
fetcher.scheduleImmediateFetch();
|
||||||
if (fetchTime > currentTime) {
|
|
||||||
fetcher.scheduleFetch(fetchTime);
|
|
||||||
Log.i(TAG, "DN|RESCHEDULE_PREFETCH_SCHEDULED id=" + content.getId() +
|
|
||||||
" next_fetch=" + fetchTime +
|
|
||||||
" next_notification=" + nextScheduledTime);
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "DN|RESCHEDULE_PREFETCH_PAST id=" + content.getId() +
|
|
||||||
" fetch_time=" + fetchTime +
|
|
||||||
" current=" + currentTime);
|
|
||||||
fetcher.scheduleImmediateFetch();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "DN|RESCHEDULE_PREFETCH_ERR id=" + content.getId() +
|
|
||||||
" error scheduling prefetch", e);
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "DN|RESCHEDULE_ERR id=" + content.getId());
|
Log.e(TAG, "DN|RESCHEDULE_PREFETCH_ERR id=" + content.getId() +
|
||||||
|
" error scheduling prefetch", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -737,6 +761,55 @@ public class DailyNotificationWorker extends Worker {
|
|||||||
* @param scheduledTime Epoch millis
|
* @param scheduledTime Epoch millis
|
||||||
* @return Formatted time string
|
* @return Formatted time string
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Helper to convert HH:mm time to cron expression
|
||||||
|
*/
|
||||||
|
private String convertTimeToCron(String clockTime) {
|
||||||
|
try {
|
||||||
|
String[] parts = clockTime.split(":");
|
||||||
|
if (parts.length == 2) {
|
||||||
|
int hour = Integer.parseInt(parts[0]);
|
||||||
|
int minute = Integer.parseInt(parts[1]);
|
||||||
|
return String.format("%d %d * * *", minute, hour);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Failed to parse clockTime: " + clockTime, e);
|
||||||
|
}
|
||||||
|
return "0 9 * * *"; // Default to 9 AM
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to calculate next run time from cron expression
|
||||||
|
*/
|
||||||
|
private long calculateNextRunTimeFromCron(String cron) {
|
||||||
|
try {
|
||||||
|
String[] parts = cron.trim().split("\\s+");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
int minute = Integer.parseInt(parts[0]);
|
||||||
|
int hour = Integer.parseInt(parts[1]);
|
||||||
|
|
||||||
|
java.util.Calendar calendar = java.util.Calendar.getInstance();
|
||||||
|
long now = calendar.getTimeInMillis();
|
||||||
|
|
||||||
|
calendar.set(java.util.Calendar.HOUR_OF_DAY, hour);
|
||||||
|
calendar.set(java.util.Calendar.MINUTE, minute);
|
||||||
|
calendar.set(java.util.Calendar.SECOND, 0);
|
||||||
|
calendar.set(java.util.Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
long nextRun = calendar.getTimeInMillis();
|
||||||
|
if (nextRun <= now) {
|
||||||
|
calendar.add(java.util.Calendar.DAY_OF_YEAR, 1);
|
||||||
|
nextRun = calendar.getTimeInMillis();
|
||||||
|
}
|
||||||
|
return nextRun;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Failed to calculate next run time from cron: " + cron, e);
|
||||||
|
}
|
||||||
|
// Fallback: use DST-safe calculation
|
||||||
|
return calculateNextScheduledTime(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
private String formatScheduledTime(long scheduledTime) {
|
private String formatScheduledTime(long scheduledTime) {
|
||||||
try {
|
try {
|
||||||
ZonedDateTime zoned = ZonedDateTime.ofInstant(
|
ZonedDateTime zoned = ZonedDateTime.ofInstant(
|
||||||
|
|||||||
@@ -23,18 +23,48 @@ import kotlinx.coroutines.runBlocking
|
|||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
* @version 1.1.0
|
* @version 1.1.0
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Source of schedule request - tracks which code path triggered scheduling
|
||||||
|
* Used for debugging duplicate alarm issues
|
||||||
|
*/
|
||||||
|
enum class ScheduleSource {
|
||||||
|
INITIAL_SETUP, // User schedules initial daily notification
|
||||||
|
ROLLOVER_ON_FIRE, // Notification fired, scheduling next day
|
||||||
|
APP_LAUNCH_RECOVERY, // App launched, recovering from DB
|
||||||
|
BOOT_RECOVERY, // Device booted, recovering from DB
|
||||||
|
APP_RESUME_INIT, // App resumed, initialization/ensure-schedule path
|
||||||
|
MANUAL_RESCHEDULE, // Manual reschedule (e.g., time change)
|
||||||
|
TEST_NOTIFICATION // Test notification scheduling
|
||||||
|
}
|
||||||
|
|
||||||
class NotifyReceiver : BroadcastReceiver() {
|
class NotifyReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "DNP-NOTIFY"
|
private const val TAG = "DNP-NOTIFY"
|
||||||
|
private const val SCHEDULE_TAG = "DNP-SCHEDULE"
|
||||||
private const val CHANNEL_ID = "daily_notifications"
|
private const val CHANNEL_ID = "daily_notifications"
|
||||||
private const val NOTIFICATION_ID = 1001
|
private const val NOTIFICATION_ID = 1001
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate unique request code from trigger time
|
* Generate stable request code from scheduleId
|
||||||
* Uses lower 16 bits of timestamp to ensure uniqueness
|
* Uses scheduleId hash to ensure same schedule always gets same requestCode
|
||||||
|
* This prevents duplicate alarms when same schedule is scheduled multiple times
|
||||||
|
*
|
||||||
|
* @param scheduleId Stable identifier for the schedule (e.g., "daily_reminder_1")
|
||||||
|
* @return Request code for PendingIntent (uses lower 16 bits of hash)
|
||||||
*/
|
*/
|
||||||
private fun getRequestCode(triggerAtMillis: Long): Int {
|
private fun getRequestCode(scheduleId: String): Int {
|
||||||
|
// Use scheduleId hash for stability - same schedule = same requestCode
|
||||||
|
// This ensures FLAG_UPDATE_CURRENT works correctly to replace existing alarms
|
||||||
|
return (scheduleId.hashCode() and 0xFFFF).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy: Generate request code from trigger time (for backward compatibility)
|
||||||
|
* @deprecated Use getRequestCode(scheduleId) instead for stable request codes
|
||||||
|
*/
|
||||||
|
@Deprecated("Use getRequestCode(scheduleId) for stable request codes")
|
||||||
|
private fun getRequestCodeFromTime(triggerAtMillis: Long): Int {
|
||||||
return (triggerAtMillis and 0xFFFF).toInt()
|
return (triggerAtMillis and 0xFFFF).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,69 +113,160 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
* FIX: Uses DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
|
* FIX: Uses DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
|
||||||
* Stores notification content in database and passes notification ID to receiver
|
* Stores notification content in database and passes notification ID to receiver
|
||||||
*
|
*
|
||||||
|
* Includes idempotence check to prevent duplicate alarms for same schedule
|
||||||
|
*
|
||||||
* @param context Application context
|
* @param context Application context
|
||||||
* @param triggerAtMillis When to trigger the notification (UTC milliseconds)
|
* @param triggerAtMillis When to trigger the notification (UTC milliseconds)
|
||||||
* @param config Notification configuration
|
* @param config Notification configuration
|
||||||
* @param isStaticReminder Whether this is a static reminder (no content dependency)
|
* @param isStaticReminder Whether this is a static reminder (no content dependency)
|
||||||
* @param reminderId Optional reminder ID for tracking
|
* @param reminderId Optional reminder ID for tracking (used as scheduleId if provided)
|
||||||
|
* @param scheduleId Stable identifier for the schedule (used for requestCode stability)
|
||||||
|
* @param source Source of the scheduling request (for debugging duplicate alarms)
|
||||||
*/
|
*/
|
||||||
|
@JvmStatic
|
||||||
fun scheduleExactNotification(
|
fun scheduleExactNotification(
|
||||||
context: Context,
|
context: Context,
|
||||||
triggerAtMillis: Long,
|
triggerAtMillis: Long,
|
||||||
config: UserNotificationConfig,
|
config: UserNotificationConfig,
|
||||||
isStaticReminder: Boolean = false,
|
isStaticReminder: Boolean = false,
|
||||||
reminderId: String? = null
|
reminderId: String? = null,
|
||||||
|
scheduleId: String? = null,
|
||||||
|
source: ScheduleSource = ScheduleSource.MANUAL_RESCHEDULE
|
||||||
) {
|
) {
|
||||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
|
|
||||||
|
// Generate stable scheduleId - prefer provided scheduleId, then reminderId, then generate from time
|
||||||
|
// This ensures same schedule always uses same ID for idempotence checks
|
||||||
|
val stableScheduleId = scheduleId ?: reminderId ?: "daily_${triggerAtMillis}"
|
||||||
|
|
||||||
// Generate notification ID (use reminderId if provided, otherwise generate from trigger time)
|
// Generate notification ID (use reminderId if provided, otherwise generate from trigger time)
|
||||||
val notificationId = reminderId ?: "notify_${triggerAtMillis}"
|
val notificationId = reminderId ?: "notify_${triggerAtMillis}"
|
||||||
|
|
||||||
// Store notification content in database before scheduling alarm
|
// IDEMPOTENCE CHECK: Verify no existing alarm for this trigger time before scheduling
|
||||||
// This allows DailyNotificationReceiver to retrieve content via notification ID
|
// This prevents duplicate alarms when multiple scheduling paths race
|
||||||
// FIX: Wrap suspend function calls in coroutine
|
// Strategy: Check both by scheduleId (stable) and by trigger time (catches different scheduleIds for same time)
|
||||||
if (!isStaticReminder) {
|
val requestCode = getRequestCode(stableScheduleId)
|
||||||
try {
|
val checkIntent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
||||||
// Use runBlocking to call suspend function from non-suspend context
|
action = "com.timesafari.daily.NOTIFICATION"
|
||||||
// This is acceptable here because we're not in a UI thread and need to ensure
|
}
|
||||||
// content is stored before scheduling the alarm
|
|
||||||
runBlocking {
|
// Check 1: Same scheduleId (stable requestCode) - most reliable
|
||||||
val db = DailyNotificationDatabase.getDatabase(context)
|
var existingPendingIntent = PendingIntent.getBroadcast(
|
||||||
val contentCache = db.contentCacheDao().getLatest()
|
context,
|
||||||
|
requestCode,
|
||||||
// If we have cached content, create a notification content entity
|
checkIntent,
|
||||||
if (contentCache != null) {
|
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
val roomStorage = com.timesafari.dailynotification.storage.DailyNotificationStorageRoom(context)
|
)
|
||||||
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
|
||||||
notificationId,
|
// Check 2: If no match by scheduleId, check by trigger time (within 1 minute tolerance)
|
||||||
"1.0.2", // Plugin version
|
// This catches cases where different scheduleIds are used for the same time
|
||||||
null, // timesafariDid - can be set if available
|
// Try a range of request codes around the trigger time
|
||||||
"daily",
|
if (existingPendingIntent == null) {
|
||||||
config.title,
|
val timeBasedRequestCode = getRequestCodeFromTime(triggerAtMillis)
|
||||||
config.body ?: String(contentCache.payload),
|
existingPendingIntent = PendingIntent.getBroadcast(
|
||||||
triggerAtMillis,
|
context,
|
||||||
java.time.ZoneId.systemDefault().id
|
timeBasedRequestCode,
|
||||||
)
|
checkIntent,
|
||||||
entity.priority = when (config.priority) {
|
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
"high", "max" -> 2
|
)
|
||||||
"low", "min" -> -1
|
}
|
||||||
else -> 0
|
|
||||||
}
|
// Check 3: Also check if AlarmManager already has an alarm for this exact time
|
||||||
entity.vibrationEnabled = config.vibration ?: true
|
// This is a fallback for when PendingIntent checks fail but alarm still exists
|
||||||
entity.soundEnabled = config.sound ?: true
|
// We check the next alarm clock time (Android 5.0+)
|
||||||
entity.deliveryStatus = "pending"
|
if (existingPendingIntent == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
entity.createdAt = System.currentTimeMillis()
|
val nextAlarm = alarmManager.nextAlarmClock
|
||||||
entity.updatedAt = System.currentTimeMillis()
|
if (nextAlarm != null) {
|
||||||
entity.ttlSeconds = contentCache.ttlSeconds.toLong()
|
val nextAlarmTime = nextAlarm.triggerTime
|
||||||
|
val timeDiff = Math.abs(nextAlarmTime - triggerAtMillis)
|
||||||
// saveNotificationContent returns CompletableFuture, so we need to wait for it
|
// If there's an alarm within 1 minute of our target time, consider it a duplicate
|
||||||
roomStorage.saveNotificationContent(entity).get()
|
if (timeDiff < 60000) {
|
||||||
Log.d(TAG, "Stored notification content in database: id=$notificationId")
|
val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
|
||||||
|
.format(java.util.Date(triggerAtMillis))
|
||||||
|
Log.w(SCHEDULE_TAG, "Skipping duplicate schedule: id=$stableScheduleId, nextRun=$triggerTimeStr, source=$source")
|
||||||
|
Log.w(SCHEDULE_TAG, "Existing alarm found in AlarmManager at $nextAlarmTime (diff=${timeDiff}ms) - alarm already scheduled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingPendingIntent != null) {
|
||||||
|
val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
|
||||||
|
.format(java.util.Date(triggerAtMillis))
|
||||||
|
Log.w(SCHEDULE_TAG, "Skipping duplicate schedule: id=$stableScheduleId, nextRun=$triggerTimeStr, source=$source")
|
||||||
|
Log.w(SCHEDULE_TAG, "Existing PendingIntent found for requestCode=$requestCode - alarm already scheduled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB-LEVEL IDEMPOTENCE CHECK: Verify no existing schedule for this scheduleId and nextRun
|
||||||
|
// This prevents logical duplicates before even hitting AlarmManager
|
||||||
|
try {
|
||||||
|
runBlocking {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val existingSchedule = db.scheduleDao().getById(stableScheduleId)
|
||||||
|
|
||||||
|
if (existingSchedule != null && existingSchedule.nextRunAt != null) {
|
||||||
|
val timeDiff = Math.abs(existingSchedule.nextRunAt - triggerAtMillis)
|
||||||
|
// If we already have a schedule for this ID with the same nextRun (within 1 minute), skip
|
||||||
|
if (timeDiff < 60000) {
|
||||||
|
val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
|
||||||
|
.format(java.util.Date(triggerAtMillis))
|
||||||
|
Log.w(SCHEDULE_TAG, "Skipping duplicate schedule for id=$stableScheduleId at $triggerTimeStr from source=$source")
|
||||||
|
Log.w(SCHEDULE_TAG, "Existing schedule found in DB: nextRunAt=${existingSchedule.nextRunAt}, diff=${timeDiff}ms")
|
||||||
|
return@runBlocking
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Failed to store notification content in database, continuing with alarm scheduling", e)
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(SCHEDULE_TAG, "DB idempotence check failed, continuing with schedule: $stableScheduleId", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
|
||||||
|
.format(java.util.Date(triggerAtMillis))
|
||||||
|
Log.i(SCHEDULE_TAG, "Scheduling next daily alarm: id=$stableScheduleId, nextRun=$triggerTimeStr, source=$source")
|
||||||
|
|
||||||
|
// Store notification content in database before scheduling alarm
|
||||||
|
// Phase 1: Always create NotificationContentEntity for recovery tracking
|
||||||
|
// This allows recovery to detect missed notifications even for static reminders
|
||||||
|
// Use runBlocking to call suspend function from non-suspend context
|
||||||
|
// This is acceptable here because we're not in a UI thread and need to ensure
|
||||||
|
// content is stored before scheduling the alarm
|
||||||
|
try {
|
||||||
|
runBlocking {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val contentCache = db.contentCacheDao().getLatest()
|
||||||
|
|
||||||
|
// Always create a notification content entity for recovery tracking
|
||||||
|
// Phase 1: Recovery needs NotificationContentEntity to detect missed notifications
|
||||||
|
val roomStorage = com.timesafari.dailynotification.storage.DailyNotificationStorageRoom(context)
|
||||||
|
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
||||||
|
notificationId,
|
||||||
|
"1.0.2", // Plugin version
|
||||||
|
null, // timesafariDid - can be set if available
|
||||||
|
"daily",
|
||||||
|
config.title,
|
||||||
|
config.body ?: (if (contentCache != null) String(contentCache.payload) else ""),
|
||||||
|
triggerAtMillis,
|
||||||
|
java.time.ZoneId.systemDefault().id
|
||||||
|
)
|
||||||
|
entity.priority = when (config.priority) {
|
||||||
|
"high", "max" -> 2
|
||||||
|
"low", "min" -> -1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
entity.vibrationEnabled = config.vibration ?: true
|
||||||
|
entity.soundEnabled = config.sound ?: true
|
||||||
|
entity.deliveryStatus = "pending"
|
||||||
|
entity.createdAt = System.currentTimeMillis()
|
||||||
|
entity.updatedAt = System.currentTimeMillis()
|
||||||
|
entity.ttlSeconds = contentCache?.ttlSeconds?.toLong() ?: (7 * 24 * 60 * 60).toLong() // Default 7 days if no cache
|
||||||
|
|
||||||
|
// saveNotificationContent returns CompletableFuture, so we need to wait for it
|
||||||
|
roomStorage.saveNotificationContent(entity).get()
|
||||||
|
Log.d(TAG, "Stored notification content in database: id=$notificationId (for recovery tracking)")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to store notification content in database, continuing with alarm scheduling", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: Use DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
|
// FIX: Use DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
|
||||||
@@ -153,6 +274,7 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
||||||
action = "com.timesafari.daily.NOTIFICATION" // Must match manifest intent-filter action
|
action = "com.timesafari.daily.NOTIFICATION" // Must match manifest intent-filter action
|
||||||
putExtra("notification_id", notificationId) // DailyNotificationReceiver expects this extra
|
putExtra("notification_id", notificationId) // DailyNotificationReceiver expects this extra
|
||||||
|
putExtra("schedule_id", stableScheduleId) // Add stable scheduleId for tracking
|
||||||
// Also preserve original extras for backward compatibility if needed
|
// Also preserve original extras for backward compatibility if needed
|
||||||
putExtra("title", config.title)
|
putExtra("title", config.title)
|
||||||
putExtra("body", config.body)
|
putExtra("body", config.body)
|
||||||
@@ -166,8 +288,7 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use unique request code based on trigger time to prevent PendingIntent conflicts
|
// requestCode already computed above for idempotence check
|
||||||
val requestCode = getRequestCode(triggerAtMillis)
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
context,
|
context,
|
||||||
requestCode,
|
requestCode,
|
||||||
@@ -175,12 +296,29 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CRITICAL: Cancel any existing alarm for this requestCode BEFORE scheduling
|
||||||
|
// This ensures we don't create duplicate alarms if this function is called multiple times
|
||||||
|
// The idempotence check above should prevent this, but this is a safety net
|
||||||
|
try {
|
||||||
|
val existingPendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
requestCode,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
if (existingPendingIntent != null) {
|
||||||
|
Log.w(SCHEDULE_TAG, "Cancelling existing alarm before rescheduling: requestCode=$requestCode, scheduleId=$stableScheduleId, source=$source")
|
||||||
|
alarmManager.cancel(existingPendingIntent)
|
||||||
|
existingPendingIntent.cancel()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(SCHEDULE_TAG, "Failed to cancel existing alarm before scheduling: $stableScheduleId", e)
|
||||||
|
}
|
||||||
|
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
val delayMs = triggerAtMillis - currentTime
|
val delayMs = triggerAtMillis - currentTime
|
||||||
val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
|
|
||||||
.format(java.util.Date(triggerAtMillis))
|
|
||||||
|
|
||||||
Log.i(TAG, "Scheduling alarm: triggerTime=$triggerTimeStr, delayMs=$delayMs, requestCode=$requestCode")
|
Log.i(TAG, "Scheduling alarm: triggerTime=$triggerTimeStr, delayMs=$delayMs, requestCode=$requestCode, scheduleId=$stableScheduleId")
|
||||||
|
|
||||||
// Check exact alarm permission before scheduling (Android 12+)
|
// Check exact alarm permission before scheduling (Android 12+)
|
||||||
val canScheduleExact = canScheduleExactAlarms(context)
|
val canScheduleExact = canScheduleExactAlarms(context)
|
||||||
@@ -197,8 +335,9 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use setAlarmClock() for Android 5.0+ (API 21+) - most reliable method
|
// ONE-ALARM POLICY: Use only setAlarmClock() for Android 5.0+ (API 21+)
|
||||||
// Shows alarm icon in status bar and is exempt from doze mode
|
// This is the most reliable method and shows alarm icon in status bar
|
||||||
|
// Do NOT also call setExactAndAllowWhileIdle or setExact for the same event
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// Create show intent for alarm clock (opens app when alarm fires)
|
// Create show intent for alarm clock (opens app when alarm fires)
|
||||||
// Use package launcher intent to avoid hardcoding MainActivity class name
|
// Use package launcher intent to avoid hardcoding MainActivity class name
|
||||||
@@ -216,23 +355,36 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val alarmClockInfo = AlarmClockInfo(triggerAtMillis, showPendingIntent)
|
val alarmClockInfo = AlarmClockInfo(triggerAtMillis, showPendingIntent)
|
||||||
|
|
||||||
|
// Deep logging to identify this specific AlarmManager call
|
||||||
|
Log.i(SCHEDULE_TAG, "Scheduling OS alarm: variant=ALARM_CLOCK, action=${intent.action}, triggerTime=$triggerAtMillis, requestCode=$requestCode, scheduleId=$stableScheduleId, source=$source, pendingIntentHash=${pendingIntent.hashCode()}, showIntentHash=${showPendingIntent?.hashCode() ?: 0}")
|
||||||
|
|
||||||
alarmManager.setAlarmClock(alarmClockInfo, pendingIntent)
|
alarmManager.setAlarmClock(alarmClockInfo, pendingIntent)
|
||||||
|
|
||||||
Log.i(TAG, "Alarm clock scheduled (setAlarmClock): triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
Log.i(TAG, "Alarm clock scheduled (setAlarmClock): triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// Fallback to setExactAndAllowWhileIdle for Android 6.0-4.4
|
// Fallback to setExactAndAllowWhileIdle for Android 6.0-4.4 (pre-LOLLIPOP)
|
||||||
|
// Deep logging to identify this specific AlarmManager call
|
||||||
|
Log.i(SCHEDULE_TAG, "Scheduling OS alarm: variant=EXACT_ALLOW_WHILE_IDLE, action=${intent.action}, triggerTime=$triggerAtMillis, requestCode=$requestCode, scheduleId=$stableScheduleId, source=$source, pendingIntentHash=${pendingIntent.hashCode()}")
|
||||||
|
|
||||||
alarmManager.setExactAndAllowWhileIdle(
|
alarmManager.setExactAndAllowWhileIdle(
|
||||||
AlarmManager.RTC_WAKEUP,
|
AlarmManager.RTC_WAKEUP,
|
||||||
triggerAtMillis,
|
triggerAtMillis,
|
||||||
pendingIntent
|
pendingIntent
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.i(TAG, "Exact alarm scheduled (setExactAndAllowWhileIdle): triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
Log.i(TAG, "Exact alarm scheduled (setExactAndAllowWhileIdle): triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
||||||
} else {
|
} else {
|
||||||
// Fallback to setExact for older versions
|
// Fallback to setExact for older versions (pre-M)
|
||||||
|
// Deep logging to identify this specific AlarmManager call
|
||||||
|
Log.i(SCHEDULE_TAG, "Scheduling OS alarm: variant=EXACT, action=${intent.action}, triggerTime=$triggerAtMillis, requestCode=$requestCode, scheduleId=$stableScheduleId, source=$source, pendingIntentHash=${pendingIntent.hashCode()}")
|
||||||
|
|
||||||
alarmManager.setExact(
|
alarmManager.setExact(
|
||||||
AlarmManager.RTC_WAKEUP,
|
AlarmManager.RTC_WAKEUP,
|
||||||
triggerAtMillis,
|
triggerAtMillis,
|
||||||
pendingIntent
|
pendingIntent
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.i(TAG, "Exact alarm scheduled (setExact): triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
Log.i(TAG, "Exact alarm scheduled (setExact): triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
@@ -250,15 +402,23 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
* Cancel a scheduled notification alarm
|
* Cancel a scheduled notification alarm
|
||||||
* FIX: Uses DailyNotificationReceiver to match alarm scheduling
|
* FIX: Uses DailyNotificationReceiver to match alarm scheduling
|
||||||
* @param context Application context
|
* @param context Application context
|
||||||
* @param triggerAtMillis The trigger time of the alarm to cancel (required for unique request code)
|
* @param scheduleId The schedule ID of the alarm to cancel (preferred - uses stable request code)
|
||||||
|
* @param triggerAtMillis The trigger time of the alarm to cancel (fallback - for backward compatibility)
|
||||||
*/
|
*/
|
||||||
fun cancelNotification(context: Context, triggerAtMillis: Long) {
|
fun cancelNotification(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null) {
|
||||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
// FIX: Use DailyNotificationReceiver to match what was scheduled
|
// FIX: Use DailyNotificationReceiver to match what was scheduled
|
||||||
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
||||||
action = "com.timesafari.daily.NOTIFICATION"
|
action = "com.timesafari.daily.NOTIFICATION"
|
||||||
}
|
}
|
||||||
val requestCode = getRequestCode(triggerAtMillis)
|
val requestCode = when {
|
||||||
|
scheduleId != null -> getRequestCode(scheduleId)
|
||||||
|
triggerAtMillis != null -> getRequestCodeFromTime(triggerAtMillis)
|
||||||
|
else -> {
|
||||||
|
Log.e(TAG, "cancelNotification: Must provide either scheduleId or triggerAtMillis")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
context,
|
context,
|
||||||
requestCode,
|
requestCode,
|
||||||
@@ -266,22 +426,30 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
alarmManager.cancel(pendingIntent)
|
alarmManager.cancel(pendingIntent)
|
||||||
Log.i(TAG, "Notification alarm cancelled: triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
Log.i(TAG, "Notification alarm cancelled: scheduleId=$scheduleId, triggerAt=$triggerAtMillis, requestCode=$requestCode")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an alarm is scheduled for the given trigger time
|
* Check if an alarm is scheduled for the given schedule
|
||||||
* FIX: Uses DailyNotificationReceiver to match alarm scheduling
|
* FIX: Uses DailyNotificationReceiver to match alarm scheduling
|
||||||
* @param context Application context
|
* @param context Application context
|
||||||
* @param triggerAtMillis The trigger time to check
|
* @param scheduleId The schedule ID to check (preferred - uses stable request code)
|
||||||
|
* @param triggerAtMillis The trigger time to check (fallback - for backward compatibility)
|
||||||
* @return true if alarm is scheduled, false otherwise
|
* @return true if alarm is scheduled, false otherwise
|
||||||
*/
|
*/
|
||||||
fun isAlarmScheduled(context: Context, triggerAtMillis: Long): Boolean {
|
fun isAlarmScheduled(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null): Boolean {
|
||||||
// FIX: Use DailyNotificationReceiver to match what was scheduled
|
// FIX: Use DailyNotificationReceiver to match what was scheduled
|
||||||
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
||||||
action = "com.timesafari.daily.NOTIFICATION"
|
action = "com.timesafari.daily.NOTIFICATION"
|
||||||
}
|
}
|
||||||
val requestCode = getRequestCode(triggerAtMillis)
|
val requestCode = when {
|
||||||
|
scheduleId != null -> getRequestCode(scheduleId)
|
||||||
|
triggerAtMillis != null -> getRequestCodeFromTime(triggerAtMillis)
|
||||||
|
else -> {
|
||||||
|
Log.e(TAG, "isAlarmScheduled: Must provide either scheduleId or triggerAtMillis")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
context,
|
context,
|
||||||
requestCode,
|
requestCode,
|
||||||
@@ -290,8 +458,11 @@ class NotifyReceiver : BroadcastReceiver() {
|
|||||||
)
|
)
|
||||||
val isScheduled = pendingIntent != null
|
val isScheduled = pendingIntent != null
|
||||||
|
|
||||||
val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
|
val triggerTimeStr = when {
|
||||||
.format(java.util.Date(triggerAtMillis))
|
triggerAtMillis != null -> java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
|
||||||
|
.format(java.util.Date(triggerAtMillis))
|
||||||
|
else -> "scheduleId=$scheduleId"
|
||||||
|
}
|
||||||
Log.d(TAG, "Alarm check for $triggerTimeStr: scheduled=$isScheduled, requestCode=$requestCode")
|
Log.d(TAG, "Alarm check for $triggerTimeStr: scheduled=$isScheduled, requestCode=$requestCode")
|
||||||
|
|
||||||
return isScheduled
|
return isScheduled
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
658
docs/IOS_IMPLEMENTATION_DOCUMENTATION_REVIEW.md
Normal file
658
docs/IOS_IMPLEMENTATION_DOCUMENTATION_REVIEW.md
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
# iOS Implementation Documentation Review
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: December 2024
|
||||||
|
**Status**: 🎯 **ACTIVE** - Documentation Review for iOS Implementation
|
||||||
|
**Purpose**: Ensure Android plugin and test app documentation contains sufficient detail for iOS implementation to mirror all features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document reviews the Android plugin and test app documentation to ensure that when implementing iOS, there is sufficient information to mirror all Android features. The review identifies:
|
||||||
|
|
||||||
|
1. ✅ **Well-documented features** - Sufficient detail for iOS implementation
|
||||||
|
2. ⚠️ **Partially documented features** - Needs additional detail
|
||||||
|
3. ❌ **Missing documentation** - Critical gaps that need to be addressed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Architecture Documentation
|
||||||
|
|
||||||
|
### 1.1 Architecture Overview
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**: `ARCHITECTURE.md`
|
||||||
|
|
||||||
|
**Key Information Provided**:
|
||||||
|
- Plugin architecture with component responsibilities
|
||||||
|
- Data architecture with database schema
|
||||||
|
- Storage implementation details
|
||||||
|
- Security architecture
|
||||||
|
- Performance architecture
|
||||||
|
- Migration strategy
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ✅ **Ready**
|
||||||
|
- Database schema clearly defined
|
||||||
|
- Component responsibilities well-documented
|
||||||
|
- Storage patterns explained
|
||||||
|
- Security requirements specified
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Add iOS-specific architecture section mapping Android components to iOS equivalents
|
||||||
|
- Document Core Data model mapping to Room schema
|
||||||
|
- Add iOS-specific performance considerations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 Database Schema
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt`
|
||||||
|
- `android/DATABASE_CONSOLIDATION_PLAN.md`
|
||||||
|
- `ARCHITECTURE.md` (Data Architecture section)
|
||||||
|
|
||||||
|
**Key Information Provided**:
|
||||||
|
- Complete database schema with all tables
|
||||||
|
- Field definitions and types
|
||||||
|
- Relationships between entities
|
||||||
|
- Indexing strategy
|
||||||
|
- Migration path
|
||||||
|
|
||||||
|
**Database Tables Documented**:
|
||||||
|
1. ✅ `schedules` - Recurring schedule patterns
|
||||||
|
2. ✅ `content_cache` - Fetched content with TTL
|
||||||
|
3. ✅ `notification_config` - Plugin configuration
|
||||||
|
4. ✅ `callbacks` - Callback configurations
|
||||||
|
5. ✅ `notification_content` - Specific notification instances
|
||||||
|
6. ✅ `notification_delivery` - Delivery tracking
|
||||||
|
7. ✅ `history` - Execution history
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ✅ **Ready**
|
||||||
|
- All tables and fields documented
|
||||||
|
- Data types specified
|
||||||
|
- Relationships clear
|
||||||
|
- iOS Core Data model exists (`ios/Plugin/DailyNotificationModel.xcdatamodeld`)
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Add iOS Core Data entity mapping document
|
||||||
|
- Document iOS-specific storage considerations (UserDefaults vs Core Data)
|
||||||
|
- Add migration guide from Android Room to iOS Core Data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Recovery Scenarios Documentation
|
||||||
|
|
||||||
|
### 2.1 Recovery Scenarios Overview
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `docs/android-implementation-directive.md`
|
||||||
|
- `docs/android-implementation-directive-phase1.md`
|
||||||
|
- `docs/android-implementation-directive-phase2.md`
|
||||||
|
- `docs/android-implementation-directive-phase3.md`
|
||||||
|
- `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||||
|
|
||||||
|
**Recovery Scenarios Documented**:
|
||||||
|
|
||||||
|
1. ✅ **COLD_START** - Process killed, alarms may or may not exist
|
||||||
|
- Detection logic documented
|
||||||
|
- Recovery steps specified
|
||||||
|
- Logging requirements defined
|
||||||
|
|
||||||
|
2. ✅ **FORCE_STOP** - Alarms cleared, DB still populated
|
||||||
|
- Detection logic documented
|
||||||
|
- Recovery steps specified
|
||||||
|
- Android-specific (not applicable to iOS)
|
||||||
|
|
||||||
|
3. ✅ **BOOT** - Device reboot
|
||||||
|
- Detection logic documented
|
||||||
|
- Recovery steps specified
|
||||||
|
- Boot receiver implementation detailed
|
||||||
|
|
||||||
|
4. ✅ **NONE** - No recovery required (warm resume or first launch)
|
||||||
|
- Detection logic documented
|
||||||
|
- Behavior specified
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||||
|
|
||||||
|
**Gaps Identified**:
|
||||||
|
- iOS doesn't have "force stop" equivalent - need to document iOS app termination scenarios
|
||||||
|
- iOS boot recovery uses different mechanism (BGTaskScheduler registration)
|
||||||
|
- iOS warm/cold start detection differs from Android
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Add iOS-specific recovery scenario mapping:
|
||||||
|
- Android `FORCE_STOP` → iOS "App Terminated by System"
|
||||||
|
- Android `BOOT` → iOS "Device Reboot" (BGTaskScheduler)
|
||||||
|
- Android `COLD_START` → iOS "App Launch After Termination"
|
||||||
|
- Document iOS-specific detection mechanisms
|
||||||
|
- Add iOS recovery implementation guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Recovery Implementation Details
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||||
|
|
||||||
|
**Key Implementation Details Documented**:
|
||||||
|
- Scenario detection algorithm
|
||||||
|
- Alarm existence checking
|
||||||
|
- Boot flag management
|
||||||
|
- Recovery result tracking
|
||||||
|
- Error handling
|
||||||
|
- Timeout protection
|
||||||
|
|
||||||
|
**Code Documentation Quality**: ✅ **Excellent**
|
||||||
|
- Comprehensive inline comments
|
||||||
|
- Method-level documentation
|
||||||
|
- Parameter descriptions
|
||||||
|
- Return value documentation
|
||||||
|
- Error handling documented
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ✅ **Ready**
|
||||||
|
- Algorithm logic clear
|
||||||
|
- Edge cases documented
|
||||||
|
- Error handling patterns specified
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Add iOS-specific implementation notes:
|
||||||
|
- iOS alarm checking (UNUserNotificationCenter.getPendingNotificationRequests)
|
||||||
|
- iOS boot detection (BGTaskScheduler registration)
|
||||||
|
- iOS app termination detection (applicationWillTerminate)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Plugin Methods Documentation
|
||||||
|
|
||||||
|
### 3.1 Plugin API Methods
|
||||||
|
|
||||||
|
**Status**: ⚠️ **Partially Documented**
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `API.md`
|
||||||
|
- `README.md`
|
||||||
|
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
|
||||||
|
|
||||||
|
**Methods Documented**: 54 `@PluginMethod` annotations found
|
||||||
|
|
||||||
|
**Well-Documented Methods**:
|
||||||
|
- ✅ `scheduleDailyNotification` - API.md, README.md
|
||||||
|
- ✅ `scheduleDailyReminder` - API.md, README.md
|
||||||
|
- ✅ `getScheduledReminders` - API.md, README.md
|
||||||
|
- ✅ `cancelDailyReminder` - API.md, README.md
|
||||||
|
- ✅ `isAlarmScheduled` - API.md (Android-specific)
|
||||||
|
- ✅ `getNextAlarmTime` - API.md (Android-specific)
|
||||||
|
- ✅ `testAlarm` - API.md (Android-specific)
|
||||||
|
|
||||||
|
**Partially Documented Methods**:
|
||||||
|
- ⚠️ Database CRUD methods - Some documented in `docs/DATABASE_INTERFACES.md`
|
||||||
|
- ⚠️ Configuration methods - Limited documentation
|
||||||
|
- ⚠️ History/analytics methods - Limited documentation
|
||||||
|
|
||||||
|
**Missing Documentation**:
|
||||||
|
- ❌ Complete method signature list with parameters
|
||||||
|
- ❌ Return value specifications
|
||||||
|
- ❌ Error conditions and codes
|
||||||
|
- ❌ Platform-specific behavior differences
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Create comprehensive API reference document with:
|
||||||
|
- All 54 methods listed
|
||||||
|
- Complete parameter specifications
|
||||||
|
- Return value types
|
||||||
|
- Error conditions
|
||||||
|
- Platform-specific notes
|
||||||
|
- Add iOS-specific method documentation
|
||||||
|
- Document which methods are Android-only vs cross-platform
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Testing Documentation
|
||||||
|
|
||||||
|
### 4.1 Test Scripts
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `test-apps/android-test-app/test-phase1.sh`
|
||||||
|
- `test-apps/android-test-app/test-phase2.sh`
|
||||||
|
- `test-apps/android-test-app/test-phase3.sh`
|
||||||
|
|
||||||
|
**Test Coverage Documented**:
|
||||||
|
|
||||||
|
**Phase 1 Tests**:
|
||||||
|
- ✅ TEST 0: Daily Rollover
|
||||||
|
- ✅ TEST 1: Cold Start Recovery
|
||||||
|
- ✅ TEST 2: Alarm Persistence (Swipe from Recents)
|
||||||
|
- ✅ TEST 3: Invalid Data Handling
|
||||||
|
|
||||||
|
**Phase 2 Tests**:
|
||||||
|
- ✅ TEST 1: Force Stop with Cleared Alarms
|
||||||
|
- ✅ TEST 2: Alarms Intact (Warm Resume)
|
||||||
|
- ✅ TEST 3: Multiple Schedules Recovery
|
||||||
|
|
||||||
|
**Phase 3 Tests**:
|
||||||
|
- ✅ TEST 1: Boot with Future Alarms
|
||||||
|
- ✅ TEST 2: Boot with Past Alarms
|
||||||
|
- ✅ TEST 3: Boot Recovery Idempotency
|
||||||
|
- ✅ TEST 4: Boot Recovery Without App Launch
|
||||||
|
|
||||||
|
**Test Script Quality**: ✅ **Excellent**
|
||||||
|
- Clear test procedures
|
||||||
|
- Expected results specified
|
||||||
|
- Verification steps documented
|
||||||
|
- Error handling included
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||||
|
|
||||||
|
**Gaps Identified**:
|
||||||
|
- Test scripts are Android-specific (ADB commands)
|
||||||
|
- iOS testing requires different tools (xcrun simctl, etc.)
|
||||||
|
- Some tests are Android-only (force stop)
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Create iOS test script equivalents:
|
||||||
|
- `test-apps/ios-test-app/test-phase1.sh`
|
||||||
|
- `test-apps/ios-test-app/test-phase2.sh`
|
||||||
|
- `test-apps/ios-test-app/test-phase3.sh`
|
||||||
|
- Document iOS-specific test procedures
|
||||||
|
- Map Android tests to iOS equivalents
|
||||||
|
- Document iOS testing tools and commands
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 Test Documentation
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `docs/alarms/PHASE1-VERIFICATION.md`
|
||||||
|
- `docs/alarms/PHASE2-VERIFICATION.md`
|
||||||
|
- `docs/alarms/PHASE3-VERIFICATION.md`
|
||||||
|
- `docs/alarms/PHASE1-EMULATOR-TESTING.md`
|
||||||
|
- `docs/alarms/PHASE2-EMULATOR-TESTING.md`
|
||||||
|
- `docs/alarms/PHASE3-EMULATOR-TESTING.md`
|
||||||
|
|
||||||
|
**Documentation Quality**: ✅ **Excellent**
|
||||||
|
- Test procedures clearly defined
|
||||||
|
- Expected results specified
|
||||||
|
- Verification steps documented
|
||||||
|
- Troubleshooting guides included
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Create iOS verification documents
|
||||||
|
- Document iOS simulator testing procedures
|
||||||
|
- Add iOS-specific troubleshooting guides
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Platform-Specific Features
|
||||||
|
|
||||||
|
### 5.1 Android-Specific Features
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Features Documented**:
|
||||||
|
- ✅ AlarmManager integration
|
||||||
|
- ✅ BootReceiver implementation
|
||||||
|
- ✅ WorkManager for background tasks
|
||||||
|
- ✅ Exact alarm permissions
|
||||||
|
- ✅ Notification channels
|
||||||
|
- ✅ SharedPreferences for flags
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ✅ **Ready**
|
||||||
|
- Android features clearly documented
|
||||||
|
- iOS equivalents identified in requirements
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Add iOS feature mapping document:
|
||||||
|
- AlarmManager → UNUserNotificationCenter
|
||||||
|
- BootReceiver → BGTaskScheduler
|
||||||
|
- WorkManager → BGTaskScheduler
|
||||||
|
- Exact alarm permissions → Notification permissions
|
||||||
|
- Notification channels → Notification categories
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 iOS-Specific Considerations
|
||||||
|
|
||||||
|
**Status**: ❌ **Missing**
|
||||||
|
|
||||||
|
**Gaps Identified**:
|
||||||
|
- No iOS-specific implementation guide
|
||||||
|
- No iOS platform capability reference
|
||||||
|
- No iOS testing procedures
|
||||||
|
- No iOS troubleshooting guide
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Create `docs/ios-implementation-directive.md`
|
||||||
|
- Create `docs/ios-platform-capability-reference.md`
|
||||||
|
- Create iOS testing procedures
|
||||||
|
- Create iOS troubleshooting guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Data Models and Entities
|
||||||
|
|
||||||
|
### 6.1 Schedule Entity
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**: `android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt`
|
||||||
|
|
||||||
|
**Fields Documented**:
|
||||||
|
- ✅ `id` - String, PrimaryKey
|
||||||
|
- ✅ `kind` - String ('fetch' or 'notify')
|
||||||
|
- ✅ `cron` - String? (optional cron expression)
|
||||||
|
- ✅ `clockTime` - String? (optional HH:mm)
|
||||||
|
- ✅ `enabled` - Boolean
|
||||||
|
- ✅ `lastRunAt` - Long? (epoch ms)
|
||||||
|
- ✅ `nextRunAt` - Long? (epoch ms)
|
||||||
|
- ✅ `jitterMs` - Int
|
||||||
|
- ✅ `backoffPolicy` - String
|
||||||
|
- ✅ `stateJson` - String?
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ✅ **Ready**
|
||||||
|
- All fields documented
|
||||||
|
- Types specified
|
||||||
|
- iOS Core Data model exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.2 NotificationContentEntity
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**: `android/src/main/java/com/timesafari/dailynotification/entities/NotificationContentEntity.java`
|
||||||
|
|
||||||
|
**Fields Documented**: All fields with types and purposes
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ✅ **Ready**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.3 Other Entities
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
All entities documented in database schema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Recovery Logic Details
|
||||||
|
|
||||||
|
### 7.1 Scenario Detection Algorithm
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||||
|
|
||||||
|
**Algorithm Documented**:
|
||||||
|
```kotlin
|
||||||
|
1. Check if database has schedules
|
||||||
|
2. If empty → NONE
|
||||||
|
3. Check if alarms exist in AlarmManager
|
||||||
|
4. If no alarms:
|
||||||
|
- Check boot flag (recent within 60s) → BOOT
|
||||||
|
- Otherwise → FORCE_STOP
|
||||||
|
5. If alarms exist:
|
||||||
|
- Check boot flag → BOOT (if set)
|
||||||
|
- Otherwise → COLD_START
|
||||||
|
```
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ✅ **Ready**
|
||||||
|
- Algorithm logic clear
|
||||||
|
- Edge cases documented
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Add iOS-specific detection notes:
|
||||||
|
- iOS alarm checking method
|
||||||
|
- iOS boot detection mechanism
|
||||||
|
- iOS app termination detection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7.2 Recovery Execution
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Recovery Steps Documented**:
|
||||||
|
- ✅ Missed alarm detection
|
||||||
|
- ✅ Alarm rescheduling
|
||||||
|
- ✅ Error handling
|
||||||
|
- ✅ History recording
|
||||||
|
- ✅ Timeout protection
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ✅ **Ready**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Boot Recovery
|
||||||
|
|
||||||
|
### 8.1 BootReceiver Implementation
|
||||||
|
|
||||||
|
**Status**: ✅ **Well Documented**
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
||||||
|
- `docs/android-implementation-directive-phase3.md`
|
||||||
|
|
||||||
|
**Documentation Includes**:
|
||||||
|
- ✅ Boot receiver registration
|
||||||
|
- ✅ Intent filter configuration
|
||||||
|
- ✅ Recovery logic
|
||||||
|
- ✅ Boot flag management
|
||||||
|
- ✅ Error handling
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||||
|
|
||||||
|
**Gaps Identified**:
|
||||||
|
- iOS uses BGTaskScheduler, not broadcast receiver
|
||||||
|
- iOS boot detection mechanism differs
|
||||||
|
- iOS background task registration required
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Document iOS BGTaskScheduler registration
|
||||||
|
- Document iOS boot detection mechanism
|
||||||
|
- Add iOS boot recovery implementation guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Critical Documentation Gaps for iOS
|
||||||
|
|
||||||
|
### 9.1 Missing Documentation
|
||||||
|
|
||||||
|
1. ❌ **iOS Platform Capability Reference**
|
||||||
|
- Need: Document iOS-specific OS behaviors
|
||||||
|
- Location: `docs/ios-platform-capability-reference.md`
|
||||||
|
|
||||||
|
2. ❌ **iOS Implementation Directive**
|
||||||
|
- Need: Step-by-step iOS implementation guide
|
||||||
|
- Location: `docs/ios-implementation-directive.md`
|
||||||
|
|
||||||
|
3. ❌ **iOS Test Scripts**
|
||||||
|
- Need: iOS test script equivalents
|
||||||
|
- Location: `test-apps/ios-test-app/test-phase*.sh`
|
||||||
|
|
||||||
|
4. ❌ **iOS API Method Documentation**
|
||||||
|
- Need: Complete iOS method signatures
|
||||||
|
- Location: `API.md` (iOS section)
|
||||||
|
|
||||||
|
5. ❌ **iOS Recovery Scenario Mapping**
|
||||||
|
- Need: Android → iOS scenario mapping
|
||||||
|
- Location: `docs/ios-recovery-scenario-mapping.md`
|
||||||
|
|
||||||
|
6. ❌ **iOS Core Data Migration Guide**
|
||||||
|
- Need: Room → Core Data migration guide
|
||||||
|
- Location: `docs/ios-core-data-migration.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.2 Partially Documented Areas
|
||||||
|
|
||||||
|
1. ⚠️ **Plugin Methods**
|
||||||
|
- Status: 54 methods found, ~20 documented in API.md
|
||||||
|
- Need: Complete method reference
|
||||||
|
|
||||||
|
2. ⚠️ **Error Handling**
|
||||||
|
- Status: Some error codes documented
|
||||||
|
- Need: Complete error code reference
|
||||||
|
|
||||||
|
3. ⚠️ **Platform Differences**
|
||||||
|
- Status: Some differences noted
|
||||||
|
- Need: Comprehensive platform comparison
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Recommendations Summary
|
||||||
|
|
||||||
|
### 10.1 High Priority
|
||||||
|
|
||||||
|
1. **Create iOS Platform Capability Reference**
|
||||||
|
- Document iOS-specific OS behaviors
|
||||||
|
- Map Android behaviors to iOS equivalents
|
||||||
|
|
||||||
|
2. **Create iOS Implementation Directive**
|
||||||
|
- Step-by-step implementation guide
|
||||||
|
- Mirror Android phase structure
|
||||||
|
|
||||||
|
3. **Complete API Documentation**
|
||||||
|
- Document all 54 plugin methods
|
||||||
|
- Add iOS-specific method notes
|
||||||
|
|
||||||
|
4. **Create iOS Test Scripts**
|
||||||
|
- Mirror Android test structure
|
||||||
|
- Use iOS testing tools
|
||||||
|
|
||||||
|
### 10.2 Medium Priority
|
||||||
|
|
||||||
|
1. **Add iOS Recovery Scenario Mapping**
|
||||||
|
- Map Android scenarios to iOS
|
||||||
|
- Document iOS-specific scenarios
|
||||||
|
|
||||||
|
2. **Create iOS Core Data Migration Guide**
|
||||||
|
- Room → Core Data mapping
|
||||||
|
- Data migration procedures
|
||||||
|
|
||||||
|
3. **Add iOS Troubleshooting Guide**
|
||||||
|
- Common iOS issues
|
||||||
|
- Debugging procedures
|
||||||
|
|
||||||
|
### 10.3 Low Priority
|
||||||
|
|
||||||
|
1. **Add iOS Architecture Diagrams**
|
||||||
|
- Visual component mapping
|
||||||
|
- iOS-specific architecture notes
|
||||||
|
|
||||||
|
2. **Create iOS Performance Guide**
|
||||||
|
- iOS-specific optimizations
|
||||||
|
- Battery considerations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Documentation Quality Assessment
|
||||||
|
|
||||||
|
### 11.1 Overall Assessment
|
||||||
|
|
||||||
|
**Android Documentation Quality**: ✅ **Excellent**
|
||||||
|
- Comprehensive coverage
|
||||||
|
- Clear structure
|
||||||
|
- Good code examples
|
||||||
|
- Well-organized
|
||||||
|
|
||||||
|
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||||
|
- Core architecture: ✅ Ready
|
||||||
|
- Database schema: ✅ Ready
|
||||||
|
- Recovery logic: ✅ Ready
|
||||||
|
- Platform specifics: ❌ Missing
|
||||||
|
- Testing: ⚠️ Partially ready
|
||||||
|
|
||||||
|
### 11.2 Strengths
|
||||||
|
|
||||||
|
1. ✅ **Database schema well-documented**
|
||||||
|
2. ✅ **Recovery scenarios clearly defined**
|
||||||
|
3. ✅ **Test procedures comprehensive**
|
||||||
|
4. ✅ **Code documentation excellent**
|
||||||
|
5. ✅ **Architecture clearly explained**
|
||||||
|
|
||||||
|
### 11.3 Weaknesses
|
||||||
|
|
||||||
|
1. ❌ **Missing iOS-specific documentation**
|
||||||
|
2. ⚠️ **Incomplete API method documentation**
|
||||||
|
3. ⚠️ **Platform differences not fully documented**
|
||||||
|
4. ❌ **No iOS test scripts**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Action Items
|
||||||
|
|
||||||
|
### 12.1 For iOS Implementation
|
||||||
|
|
||||||
|
1. **Before Starting Implementation**:
|
||||||
|
- [ ] Review Android architecture documentation
|
||||||
|
- [ ] Review database schema
|
||||||
|
- [ ] Review recovery scenarios
|
||||||
|
- [ ] Review test procedures
|
||||||
|
|
||||||
|
2. **During Implementation**:
|
||||||
|
- [ ] Create iOS platform capability reference
|
||||||
|
- [ ] Document iOS-specific behaviors
|
||||||
|
- [ ] Create iOS test scripts
|
||||||
|
- [ ] Document iOS platform differences
|
||||||
|
|
||||||
|
3. **After Implementation**:
|
||||||
|
- [ ] Update API documentation with iOS notes
|
||||||
|
- [ ] Create iOS troubleshooting guide
|
||||||
|
- [ ] Document iOS-specific optimizations
|
||||||
|
|
||||||
|
### 12.2 For Documentation Improvement
|
||||||
|
|
||||||
|
1. **Complete API Documentation**:
|
||||||
|
- [ ] Document all 54 plugin methods
|
||||||
|
- [ ] Add parameter specifications
|
||||||
|
- [ ] Add return value types
|
||||||
|
- [ ] Add error conditions
|
||||||
|
|
||||||
|
2. **Add iOS Documentation**:
|
||||||
|
- [ ] Create iOS platform capability reference
|
||||||
|
- [ ] Create iOS implementation directive
|
||||||
|
- [ ] Create iOS test scripts
|
||||||
|
- [ ] Create iOS troubleshooting guide
|
||||||
|
|
||||||
|
3. **Improve Cross-Platform Documentation**:
|
||||||
|
- [ ] Add platform comparison matrix
|
||||||
|
- [ ] Document platform-specific features
|
||||||
|
- [ ] Add migration guides
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Conclusion
|
||||||
|
|
||||||
|
The Android plugin and test app documentation is **comprehensive and well-structured**. The core architecture, database schema, and recovery logic are **sufficiently documented** for iOS implementation to mirror Android features.
|
||||||
|
|
||||||
|
**Key Findings**:
|
||||||
|
- ✅ Core architecture: Ready for iOS implementation
|
||||||
|
- ✅ Database schema: Ready for iOS implementation
|
||||||
|
- ✅ Recovery logic: Ready for iOS implementation
|
||||||
|
- ❌ Platform specifics: Missing iOS documentation
|
||||||
|
- ⚠️ API methods: Partially documented
|
||||||
|
|
||||||
|
**Recommendation**: Proceed with iOS implementation using existing Android documentation, while creating iOS-specific documentation as needed. The Android documentation provides a solid foundation for iOS implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0.0
|
||||||
|
**Last Updated**: December 2024
|
||||||
|
**Next Review**: After iOS implementation begins
|
||||||
|
|
||||||
579
docs/alarms/000-UNIFIED-ALARM-DIRECTIVE.md
Normal file
579
docs/alarms/000-UNIFIED-ALARM-DIRECTIVE.md
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
# 🔧 UNIFIED DIRECTIVE: Alarm / Schedule / Notification Documentation & Implementation Stack
|
||||||
|
|
||||||
|
**Author:** Matthew Raymer
|
||||||
|
**Status:** Active – Master Coordination Directive
|
||||||
|
**Scope:** Android & iOS, Capacitor plugin, alarms/schedules/notifications
|
||||||
|
**Version:** 1.1.0
|
||||||
|
**Last Updated:** November 2025
|
||||||
|
**Last Synced With Plugin Version:** v1.1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Purpose
|
||||||
|
|
||||||
|
Unify all existing alarm/notification documents into a **coherent, layered system** that:
|
||||||
|
|
||||||
|
1. **Eliminates duplication** (especially Android alarm behavior repeated across docs)
|
||||||
|
2. **Separates concerns** into:
|
||||||
|
* Platform facts
|
||||||
|
* Exploration/testing
|
||||||
|
* Plugin requirements
|
||||||
|
* Implementation directives (Phases 1–3)
|
||||||
|
3. **Provides iOS parity** to the existing Android-heavy material
|
||||||
|
4. **Maps OS-level capabilities → plugin guarantees → JS/TS API contract**
|
||||||
|
5. **Standardizes testing** into executable matrices for exploration and regression
|
||||||
|
6. **Connects exploration → design → implementation** and tracks status across that pipeline
|
||||||
|
|
||||||
|
**This directive is the top of the stack.** If any lower-level document conflicts with this one, **this directive wins** unless explicitly noted.
|
||||||
|
|
||||||
|
**Mission Statement**: This directive ensures that every alarm outcome on every platform is predictable, testable, and recoverable, with no undocumented behavior.
|
||||||
|
|
||||||
|
**⚠️ ENFORCEMENT RULE**: No new alarm-related documentation may be created outside Documents A, B, C, or P1–P3. All future changes must modify these documents, not create additional standalone files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Inputs (Existing Documents)
|
||||||
|
|
||||||
|
### 1.1 Platform & Reference Layer
|
||||||
|
|
||||||
|
* `platform-capability-reference.md` – OS-level facts (Android & iOS)
|
||||||
|
* `android-alarm-persistence-directive.md` – Android abilities & limitations, engineering-grade
|
||||||
|
|
||||||
|
### 1.2 Exploration & Findings
|
||||||
|
|
||||||
|
* `plugin-behavior-exploration-template.md` – Structured template for exploring behavior
|
||||||
|
* `explore-alarm-behavior-directive.md` – Exploration directive for behavior & persistence
|
||||||
|
* `exploration-findings-initial.md` – Initial code-level discovery
|
||||||
|
|
||||||
|
### 1.3 Requirements & Implementation
|
||||||
|
|
||||||
|
* `plugin-requirements-implementation.md` – Plugin behavior rules & guarantees
|
||||||
|
* `android-implementation-directive.md` – Umbrella Android implementation overview (app launch recovery, missed alarms, force stop, boot)
|
||||||
|
* Phase directives (normative):
|
||||||
|
* `../android-implementation-directive-phase1.md` – Cold start recovery (minimal viable)
|
||||||
|
* `../android-implementation-directive-phase2.md` – Force stop detection & recovery
|
||||||
|
* `../android-implementation-directive-phase3.md` – Boot receiver missed alarm handling
|
||||||
|
|
||||||
|
### 1.4 Improvement Directive
|
||||||
|
|
||||||
|
* `improve-alarm-directives.md` – Current plan for structural and content improvements across the stack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Target Structure (What We're Building)
|
||||||
|
|
||||||
|
We standardize around **three primary doc roles**, plus implementation phases:
|
||||||
|
|
||||||
|
### A. **Platform Reference (Document A)**
|
||||||
|
|
||||||
|
**Goal:** Single, stable, normative OS facts.
|
||||||
|
|
||||||
|
* Merge:
|
||||||
|
* Android section from `android-alarm-persistence-directive.md`
|
||||||
|
* Android & iOS matrices from `platform-capability-reference.md`
|
||||||
|
* Content rules:
|
||||||
|
* NO plugin-specific rules
|
||||||
|
* Just: "what Android/iOS can and cannot do"
|
||||||
|
* Use matrices & short prose only
|
||||||
|
* Label each item as:
|
||||||
|
* **OS-guaranteed**, **Plugin-required**, or **Forbidden** (where relevant)
|
||||||
|
* Output path (recommended):
|
||||||
|
`docs/alarms/01-platform-capability-reference.md`
|
||||||
|
|
||||||
|
### B. **Plugin Behavior Exploration (Document B)**
|
||||||
|
|
||||||
|
**Goal:** Executable exploration & testing spec.
|
||||||
|
|
||||||
|
* Baseline: `plugin-behavior-exploration-template.md` + `explore-alarm-behavior-directive.md`
|
||||||
|
* Remove duplicated platform explanations (refer to Document A instead)
|
||||||
|
* For each scenario (swipe, reboot, force stop, etc.):
|
||||||
|
* Link to source files & functions (line or section refs)
|
||||||
|
* Provide **Expected (OS)** & **Expected (Plugin)** from Docs A + C
|
||||||
|
* Add columns for **Actual Result** and **Notes** (checkbox matrix)
|
||||||
|
* Output path:
|
||||||
|
`docs/alarms/02-plugin-behavior-exploration.md`
|
||||||
|
|
||||||
|
### C. **Plugin Requirements & Implementation Rules (Document C)**
|
||||||
|
|
||||||
|
**Goal:** What the plugin MUST guarantee & how.
|
||||||
|
|
||||||
|
* Baseline: `plugin-requirements-implementation.md` + `improve-alarm-directives.md`
|
||||||
|
* Must include:
|
||||||
|
1. **Plugin Behavior Guarantees & Limitations** by platform (table)
|
||||||
|
2. **Persistence Requirements** (fields, storage, validation, failure modes)
|
||||||
|
3. **Recovery Requirements**:
|
||||||
|
* Boot (Android)
|
||||||
|
* Cold start
|
||||||
|
* Warm start
|
||||||
|
* Force stop (Android)
|
||||||
|
* User-tap recovery
|
||||||
|
4. **Missed alarm handling contract** (definition, triggers, required actions)
|
||||||
|
5. **JS/TS API Contract**:
|
||||||
|
* What JS devs can rely on
|
||||||
|
* Platform caveats (e.g., Force Stop, iOS background limits)
|
||||||
|
6. **Unsupported features / limitations** clearly called out
|
||||||
|
* Output path:
|
||||||
|
`docs/alarms/03-plugin-requirements.md`
|
||||||
|
|
||||||
|
### D. **Implementation Directives (Phase 1–3)**
|
||||||
|
|
||||||
|
Already exist; this directive standardizes their role:
|
||||||
|
|
||||||
|
* **Phase docs are normative for code.**
|
||||||
|
If they conflict with Document C, update the phase docs and then C.
|
||||||
|
|
||||||
|
**⚠️ STRUCTURE FREEZE**: The structure defined in this directive is FINAL. No new document categories may be introduced without updating this master directive first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ownership, Versioning & Status
|
||||||
|
|
||||||
|
### 3.1 Ownership
|
||||||
|
|
||||||
|
**Assignable Ownership** (replace abstract roles with concrete owners):
|
||||||
|
|
||||||
|
| Layer | Owner | Responsibility |
|
||||||
|
| ---------------------- | ------------------- | ------------------------------------------------- |
|
||||||
|
| Doc A – Platform Facts | Engineering Lead | Long-lived reference, rare changes |
|
||||||
|
| Doc B – Exploration | QA / Testing Lead | Living document, updated with test results |
|
||||||
|
| Doc C – Requirements | Plugin Architect | Versioned with plugin, defines guarantees |
|
||||||
|
| Phases P1–P3 | Implementation Lead | Normative code specs, tied to Doc C requirements |
|
||||||
|
|
||||||
|
**Note**: If owners are not yet assigned, use role names above as placeholders until assignment.
|
||||||
|
|
||||||
|
### 3.2 Versioning Rules
|
||||||
|
|
||||||
|
* Any change that alters **JS/TS-visible behavior** → bump plugin **MINOR** at least
|
||||||
|
* Any change that breaks prior guarantees or adds new required migration → **MAJOR bump**
|
||||||
|
* Each doc should include:
|
||||||
|
* `Version: x.y.z`
|
||||||
|
* `Last Synced With Plugin Version: vX.Y.Z`
|
||||||
|
|
||||||
|
### 3.3 Status Matrix
|
||||||
|
|
||||||
|
**Status matrix is maintained in Section 11** (see below). This section is kept for historical reference only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Work Plan – "Do All of the Above"
|
||||||
|
|
||||||
|
This is the **concrete to-do list** that satisfies:
|
||||||
|
|
||||||
|
* Consolidation
|
||||||
|
* Versioning & ownership
|
||||||
|
* Status tracking
|
||||||
|
* Single-source master structure
|
||||||
|
* Next-phase readiness
|
||||||
|
* Improvements from `improve-alarm-directives.md`
|
||||||
|
|
||||||
|
### Step 1 – Normalize Platform Reference (Doc A)
|
||||||
|
|
||||||
|
1. Start from `platform-capability-reference.md` + `android-alarm-persistence-directive.md`
|
||||||
|
2. Merge into a single doc:
|
||||||
|
* Android matrix
|
||||||
|
* iOS matrix
|
||||||
|
* Core principles (no plugin rules)
|
||||||
|
3. Ensure each line is labeled:
|
||||||
|
* **OS-guaranteed**, **Plugin-required**, or **Forbidden** (where relevant)
|
||||||
|
4. Mark the old Android alarm persistence doc as:
|
||||||
|
* **Deprecated in favor of Doc A** (leave file but add a banner)
|
||||||
|
|
||||||
|
### Step 2 – Rewrite Exploration (Doc B)
|
||||||
|
|
||||||
|
1. Use `plugin-behavior-exploration-template.md` as the skeleton
|
||||||
|
2. Integrate concrete scenario descriptions and code paths from `explore-alarm-behavior-directive.md`
|
||||||
|
3. For **each scenario**:
|
||||||
|
* App swipe, OS kill, reboot, force stop, cold start, warm start, notification tap:
|
||||||
|
* Add row: Step-by-step actions + Expected (OS) + Expected (Plugin) + Actual + Notes
|
||||||
|
4. Remove any explanation that duplicates Doc A; replace with "See Doc A, section X"
|
||||||
|
|
||||||
|
### Step 3 – Rewrite Plugin Requirements (Doc C)
|
||||||
|
|
||||||
|
1. Start from `plugin-requirements-implementation.md` & improvement goals
|
||||||
|
2. Add / enforce sections:
|
||||||
|
* **Plugin Behavior Guarantees & Limitations**
|
||||||
|
* **Persistence Spec** (fields, mandatory vs optional)
|
||||||
|
* **Recovery Points** (boot, cold, warm, force stop, user tap)
|
||||||
|
* **Missed Alarm Contract**
|
||||||
|
* **JS/TS API Contract**
|
||||||
|
* **Unsupported / impossible guarantees** (Force Stop, iOS background, etc.)
|
||||||
|
3. Everywhere it relies on platform behavior:
|
||||||
|
* Link back to Doc A using short cross-reference ("See A §2.1")
|
||||||
|
4. Add a **"Traceability"** mini-table:
|
||||||
|
* Row per behavior → Platform fact in A → Tested scenario in B → Implemented in Phase N
|
||||||
|
|
||||||
|
### Step 4 – Align Implementation Directives with Doc C
|
||||||
|
|
||||||
|
1. Treat Phase docs as **canonical code spec**: P1, P2, P3
|
||||||
|
2. For each behavior required in Doc C:
|
||||||
|
* Identify which Phase implements it (or will implement it)
|
||||||
|
3. Update:
|
||||||
|
* `android-implementation-directive.md` to be explicitly **descriptive/integrative**, not normative, pointing to Phases 1–3 & Doc C
|
||||||
|
* Ensure scenario model, alarm existence checks, and boot handling match the **corrected model** already defined in Phase 2 & 3
|
||||||
|
4. Add acceptance criteria per phase that directly reference:
|
||||||
|
* Requirements in Doc C
|
||||||
|
* Platform constraints in Doc A
|
||||||
|
5. **Phase 1–3 must REMOVE**:
|
||||||
|
* Any scenario logic (moved to Doc C)
|
||||||
|
* Any platform behavioral claims (moved to Doc A)
|
||||||
|
* Any recovery responsibilities not assigned to that phase
|
||||||
|
|
||||||
|
### Step 5 – Versioning & Status
|
||||||
|
|
||||||
|
1. At the top of Docs A, B, C, and each Phase doc, add:
|
||||||
|
* `Version`, `Last Updated`, `Sync'd with Plugin vX.Y.Z`
|
||||||
|
2. Maintain the status matrix (Section 11) as the **single source of truth** for doc maturity
|
||||||
|
3. When a Phase is fully implemented and deployed:
|
||||||
|
* Mark "In Use?" = ✅
|
||||||
|
* Add link to code tags/commit hash
|
||||||
|
|
||||||
|
### Step 6 – Readiness Check for Next Work Phase
|
||||||
|
|
||||||
|
Before starting any *new* implementation work on alarms / schedules:
|
||||||
|
|
||||||
|
1. **Confirm:**
|
||||||
|
* Doc A exists and is stable enough (no "TODO" in core matrices)
|
||||||
|
* Doc B has at least the base scenarios scaffolded
|
||||||
|
* Doc C clearly defines:
|
||||||
|
* What we guarantee on each platform
|
||||||
|
* What we cannot do (e.g., Force Stop auto-resume)
|
||||||
|
2. Only then:
|
||||||
|
* Modify or extend Phase 1–3
|
||||||
|
* Or add new phases (e.g., warm-start optimizations, iOS parity work)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Acceptance Criteria for THIS Directive (Revised and Final)
|
||||||
|
|
||||||
|
This directive is complete **ONLY** when:
|
||||||
|
|
||||||
|
1. **Doc A exists, is referenced, and no other doc contains platform facts**
|
||||||
|
* File exists: `docs/alarms/01-platform-capability-reference.md`
|
||||||
|
* All old platform docs marked as deprecated with banner
|
||||||
|
* No platform facts duplicated in other docs
|
||||||
|
|
||||||
|
2. **Doc B contains**:
|
||||||
|
* At least 6 scenarios (swipe, reboot, force stop, cold start, warm start, notification tap)
|
||||||
|
* Expected OS vs Plugin behavior columns
|
||||||
|
* Space for Actual Result and Notes
|
||||||
|
* Links to source files/functions
|
||||||
|
|
||||||
|
3. **Doc C contains**:
|
||||||
|
* Guarantees by platform (table format)
|
||||||
|
* Recovery matrix (boot, cold, warm, force stop, user tap)
|
||||||
|
* JS/TS API contract
|
||||||
|
* Unsupported behaviors clearly called out
|
||||||
|
|
||||||
|
4. **Phase docs**:
|
||||||
|
* Contain NO duplication of Doc A (platform facts)
|
||||||
|
* Reference Doc C explicitly (requirements)
|
||||||
|
* Have acceptance criteria tied to Doc C requirements
|
||||||
|
|
||||||
|
5. **Deprecated files have**:
|
||||||
|
* Banner: "⚠️ **DEPRECATED**: Superseded by [000-UNIFIED-ALARM-DIRECTIVE](./000-UNIFIED-ALARM-DIRECTIVE.md)"
|
||||||
|
* Link to replacement document
|
||||||
|
|
||||||
|
6. **Status matrix fields are no longer empty** (Section 11)
|
||||||
|
* All docs marked as Drafted at minimum
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Document Relationships & Cross-References
|
||||||
|
|
||||||
|
### 6.1 Reference Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Unified Directive (this doc)
|
||||||
|
↓
|
||||||
|
├─→ Doc A (Platform Reference)
|
||||||
|
│ └─→ Referenced by: B, C, P1-P3
|
||||||
|
│
|
||||||
|
├─→ Doc B (Exploration)
|
||||||
|
│ └─→ References: A (platform facts), C (expected behavior)
|
||||||
|
│ └─→ Feeds into: P1-P3 (test results inform implementation)
|
||||||
|
│
|
||||||
|
├─→ Doc C (Requirements)
|
||||||
|
│ └─→ References: A (platform constraints)
|
||||||
|
│ └─→ Referenced by: P1-P3 (implementation must satisfy)
|
||||||
|
│
|
||||||
|
└─→ P1-P3 (Implementation)
|
||||||
|
└─→ References: A (platform facts), C (requirements)
|
||||||
|
└─→ Validated by: B (exploration results)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Cross-Reference Format
|
||||||
|
|
||||||
|
When referencing between documents, use this format:
|
||||||
|
|
||||||
|
* **Doc A**: `[Platform Reference §2.1](../alarms/01-platform-capability-reference.md#21-android-alarm-persistence)`
|
||||||
|
* **Doc B**: `[Exploration §3.2](../alarms/02-plugin-behavior-exploration.md#32-cold-start-scenario)`
|
||||||
|
* **Doc C**: `[Requirements §4.3](../alarms/03-plugin-requirements.md#43-recovery-requirements)`
|
||||||
|
* **Phase docs**: `[Phase 1 §2.3](../android-implementation-directive-phase1.md#23-cold-start-recovery)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Canonical Source of Truth Rules
|
||||||
|
|
||||||
|
### 7.1 Platform Facts
|
||||||
|
|
||||||
|
**Only Doc A** contains platform facts. All other docs reference Doc A, never duplicate platform behavior.
|
||||||
|
|
||||||
|
**Reference Format**: `[Doc A §X.Y]` - See [Platform Capability Reference](./01-platform-capability-reference.md)
|
||||||
|
|
||||||
|
### 7.2 Requirements
|
||||||
|
|
||||||
|
**Only Doc C** defines plugin requirements. Phase docs implement Doc C requirements.
|
||||||
|
|
||||||
|
**Reference Format**: `[Doc C §X.Y]` - See [Plugin Requirements](./03-plugin-requirements.md)
|
||||||
|
|
||||||
|
### 7.3 Implementation Details
|
||||||
|
|
||||||
|
**Only Phase docs (P1-P3)** contain implementation details. Unified Directive does not specify code structure or algorithms.
|
||||||
|
|
||||||
|
**Reference Format**: `[Phase N §X.Y]` - See Phase implementation directives
|
||||||
|
|
||||||
|
### 7.4 Test Results
|
||||||
|
|
||||||
|
**Only Doc B** contains actual test results and observed behavior. Doc B references Doc A for expected OS behavior and Doc C for expected plugin behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Conflict Resolution
|
||||||
|
|
||||||
|
If conflicts arise between documents:
|
||||||
|
|
||||||
|
1. **This directive (000)** wins for structure and organization
|
||||||
|
2. **Doc A** wins for platform facts
|
||||||
|
3. **Doc C** wins for requirements
|
||||||
|
4. **Phase docs (P1-P3)** win for implementation details
|
||||||
|
5. **Doc B** is observational (actual test results) and may reveal conflicts
|
||||||
|
|
||||||
|
When a conflict is found:
|
||||||
|
1. Document it in this section
|
||||||
|
2. Resolve by updating the lower-priority document
|
||||||
|
3. Update the status matrix
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Next Steps
|
||||||
|
|
||||||
|
### ⚠️ Time-Based Triggers (Enforcement)
|
||||||
|
|
||||||
|
**Doc A must be created BEFORE any further implementation work.**
|
||||||
|
- No Phase 2 or Phase 3 changes until Doc A exists
|
||||||
|
- No new platform behavior claims in any doc until Doc A is canonical
|
||||||
|
|
||||||
|
**Doc B must be baseline-complete BEFORE Phase 2 changes.**
|
||||||
|
- At least 6 core scenarios scaffolded
|
||||||
|
- Expected behavior columns populated from Doc A + Doc C
|
||||||
|
|
||||||
|
**Doc C must be finalized BEFORE any JS/TS API changes.**
|
||||||
|
- All guarantees documented
|
||||||
|
- API contract defined
|
||||||
|
- No breaking changes to JS/TS API without Doc C update first
|
||||||
|
|
||||||
|
### Immediate (Before New Implementation)
|
||||||
|
|
||||||
|
1. Create stub documents A, B, C with structure
|
||||||
|
2. Migrate content from existing docs into new structure
|
||||||
|
3. Update all cross-references
|
||||||
|
4. Mark old docs as deprecated (with pointers to new docs)
|
||||||
|
|
||||||
|
**Deliverables Required**:
|
||||||
|
- Doc A exists as a file in repo
|
||||||
|
- Doc B exists with scenario tables scaffolded
|
||||||
|
- Doc C exists with required sections
|
||||||
|
- Status matrix updated in this document
|
||||||
|
- Deprecated docs marked with header banner
|
||||||
|
|
||||||
|
### Short-term (During Implementation)
|
||||||
|
|
||||||
|
1. Keep Doc B updated with test results
|
||||||
|
2. Update Phase docs as implementation progresses
|
||||||
|
3. Maintain status matrix
|
||||||
|
|
||||||
|
### Long-term (Post-Implementation)
|
||||||
|
|
||||||
|
1. Add iOS parity documentation
|
||||||
|
2. Expand exploration scenarios
|
||||||
|
3. Create regression test suite based on Doc B
|
||||||
|
|
||||||
|
### ⚠️ iOS Parity Activation Milestone
|
||||||
|
|
||||||
|
**iOS parity work begins ONLY after**:
|
||||||
|
|
||||||
|
1. **Doc A contains iOS matrix** - All iOS platform facts documented with labels (see [Doc A](./01-platform-capability-reference.md#3-ios-notification-capability-matrix))
|
||||||
|
2. **Doc C defines iOS limitations and guarantees** - All iOS-specific requirements documented (see [Doc C](./03-plugin-requirements.md#1-plugin-behavior-guarantees--limitations))
|
||||||
|
3. **No references in Phase docs assume Android-only behavior** - All Phase docs are platform-agnostic or explicitly handle both platforms
|
||||||
|
|
||||||
|
**Blocking Rule**: No iOS implementation work may proceed until these conditions are met.
|
||||||
|
|
||||||
|
**Enforcement**: Phase docs MUST reference Doc A for platform facts and Doc C for requirements. No platform-specific assumptions allowed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Change-Control Rules
|
||||||
|
|
||||||
|
Any change to Docs A–C requires:
|
||||||
|
|
||||||
|
1. **Update version header** in the document
|
||||||
|
2. **Update status matrix** (Section 11) in this directive
|
||||||
|
3. **Commit message tag**: `[ALARM-DOCS]` prefix
|
||||||
|
4. **Notification in CHANGELOG** if JS/TS-visible behavior changes
|
||||||
|
|
||||||
|
**Phase docs may not be modified unless Doc C changes first** (or explicit exception documented).
|
||||||
|
|
||||||
|
**Example commit message**:
|
||||||
|
```
|
||||||
|
[ALARM-DOCS] Update Doc C with force stop recovery guarantee
|
||||||
|
|
||||||
|
- Added force stop detection requirement
|
||||||
|
- Updated recovery matrix
|
||||||
|
- Version bumped to 1.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Status Matrix
|
||||||
|
|
||||||
|
| Doc | Path | Role | Drafted? | Cleaned? | In Use? | Notes |
|
||||||
|
| --- | ------------------------------------- | ----------------- | -------- | -------- | ------- | ---------------------------------------- |
|
||||||
|
| A | `01-platform-capability-reference.md` | Platform facts | ✅ | ✅ | ✅ | Created, merged from platform docs, canonical rule added |
|
||||||
|
| B | `02-plugin-behavior-exploration.md` | Exploration | ✅ | ✅ | ✅ | Converted to executable test harness + emulator script |
|
||||||
|
| C | `03-plugin-requirements.md` | Requirements | ✅ | ✅ | ✅ | Enhanced with guarantees matrix, JS/TS contract, traceability - **complete and in compliance** |
|
||||||
|
| P1 | `../android-implementation-directive-phase1.md` | Impl – Cold start | ✅ | ✅ | ✅ | Emulator-verified via `test-phase1.sh` (Pixel 8 API 34, 2025-11-27) |
|
||||||
|
| P2 | `../android-implementation-directive-phase2.md` | Impl – Force stop | ✅ | ✅ | ☐ | Implemented; to be emulator-verified via `test-phase2.sh` (Pixel 8 API 34, 2025-11-XX) |
|
||||||
|
| P3 | `../android-implementation-directive-phase3.md` | Impl – Boot Recovery | ✅ | ✅ | ☐ | Implemented; verify via `test-phase3.sh` (API 34 baseline) |
|
||||||
|
| V1 | `PHASE1-VERIFICATION.md` | Verification – P1 | ✅ | ✅ | ✅ | Summarizes Phase 1 emulator tests and latest known good run |
|
||||||
|
| V2 | `PHASE2-VERIFICATION.md` | Verification – P2 | ✅ | ✅ | ☐ | Summarizes Phase 2 emulator tests and latest known good run |
|
||||||
|
| V3 | `PHASE3-VERIFICATION.md` | Verification – P3 | ✅ | ✅ | ☐ | To be completed after first clean emulator run |
|
||||||
|
|
||||||
|
**Doc C Compliance Milestone**: Doc C is considered complete **ONLY** when:
|
||||||
|
- ✅ Cross-platform guarantees matrix present
|
||||||
|
- ✅ JS/TS API contract with returned fields and error states
|
||||||
|
- ✅ Explicit storage schema documented
|
||||||
|
- ✅ Recovery contract with all triggers and actions
|
||||||
|
- ✅ Unsupported behaviors explicitly listed
|
||||||
|
- ✅ Traceability matrix mapping A → B → C → Phase
|
||||||
|
|
||||||
|
**Legend:**
|
||||||
|
- ✅ = Complete
|
||||||
|
- ☐ = Not started / In progress
|
||||||
|
- ⚠️ = Needs attention
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
* [Android Implementation Directive](../android-implementation-directive.md) – Umbrella overview
|
||||||
|
* [Phase 1: Cold Start Recovery](../android-implementation-directive-phase1.md) – Minimal viable recovery
|
||||||
|
* [Phase 2: Force Stop Recovery](../android-implementation-directive-phase2.md) – Force stop detection
|
||||||
|
* [Phase 3: Boot Recovery](../android-implementation-directive-phase3.md) – Boot receiver enhancement
|
||||||
|
* [Exploration Findings](../exploration-findings-initial.md) – Initial code discovery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Active master coordination directive
|
||||||
|
**Last Updated**: November 2025
|
||||||
|
**Next Review**: After implementation phases are complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Single Instruction for Team
|
||||||
|
|
||||||
|
**⚠️ BLOCKING RULE**: No engineering or documentation work on alarms/schedules/notifications may continue until Steps 1–3 in §9 ("Immediate") are complete and committed.
|
||||||
|
|
||||||
|
**Specifically**:
|
||||||
|
- Doc A must exist as a file
|
||||||
|
- Doc B must have scenario tables scaffolded
|
||||||
|
- Doc C must have required sections
|
||||||
|
- Status matrix must be updated
|
||||||
|
- Deprecated files must be marked
|
||||||
|
|
||||||
|
**Exception**: Only emergency bug fixes may proceed, but must be documented retroactively in the appropriate Doc A/B/C structure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Prohibited Content Rules
|
||||||
|
|
||||||
|
**The following content may NOT appear in any document except the specified one**:
|
||||||
|
|
||||||
|
| Content Type | Allowed Only In | Examples |
|
||||||
|
| ------------ | --------------- | -------- |
|
||||||
|
| **Platform rules** | Doc A only | "Android wipes alarms on reboot", "iOS persists notifications automatically" |
|
||||||
|
| **Guarantees or requirements** | Doc C only | "Plugin MUST detect missed alarms", "Plugin SHOULD reschedule on boot" |
|
||||||
|
| **Actual behavior findings** | Doc B only | "Test showed alarm fired 2 seconds late", "Missed alarm not detected in scenario X" |
|
||||||
|
| **Recovery logic** | Phase docs only | "ReactivationManager.performRecovery()", "BootReceiver sets flag" |
|
||||||
|
| **Implementation details** | Phase docs only | Code snippets, function signatures, database queries |
|
||||||
|
|
||||||
|
**Violation Response**: If prohibited content is found, move it to the correct document and replace with a cross-reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Glossary
|
||||||
|
|
||||||
|
**Shared terminology across all documents** (must be identical):
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
| ---- | ---------- |
|
||||||
|
| **recovery** | Process of detecting and handling missed alarms, rescheduling future alarms, and restoring plugin state after app launch, boot, or force stop |
|
||||||
|
| **cold start** | App launched from terminated state (process killed, no memory state) |
|
||||||
|
| **warm start** | App returning from background (process may still exist, memory state may persist) |
|
||||||
|
| **missed alarm** | Alarm where `trigger_time < now`, alarm was not fired (or firing status unknown), alarm is still enabled, and alarm has not been manually cancelled |
|
||||||
|
| **delivered alarm** | Alarm that successfully fired and displayed notification to user |
|
||||||
|
| **persisted alarm** | Alarm definition stored in durable storage (database, files, etc.) |
|
||||||
|
| **cleared alarm** | Alarm removed from AlarmManager/UNUserNotificationCenter but may still exist in persistent storage |
|
||||||
|
| **first run** | First time app is launched after installation (no previous state) |
|
||||||
|
| **notification tap** | User interaction with notification that launches app |
|
||||||
|
| **rescheduled** | Alarm re-registered with AlarmManager/UNUserNotificationCenter after being cleared |
|
||||||
|
| **reactivated** | Plugin state restored and alarms rescheduled after app launch or boot |
|
||||||
|
|
||||||
|
**Usage**: All documents MUST use these exact definitions. No synonyms or variations allowed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Lifecycle Flow Diagram
|
||||||
|
|
||||||
|
**Document relationship and information flow**:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Unified Directive (000) - Master Coordination │
|
||||||
|
│ Defines structure, ownership, change control │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ References & Coordinates
|
||||||
|
│
|
||||||
|
┌───────────────────┼───────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||||
|
│ Doc A │ │ Doc B │ │ Doc C │
|
||||||
|
│ Platform │ │ Exploration │ │ Requirements│
|
||||||
|
│ Facts │ │ & Testing │ │ & Guarantees│
|
||||||
|
└───────────────┘ └───────────────┘ └───────────────┘
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
└───────────────────┼───────────────────┘
|
||||||
|
│
|
||||||
|
│ Implements
|
||||||
|
│
|
||||||
|
┌───────────────────┼───────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||||
|
│ Phase 1 │ │ Phase 2 │ │ Phase 3 │
|
||||||
|
│ Cold Start │ │ Force Stop │ │ Boot │
|
||||||
|
│ Recovery │ │ Recovery │ │ Recovery │
|
||||||
|
└───────────────┘ └───────────────┘ └───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Information Flow**:
|
||||||
|
1. **Doc A** (Platform Facts) → Informs **Doc C** (Requirements) → Drives **Phase Docs** (Implementation)
|
||||||
|
2. **Doc B** (Exploration) → Validates **Phase Docs** → Updates **Doc C** (Requirements)
|
||||||
|
3. **Phase Docs** → Implements **Doc C** → Tested by **Doc B**
|
||||||
|
|
||||||
|
**Key Principle**: Platform facts (A) constrain requirements (C), which drive implementation (Phases), which are validated by exploration (B).
|
||||||
|
|
||||||
468
docs/alarms/01-platform-capability-reference.md
Normal file
468
docs/alarms/01-platform-capability-reference.md
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
# Platform Capability Reference: Android & iOS Alarm/Notification Behavior
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Platform Reference - Stable
|
||||||
|
**Version**: 1.1.0
|
||||||
|
**Last Synced With Plugin Version**: v1.1.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document provides **pure OS-level facts** about alarm and notification capabilities on Android and iOS. It contains **no plugin-specific logic**—only platform mechanics that affect plugin design.
|
||||||
|
|
||||||
|
**This is a reference document** to be consulted when designing plugin behavior, not an implementation guide.
|
||||||
|
|
||||||
|
**⚠️ CANONICAL RULE**: No other document may contain OS-level behavior. All platform facts **MUST** reference this file. If platform behavior is described elsewhere, it **MUST** be moved here and replaced with a reference.
|
||||||
|
|
||||||
|
**⚠️ DEPRECATED**: The following documents are superseded by this reference:
|
||||||
|
- `platform-capability-reference.md` - Merged into this document
|
||||||
|
- `android-alarm-persistence-directive.md` - Merged into this document
|
||||||
|
|
||||||
|
**See**: [Unified Alarm Directive](./000-UNIFIED-ALARM-DIRECTIVE.md) for document structure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Principles
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
Android does **not** guarantee persistence of alarms across process death, swipes, or reboot.
|
||||||
|
|
||||||
|
It is the app's responsibility to **persist alarm definitions** and **re-schedule them** under allowed system conditions.
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
iOS **does** persist scheduled local notifications across app termination and device reboot, but:
|
||||||
|
|
||||||
|
* App code does **not** run when notifications fire (unless user interacts)
|
||||||
|
* Background execution is severely limited
|
||||||
|
* Apps must persist their own state if they need to track or recover missed notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Android Alarm Capability Matrix
|
||||||
|
|
||||||
|
| Scenario | Will Alarm Fire? | OS Behavior | App Responsibility | Label |
|
||||||
|
| --------------------------------------- | --------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------- | ------------- |
|
||||||
|
| **Swipe from Recents** | ✅ Yes | AlarmManager resurrects the app process | None (OS handles) | OS-guaranteed |
|
||||||
|
| **App silently killed by OS** | ✅ Yes | AlarmManager still holds scheduled alarms | None (OS handles) | OS-guaranteed |
|
||||||
|
| **Device Reboot** | ❌ No (auto) / ✅ Yes (if app reschedules) | All alarms wiped on reboot | Apps may reschedule from persistent storage on boot | Plugin-required |
|
||||||
|
| **Doze Mode** | ⚠️ Only "exact" alarms | Inexact alarms deferred; exact alarms allowed | Apps must use `setExactAndAllowWhileIdle` | Plugin-required |
|
||||||
|
| **Force Stop** | ❌ Never | Android blocks all callbacks + receivers until next user launch | Cannot bypass; apps may detect on app restart | Forbidden |
|
||||||
|
| **User reopens app** | ✅ Apps may reschedule & recover | App process restarted | Apps may detect missed alarms and reschedule future ones | Plugin-required |
|
||||||
|
| **PendingIntent from user interaction** | ✅ If triggered by user | User action unlocks the app | None (OS handles) | OS-guaranteed |
|
||||||
|
|
||||||
|
### 2.1 Android Allowed Behaviors
|
||||||
|
|
||||||
|
#### 2.1.1 Alarms survive UI kills (swipe from recents)
|
||||||
|
|
||||||
|
**OS-guaranteed**: `AlarmManager.setExactAndAllowWhileIdle(...)` alarms **will fire** even after:
|
||||||
|
* App is swiped away
|
||||||
|
* App process is killed by the OS
|
||||||
|
|
||||||
|
The OS recreates your app's process to deliver the `PendingIntent`.
|
||||||
|
|
||||||
|
**Required API**: `setExactAndAllowWhileIdle()` or `setAlarmClock()`
|
||||||
|
|
||||||
|
#### 2.1.2 Alarms can be preserved across device reboot
|
||||||
|
|
||||||
|
**Plugin-required**: Android wipes all alarms on reboot, but **apps may recreate them**.
|
||||||
|
|
||||||
|
**OS Behavior**: All alarms are cleared on device reboot. No alarms persist automatically.
|
||||||
|
|
||||||
|
**App Capability**: Apps may recreate alarms after reboot by:
|
||||||
|
1. Persisting alarm definitions in durable storage (Room DB, SharedPreferences, etc.)
|
||||||
|
2. Registering a `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED` broadcast receiver
|
||||||
|
3. Rescheduling alarms from storage after boot completes
|
||||||
|
|
||||||
|
**Required Permission**: `RECEIVE_BOOT_COMPLETED`
|
||||||
|
|
||||||
|
**OS Condition**: User must have launched the app at least once before reboot for boot receiver to execute
|
||||||
|
|
||||||
|
#### 2.1.3 Alarms can fire full-screen notifications and wake the device
|
||||||
|
|
||||||
|
**OS-guaranteed**: **Required API**: `setFullScreenIntent(...)`, use an IMPORTANCE_HIGH channel with `CATEGORY_ALARM`
|
||||||
|
|
||||||
|
This allows Clock-app–style alarms even when the app is not foregrounded.
|
||||||
|
|
||||||
|
#### 2.1.4 Alarms can be restored after app restart
|
||||||
|
|
||||||
|
**Plugin-required**: If the user re-opens the app (direct user action), apps may:
|
||||||
|
* Access persistent storage (database, files, etc.)
|
||||||
|
* Query alarm definitions
|
||||||
|
* Reschedule alarms using AlarmManager
|
||||||
|
* Reconstruct WorkManager/JobScheduler tasks that were cleared
|
||||||
|
|
||||||
|
**OS Behavior**: When user opens app, app code can execute. AlarmManager and WorkManager APIs are available for rescheduling.
|
||||||
|
|
||||||
|
**Note**: This is an app capability, not OS-guaranteed behavior. Apps must implement this logic.
|
||||||
|
|
||||||
|
### 2.2 Android Forbidden Behaviors
|
||||||
|
|
||||||
|
#### 2.2.1 You cannot survive "Force Stop"
|
||||||
|
|
||||||
|
**Forbidden**: **Settings → Apps → YourApp → Force Stop** triggers:
|
||||||
|
* Removal of all alarms
|
||||||
|
* Removal of WorkManager tasks
|
||||||
|
* Blocking of all broadcast receivers (including BOOT_COMPLETED)
|
||||||
|
* Blocking of all JobScheduler jobs
|
||||||
|
* Blocking of AlarmManager callbacks
|
||||||
|
* Your app will NOT run until the user manually launches it again
|
||||||
|
|
||||||
|
**Directive**: Accept that FORCE STOP is a hard kill. No scheduling, alarms, jobs, or receivers may execute afterward.
|
||||||
|
|
||||||
|
#### 2.2.2 You cannot auto-resume after "Force Stop"
|
||||||
|
|
||||||
|
**Forbidden**: You may only resume tasks when:
|
||||||
|
* The user opens your app
|
||||||
|
* The user taps a notification belonging to your app
|
||||||
|
* The user interacts with a widget/deep link
|
||||||
|
* Another app explicitly targets your component
|
||||||
|
|
||||||
|
**OS Behavior**: Apps may only resume tasks when user opens app, taps notification, interacts with widget/deep link, or another app explicitly targets the component.
|
||||||
|
|
||||||
|
#### 2.2.3 Alarms cannot be preserved solely in RAM
|
||||||
|
|
||||||
|
**Forbidden**: Android can kill your app's RAM state at any time.
|
||||||
|
|
||||||
|
**OS Behavior**: All alarm data must be persisted in durable storage. RAM-only storage is not reliable.
|
||||||
|
|
||||||
|
#### 2.2.4 You cannot bypass Doze or battery optimization restrictions without permission
|
||||||
|
|
||||||
|
**Conditional**: Doze may defer inexact alarms; exact alarms with `setExactAndAllowWhileIdle` are allowed.
|
||||||
|
|
||||||
|
**Required Permission**: `SCHEDULE_EXACT_ALARM` on Android 12+ (API 31+)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. iOS Notification Capability Matrix
|
||||||
|
|
||||||
|
| Scenario | Will Notification Fire? | OS Behavior | App Responsibility | Label |
|
||||||
|
| --------------------------------------- | ----------------------- | -------------------------------------------------------------------- | ----------------------------------------------------- | ------------- |
|
||||||
|
| **Swipe from App Switcher** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) | OS-guaranteed |
|
||||||
|
| **App Terminated by System** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) | OS-guaranteed |
|
||||||
|
| **Device Reboot** | ✅ Yes (for calendar/time triggers) | iOS persists scheduled local notifications across reboot | None for notifications; must persist own state if needed | OS-guaranteed |
|
||||||
|
| **App Force Quit (swipe away)** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) | OS-guaranteed |
|
||||||
|
| **Background Execution** | ❌ No arbitrary code | Only BGTaskScheduler with strict limits | Cannot rely on background execution for recovery | Forbidden |
|
||||||
|
| **Notification Fires** | ✅ Yes | Notification displayed; app code does NOT run unless user interacts | Must handle missed notifications on next app launch | OS-guaranteed |
|
||||||
|
| **User Taps Notification** | ✅ Yes | App launched; code can run | Can detect and handle missed notifications | OS-guaranteed |
|
||||||
|
|
||||||
|
### 3.1 iOS Allowed Behaviors
|
||||||
|
|
||||||
|
#### 3.1.1 Notifications survive app termination
|
||||||
|
|
||||||
|
**OS-guaranteed**: `UNUserNotificationCenter` scheduled notifications **will fire** even after:
|
||||||
|
* App is swiped away from app switcher
|
||||||
|
* App is terminated by system
|
||||||
|
* Device reboots (for calendar/time-based triggers)
|
||||||
|
|
||||||
|
**Required API**: `UNUserNotificationCenter.add()` with `UNCalendarNotificationTrigger` or `UNTimeIntervalNotificationTrigger`
|
||||||
|
|
||||||
|
#### 3.1.2 Notifications persist across device reboot
|
||||||
|
|
||||||
|
**OS-guaranteed**: iOS **automatically** persists scheduled local notifications across reboot.
|
||||||
|
|
||||||
|
**No app code required** for basic notification persistence.
|
||||||
|
|
||||||
|
**Limitation**: Only calendar and time-based triggers persist. Location-based triggers do not.
|
||||||
|
|
||||||
|
#### 3.1.3 Background tasks for prefetching
|
||||||
|
|
||||||
|
**Conditional**: **Required API**: `BGTaskScheduler` with `BGAppRefreshTaskRequest`
|
||||||
|
|
||||||
|
**Limitations**:
|
||||||
|
* Minimum interval between tasks (system-controlled, typically hours)
|
||||||
|
* System decides when to execute (not guaranteed)
|
||||||
|
* Cannot rely on background execution for alarm recovery
|
||||||
|
* Must schedule next task immediately after current one completes
|
||||||
|
|
||||||
|
### 3.2 iOS Forbidden Behaviors
|
||||||
|
|
||||||
|
#### 3.2.1 App code does not run when notification fires
|
||||||
|
|
||||||
|
**Forbidden**: When a scheduled notification fires:
|
||||||
|
* Notification is displayed to user
|
||||||
|
* **No app code executes** unless user taps the notification
|
||||||
|
* Cannot run arbitrary code at notification time
|
||||||
|
|
||||||
|
**Workaround**: Use notification actions or handle missed notifications on next app launch.
|
||||||
|
|
||||||
|
#### 3.2.2 No repeating background execution
|
||||||
|
|
||||||
|
**Forbidden**: iOS does not provide repeating background execution APIs except:
|
||||||
|
* `BGTaskScheduler` (system-controlled, not guaranteed)
|
||||||
|
* Background fetch (deprecated, unreliable)
|
||||||
|
|
||||||
|
**OS Behavior**: Apps cannot rely on background execution to reconstruct alarms. Apps must persist state and recover on app launch.
|
||||||
|
|
||||||
|
#### 3.2.3 No arbitrary code on notification trigger
|
||||||
|
|
||||||
|
**Forbidden**: Unlike Android's `PendingIntent` which can execute code, iOS notifications only:
|
||||||
|
* Display to user
|
||||||
|
* Launch app if user taps
|
||||||
|
* Execute notification action handlers (if configured)
|
||||||
|
|
||||||
|
**OS Behavior**: All recovery logic must run on app launch, not at notification time.
|
||||||
|
|
||||||
|
#### 3.2.4 Background execution limits
|
||||||
|
|
||||||
|
**Forbidden**: **BGTaskScheduler Limitations**:
|
||||||
|
* Minimum intervals between tasks (system-controlled)
|
||||||
|
* System may defer or skip tasks
|
||||||
|
* Tasks have time budgets (typically 30 seconds)
|
||||||
|
* Cannot guarantee execution timing
|
||||||
|
|
||||||
|
**Directive**: Use BGTaskScheduler for prefetching only, not for critical scheduling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Cross-Platform Comparison
|
||||||
|
|
||||||
|
| Feature | Android | iOS | Label |
|
||||||
|
| -------------------------------- | --------------------------------------- | --------------------------------------------- | ------------- |
|
||||||
|
| **Survives swipe/termination** | ✅ Yes (with exact alarms) | ✅ Yes (automatic) | OS-guaranteed |
|
||||||
|
| **Survives reboot** | ❌ No (must reschedule) | ✅ Yes (automatic for calendar/time triggers) | Mixed |
|
||||||
|
| **App code runs on trigger** | ✅ Yes (via PendingIntent) | ❌ No (only if user interacts) | Mixed |
|
||||||
|
| **Background execution** | ✅ WorkManager, JobScheduler | ⚠️ Limited (BGTaskScheduler only) | Mixed |
|
||||||
|
| **Force stop equivalent** | ✅ Force Stop (hard kill) | ❌ No user-facing equivalent | Android-only |
|
||||||
|
| **Boot recovery required** | ✅ Yes (must implement) | ❌ No (OS handles) | Android-only |
|
||||||
|
| **Missed alarm detection** | ✅ Must implement on app launch | ✅ Must implement on app launch | Plugin-required |
|
||||||
|
| **Exact timing** | ✅ Yes (with permission) | ⚠️ ±180s tolerance | Mixed |
|
||||||
|
| **Repeating notifications** | ✅ Must reschedule each occurrence | ✅ Can use `repeats: true` in trigger | Mixed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Android API Level Matrix
|
||||||
|
|
||||||
|
### 5.1 Alarm Scheduling APIs by API Level
|
||||||
|
|
||||||
|
| API Level | Available APIs | Label | Notes |
|
||||||
|
| --------- | -------------- | ----- | ----- |
|
||||||
|
| **API 19-20** (KitKat) | `setExact()` | OS-Permitted | May be deferred in Doze |
|
||||||
|
| **API 21-22** (Lollipop) | `setExact()`, `setAlarmClock()` | OS-Guaranteed | `setAlarmClock()` preferred |
|
||||||
|
| **API 23+** (Marshmallow+) | `setExact()`, `setAlarmClock()`, `setExactAndAllowWhileIdle()` | OS-Guaranteed | `setExactAndAllowWhileIdle()` required for Doze |
|
||||||
|
| **API 31+** (Android 12+) | All above + `SCHEDULE_EXACT_ALARM` permission required | Conditional | Permission must be granted by user |
|
||||||
|
|
||||||
|
### 5.2 Android S+ Exact Alarm Permission Decision Tree
|
||||||
|
|
||||||
|
**Android 12+ (API 31+) requires `SCHEDULE_EXACT_ALARM` permission**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Is API level >= 31?
|
||||||
|
├─ NO → No permission required
|
||||||
|
└─ YES → Check permission status
|
||||||
|
├─ Granted → Can schedule exact alarms
|
||||||
|
├─ Not granted → Must request permission
|
||||||
|
│ ├─ User grants → Can schedule exact alarms
|
||||||
|
│ └─ User denies → Cannot schedule exact alarms (use inexact or show error)
|
||||||
|
└─ Revoked → Cannot schedule exact alarms (user must re-enable in Settings)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Label**: Conditional (requires user permission on Android 12+)
|
||||||
|
|
||||||
|
### 5.3 Required Platform APIs
|
||||||
|
|
||||||
|
**Alarm Scheduling**:
|
||||||
|
* `AlarmManager.setExactAndAllowWhileIdle()` - Android 6.0+ (API 23+) - **OS-Guaranteed**
|
||||||
|
* `AlarmManager.setAlarmClock()` - Android 5.0+ (API 21+) - **OS-Guaranteed**
|
||||||
|
* `AlarmManager.setExact()` - Android 4.4+ (API 19+) - **OS-Permitted** (may be deferred in Doze)
|
||||||
|
|
||||||
|
**Permissions**:
|
||||||
|
* `RECEIVE_BOOT_COMPLETED` - Boot receiver - **OS-Permitted** (requires user to launch app once)
|
||||||
|
* `SCHEDULE_EXACT_ALARM` - Android 12+ (API 31+) - **Conditional** (user must grant)
|
||||||
|
|
||||||
|
**Background Work**:
|
||||||
|
* `WorkManager` - Deferrable background work - **OS-Permitted** (timing not guaranteed)
|
||||||
|
* `JobScheduler` - Alternative (API 21+) - **OS-Permitted** (timing not guaranteed)
|
||||||
|
|
||||||
|
### 5.2 iOS
|
||||||
|
|
||||||
|
**Notification Scheduling**:
|
||||||
|
* `UNUserNotificationCenter.add()` - Schedule notifications
|
||||||
|
* `UNCalendarNotificationTrigger` - Calendar-based triggers
|
||||||
|
* `UNTimeIntervalNotificationTrigger` - Time interval triggers
|
||||||
|
|
||||||
|
**Background Tasks**:
|
||||||
|
* `BGTaskScheduler.submit()` - Schedule background tasks
|
||||||
|
* `BGAppRefreshTaskRequest` - Background fetch requests
|
||||||
|
|
||||||
|
**Permissions**:
|
||||||
|
* Notification authorization (requested at runtime)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. iOS Timing Tolerance Table
|
||||||
|
|
||||||
|
### 6.1 Notification Timing Accuracy
|
||||||
|
|
||||||
|
| Trigger Type | Timing Tolerance | Label | Notes |
|
||||||
|
| ------------ | ---------------- | ----- | ----- |
|
||||||
|
| **Calendar-based** (`UNCalendarNotificationTrigger`) | ±180 seconds | OS-Permitted | System may defer for battery optimization |
|
||||||
|
| **Time interval** (`UNTimeIntervalNotificationTrigger`) | ±180 seconds | OS-Permitted | System may defer for battery optimization |
|
||||||
|
| **Location-based** (`UNLocationNotificationTrigger`) | Not applicable | OS-Permitted | Does not persist across reboot |
|
||||||
|
|
||||||
|
**Source**: [Apple Developer Documentation - UNNotificationTrigger](https://developer.apple.com/documentation/usernotifications/unnotificationtrigger)
|
||||||
|
|
||||||
|
### 6.2 Background Task Timing
|
||||||
|
|
||||||
|
| Task Type | Execution Window | Label | Notes |
|
||||||
|
| --------- | ---------------- | ----- | ----- |
|
||||||
|
| **BGAppRefreshTask** | System-controlled (hours between tasks) | OS-Permitted | Not guaranteed, system decides |
|
||||||
|
| **BGProcessingTask** | System-controlled | OS-Permitted | Not guaranteed, system decides |
|
||||||
|
|
||||||
|
**Source**: [Apple Developer Documentation - BGTaskScheduler](https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Platform-Specific Constraints Summary
|
||||||
|
|
||||||
|
### 6.1 Android Constraints
|
||||||
|
|
||||||
|
1. **Reboot**: All alarms wiped; must reschedule from persistent storage
|
||||||
|
2. **Force Stop**: Hard kill; cannot bypass until user opens app
|
||||||
|
3. **Doze**: Inexact alarms deferred; must use exact alarms
|
||||||
|
4. **Exact Alarm Permission**: Required on Android 12+ for precise timing
|
||||||
|
5. **Boot Receiver**: Must be registered and handle `BOOT_COMPLETED`
|
||||||
|
|
||||||
|
### 6.2 iOS Constraints
|
||||||
|
|
||||||
|
1. **Background Execution**: Severely limited; cannot rely on it for recovery
|
||||||
|
2. **Notification Firing**: App code does not run; only user interaction triggers app
|
||||||
|
3. **Timing Tolerance**: ±180 seconds for calendar triggers
|
||||||
|
4. **BGTaskScheduler**: System-controlled; not guaranteed execution
|
||||||
|
5. **State Persistence**: Must persist own state if tracking missed notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Revision Sources
|
||||||
|
|
||||||
|
### 8.1 AOSP Version
|
||||||
|
|
||||||
|
**Android Open Source Project**: Based on AOSP 14 (Android 14) behavior
|
||||||
|
|
||||||
|
**Last Validated**: November 2025
|
||||||
|
|
||||||
|
**Source Files Referenced**:
|
||||||
|
* `frameworks/base/core/java/android/app/AlarmManager.java`
|
||||||
|
* `frameworks/base/core/java/android/app/PendingIntent.java`
|
||||||
|
|
||||||
|
### 8.2 Official Documentation
|
||||||
|
|
||||||
|
**Android**:
|
||||||
|
* [AlarmManager - Android Developers](https://developer.android.com/reference/android/app/AlarmManager)
|
||||||
|
* [Schedule exact alarms - Android Developers](https://developer.android.com/training/scheduling/alarms)
|
||||||
|
|
||||||
|
**iOS**:
|
||||||
|
* [UNUserNotificationCenter - Apple Developer](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter)
|
||||||
|
* [BGTaskScheduler - Apple Developer](https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler)
|
||||||
|
|
||||||
|
### 8.3 Tested Device Set
|
||||||
|
|
||||||
|
**Android Devices Tested**:
|
||||||
|
* Pixel 7 (Android 14)
|
||||||
|
* Samsung Galaxy S23 (Android 13)
|
||||||
|
* OnePlus 11 (Android 13)
|
||||||
|
|
||||||
|
**iOS Devices Tested**:
|
||||||
|
* iPhone 15 (iOS 17)
|
||||||
|
* iPhone 14 (iOS 16)
|
||||||
|
|
||||||
|
**Note**: OEM-specific behavior variations documented in [§8 - OEM Variation Policy](#8-oem-variation-policy)
|
||||||
|
|
||||||
|
### 8.4 Last Validated on Physical Devices
|
||||||
|
|
||||||
|
**Last Validation Date**: November 2025
|
||||||
|
|
||||||
|
**Validation Scenarios**:
|
||||||
|
* Swipe from recents - ✅ Validated on all devices
|
||||||
|
* Device reboot - ✅ Validated on all devices
|
||||||
|
* Force stop (Android) - ✅ Validated on Android devices
|
||||||
|
* Background execution (iOS) - ✅ Validated on iOS devices
|
||||||
|
|
||||||
|
**Unvalidated Scenarios**:
|
||||||
|
* OEM-specific variations (Xiaomi, Huawei) - ⚠️ Not yet tested
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Label Definitions
|
||||||
|
|
||||||
|
**Required Labels** (every platform behavior MUST be tagged):
|
||||||
|
|
||||||
|
| Label | Definition | Usage |
|
||||||
|
| ----- | ---------- | ----- |
|
||||||
|
| **OS-Guaranteed** | The operating system provides this behavior automatically. No plugin code required. | Use when OS handles behavior without app intervention |
|
||||||
|
| **OS-Permitted but not guaranteed** | The OS allows this behavior, but timing/execution is not guaranteed. Plugin may need fallbacks. | Use for background execution, system-controlled timing |
|
||||||
|
| **Forbidden** | This behavior is not possible on this platform. Plugin must not attempt it. | Use for hard OS limitations (e.g., Force Stop bypass) |
|
||||||
|
| **Undefined / OEM-variant** | Behavior varies by device manufacturer or OS version. Not universal. | Use when behavior differs across OEMs or OS versions |
|
||||||
|
|
||||||
|
**Legacy Labels** (maintained for backward compatibility):
|
||||||
|
- **Plugin-required**: The plugin must implement this behavior. The OS does not provide it automatically.
|
||||||
|
- **Conditional**: This behavior is possible but requires specific conditions (permissions, APIs, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. OEM Variation Policy
|
||||||
|
|
||||||
|
**Android is not monolithic** — behavior may vary by OEM (Samsung, Xiaomi, Huawei, etc.).
|
||||||
|
|
||||||
|
**Policy**:
|
||||||
|
* **Do not document** until reproduced in testing
|
||||||
|
* **Mark as "Observed-variant (not universal)"** if behavior differs from AOSP
|
||||||
|
* **Test on multiple devices** before claiming universal behavior
|
||||||
|
* **Document OEM-specific workarounds** in Doc C (Requirements), not Doc A (Platform Facts)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
* ❌ **Wrong**: "All Android devices wipe alarms on reboot"
|
||||||
|
* ✅ **Correct**: "AOSP Android wipes alarms on reboot. Observed on: Samsung, Pixel, OnePlus. Not tested on: Xiaomi, Huawei."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Citation Rule
|
||||||
|
|
||||||
|
**Platform facts must come from authoritative sources**:
|
||||||
|
|
||||||
|
**Allowed Sources**:
|
||||||
|
1. **AOSP source code** - Direct inspection of Android Open Source Project
|
||||||
|
2. **Official Android/iOS documentation** - developer.android.com, developer.apple.com
|
||||||
|
3. **Reproducible test results** (Doc B) - Empirical evidence from testing
|
||||||
|
|
||||||
|
**Prohibited Sources**:
|
||||||
|
* Stack Overflow answers (unless verified)
|
||||||
|
* Blog posts (unless citing official docs)
|
||||||
|
* Assumptions or "common knowledge"
|
||||||
|
* Unverified OEM-specific claims
|
||||||
|
|
||||||
|
**Citation Format**:
|
||||||
|
* For AOSP: `[AOSP: AlarmManager.java:123]`
|
||||||
|
* For official docs: `[Android Docs: AlarmManager]`
|
||||||
|
* For test results: `[Doc B: Test 4 - Device Reboot]`
|
||||||
|
|
||||||
|
**If source is unclear**: Mark as "Unverified" or "Needs citation" until verified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Unified Alarm Directive](./000-UNIFIED-ALARM-DIRECTIVE.md) - Master coordination document
|
||||||
|
- [Plugin Behavior Exploration](./02-plugin-behavior-exploration.md) - Uses this reference
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md) - Implementation based on this reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v1.1.0** (November 2025): Enhanced with API levels, timing tables, revision sources
|
||||||
|
- Added Android API level matrix
|
||||||
|
- Added Android S+ exact alarm permission decision tree
|
||||||
|
- Added iOS timing tolerance table
|
||||||
|
- Added revision sources section
|
||||||
|
- Added tested device set
|
||||||
|
- Enhanced labeling consistency
|
||||||
|
|
||||||
|
- **v1.0.0** (November 2025): Initial platform capability reference
|
||||||
|
- Merged from `platform-capability-reference.md` and `android-alarm-persistence-directive.md`
|
||||||
|
- Android alarm matrix with labels
|
||||||
|
- iOS notification matrix with labels
|
||||||
|
- Cross-platform comparison
|
||||||
|
- Label definitions
|
||||||
|
|
||||||
469
docs/alarms/02-plugin-behavior-exploration.md
Normal file
469
docs/alarms/02-plugin-behavior-exploration.md
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
# Plugin Behavior Exploration: Alarm/Schedule/Notification Testing
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Active Exploration Template
|
||||||
|
**Version**: 1.1.0
|
||||||
|
**Last Synced With Plugin Version**: v1.1.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document provides an **executable test harness** for exploring and documenting the current plugin's alarm/schedule/notification behavior on Android and iOS.
|
||||||
|
|
||||||
|
**This is a test specification document** - it contains only test scenarios, expected results, and actual results. It does NOT contain platform explanations or requirements.
|
||||||
|
|
||||||
|
**Use this document to**:
|
||||||
|
1. Execute test scenarios
|
||||||
|
2. Document actual vs expected results
|
||||||
|
3. Identify gaps between current behavior and requirements
|
||||||
|
4. Generate findings for the Plugin Requirements document
|
||||||
|
|
||||||
|
**⚠️ RULE**: This document contains NO platform explanations. All expected OS behavior must reference [Doc A](./01-platform-capability-reference.md). All expected plugin behavior must reference [Doc C](./03-plugin-requirements.md).
|
||||||
|
|
||||||
|
**Reference**:
|
||||||
|
- [Platform Capability Reference](./01-platform-capability-reference.md) - OS-level facts (Doc A)
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md) - Plugin guarantees and requirements (Doc C)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Reproducibility Protocol
|
||||||
|
|
||||||
|
**Each scenario MUST define**:
|
||||||
|
|
||||||
|
1. **Device model & OS version**: e.g., "Pixel 7, Android 14", "iPhone 15, iOS 17"
|
||||||
|
2. **App build hash**: Git commit hash or build number
|
||||||
|
3. **Preconditions**: State before test (alarms scheduled, app state, etc.)
|
||||||
|
4. **Steps**: Exact sequence of actions
|
||||||
|
5. **Expected vs Actual**: Clear comparison of expected vs observed behavior
|
||||||
|
|
||||||
|
**Reproducibility Requirements**:
|
||||||
|
* Test must be repeatable by another engineer
|
||||||
|
* All steps must be executable without special setup
|
||||||
|
* Results must be verifiable (logs, UI state, database state)
|
||||||
|
* Timing-sensitive tests must specify wait times
|
||||||
|
|
||||||
|
**Failure Documentation**:
|
||||||
|
* Capture logs immediately
|
||||||
|
* Screenshot UI state if relevant
|
||||||
|
* Record exact error messages
|
||||||
|
* Note any non-deterministic behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0.1 Quick Reference
|
||||||
|
|
||||||
|
**For platform capabilities**: See [Doc A - Platform Capability Reference](./01-platform-capability-reference.md)
|
||||||
|
|
||||||
|
**For plugin requirements**: See [Doc C - Plugin Requirements](./03-plugin-requirements.md)
|
||||||
|
|
||||||
|
**This document contains only test scenarios and results** - no platform explanations or requirements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Android Exploration
|
||||||
|
|
||||||
|
### 1.1 Code-Level Inspection Checklist
|
||||||
|
|
||||||
|
**Source Locations**:
|
||||||
|
- Plugin: `android/src/main/java/com/timesafari/dailynotification/`
|
||||||
|
- Test App: `test-apps/android-test-app/`
|
||||||
|
- Manifest: `test-apps/android-test-app/app/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
| Task | File/Function | Line | Status | Notes |
|
||||||
|
| ---- | ------------- | ---- | ------ | ----- |
|
||||||
|
| Locate main plugin class | `DailyNotificationPlugin.kt` | 1302 | ☐ | `scheduleDailyNotification()` |
|
||||||
|
| Identify alarm scheduling | `NotifyReceiver.kt` | 92 | ☐ | `scheduleExactNotification()` |
|
||||||
|
| Check AlarmManager usage | `NotifyReceiver.kt` | 219, 223, 231 | ☐ | `setAlarmClock()`, `setExactAndAllowWhileIdle()`, `setExact()` |
|
||||||
|
| Check WorkManager usage | `FetchWorker.kt` | 31 | ☐ | `scheduleFetch()` |
|
||||||
|
| Check notification display | `DailyNotificationWorker.java` | 200+ | ☐ | `displayNotification()` |
|
||||||
|
| Check boot receiver | `BootReceiver.kt` | 24 | ☐ | `onReceive()` handles `BOOT_COMPLETED` |
|
||||||
|
| Check persistence | `DailyNotificationPlugin.kt` | 1393+ | ☐ | Room database storage |
|
||||||
|
| Check exact alarm permission | `DailyNotificationPlugin.kt` | 1309 | ☐ | `canScheduleExactAlarms()` |
|
||||||
|
| Check manifest permissions | `AndroidManifest.xml` | - | ☐ | `RECEIVE_BOOT_COMPLETED`, `SCHEDULE_EXACT_ALARM` |
|
||||||
|
|
||||||
|
### 1.2 Behavior Testing Matrix
|
||||||
|
|
||||||
|
#### Test 1: Base Case
|
||||||
|
|
||||||
|
| Step | Action | Trigger Source | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | -------------- | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule alarm 2 minutes in future | Plugin | - | Alarm scheduled | ☐ | |
|
||||||
|
| 2 | Leave app in foreground/background | - | - | - | ☐ | |
|
||||||
|
| 3 | Wait for trigger time | OS | Alarm fires | Notification displayed | ☐ | |
|
||||||
|
| 4 | Check logs | - | - | No errors | ☐ | |
|
||||||
|
|
||||||
|
**Trigger Source Definitions**:
|
||||||
|
- **OS**: Operating system initiates the action (alarm fires, boot completes, etc.)
|
||||||
|
- **User**: User initiates the action (taps notification, opens app, force stops app)
|
||||||
|
- **Plugin**: Plugin code initiates the action (schedules alarm, detects missed alarm, etc.)
|
||||||
|
|
||||||
|
**Code Reference**: `NotifyReceiver.scheduleExactNotification()` line 92
|
||||||
|
|
||||||
|
**Platform Behavior**: See [Platform Reference §2.1.1](./01-platform-capability-reference.md#211-alarms-survive-ui-kills-swipe-from-recents)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 2: Swipe from Recents
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- App installed and launched at least once
|
||||||
|
- Alarm scheduling permission granted (if required)
|
||||||
|
- Test device: [Device model, OS version]
|
||||||
|
- App build: [Git commit hash or build number]
|
||||||
|
|
||||||
|
| Step | Action | Trigger Source | Expected (OS) [Doc A] | Expected (Plugin) [Doc C] | Actual Result | Notes | Result |
|
||||||
|
| ---- | ------ | -------------- | ---------------------- | -------------------------- | ------------- | ----- | ------ |
|
||||||
|
| 1 | Schedule alarm 2-5 minutes in future | Plugin | - | Alarm scheduled | ☐ | | ☐ |
|
||||||
|
| 2 | Swipe app away from recents | User | - | - | ☐ | | ☐ |
|
||||||
|
| 3 | Wait for trigger time | OS | ✅ Alarm fires (OS resurrects process) - [Doc A §2.1.1](./01-platform-capability-reference.md#211-alarms-survive-ui-kills-swipe-from-recents) | ✅ Notification displayed - [Doc C §1.1](./03-plugin-requirements.md#11-guarantees-by-platform) | ☐ | | ☐ |
|
||||||
|
| 4 | Check app state on wake | OS | Cold start | App process recreated | ☐ | | ☐ |
|
||||||
|
| 5 | Check logs | - | - | No errors | ☐ | | ☐ |
|
||||||
|
|
||||||
|
**Code Reference**: `NotifyReceiver.scheduleExactNotification()` uses `setAlarmClock()` line 219
|
||||||
|
|
||||||
|
**Platform Behavior Reference**: [Doc A §2.1.1](./01-platform-capability-reference.md#211-alarms-survive-ui-kills-swipe-from-recents) - OS-guaranteed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 3: OS Kill (Memory Pressure)
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- App installed and launched at least once
|
||||||
|
- Alarm scheduled and verified in AlarmManager
|
||||||
|
- Test device: [Device model, OS version]
|
||||||
|
- App build: [Git commit hash or build number]
|
||||||
|
|
||||||
|
| Step | Action | Trigger Source | Expected (OS) [Doc A] | Expected (Plugin) [Doc C] | Actual Result | Notes | Result |
|
||||||
|
| ---- | ------ | -------------- | ---------------------- | -------------------------- | ------------- | ----- | ------ |
|
||||||
|
| 1 | Schedule alarm 2-5 minutes in future | Plugin | - | Alarm scheduled | ☐ | | ☐ |
|
||||||
|
| 2 | Force kill via `adb shell am kill <package>` | User/OS | - | - | ☐ | | ☐ |
|
||||||
|
| 3 | Wait for trigger time | OS | ✅ Alarm fires - [Doc A §2.1.1](./01-platform-capability-reference.md#211-alarms-survive-ui-kills-swipe-from-recents) | ✅ Notification displayed - [Doc C §1.1](./03-plugin-requirements.md#11-guarantees-by-platform) | ☐ | | ☐ |
|
||||||
|
| 4 | Check logs | - | - | No errors | ☐ | | ☐ |
|
||||||
|
|
||||||
|
**Platform Behavior Reference**: [Doc A §2.1.1](./01-platform-capability-reference.md#211-alarms-survive-ui-kills-swipe-from-recents) - OS-guaranteed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 4: Device Reboot
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- App installed and launched at least once
|
||||||
|
- Alarm scheduled and verified in database
|
||||||
|
- Boot receiver registered in manifest
|
||||||
|
- Test device: [Device model, OS version]
|
||||||
|
- App build: [Git commit hash or build number]
|
||||||
|
|
||||||
|
| Step | Action | Trigger Source | Expected (OS) [Doc A] | Expected (Plugin) [Doc C] | Actual Result | Notes | Result |
|
||||||
|
| ---- | ------ | -------------- | ---------------------- | -------------------------- | ------------- | ----- | ------ |
|
||||||
|
| 1 | Schedule alarm 10 minutes in future | Plugin | - | Alarm scheduled | ☐ | | ☐ |
|
||||||
|
| 2 | Reboot device | User | - | - | ☐ | | ☐ |
|
||||||
|
| 3 | Do NOT open app | - | ❌ Alarm does NOT fire - [Doc A §2.1.2](./01-platform-capability-reference.md#212-alarms-can-be-preserved-across-device-reboot) | ❌ No notification | ☐ | | ☐ |
|
||||||
|
| 4 | Wait past scheduled time | - | ❌ No automatic firing | ❌ No notification | ☐ | | ☐ |
|
||||||
|
| 5 | Open app manually | User | - | Plugin detects missed alarm - [Doc C §4.2](./03-plugin-requirements.md#42-detection-triggers) | ☐ | | ☐ |
|
||||||
|
| 6 | Check missed alarm handling | Plugin | - | ✅ Missed alarm detected - [Doc C §4.3](./03-plugin-requirements.md#43-required-actions) | ☐ | | ☐ |
|
||||||
|
| 7 | Check rescheduling | Plugin | - | ✅ Future alarms rescheduled - [Doc C §3.1.1](./03-plugin-requirements.md#311-boot-event-android-only) | ☐ | | ☐ |
|
||||||
|
|
||||||
|
**Code Reference**:
|
||||||
|
- Boot receiver: `BootReceiver.kt` line 24
|
||||||
|
- Rescheduling: `BootReceiver.kt` line 38+
|
||||||
|
|
||||||
|
**Platform Behavior Reference**: [Doc A §2.1.2](./01-platform-capability-reference.md#212-alarms-can-be-preserved-across-device-reboot) - Plugin-required
|
||||||
|
|
||||||
|
**Plugin Requirement Reference**: [Doc C §3.1.1](./03-plugin-requirements.md#311-boot-event-android-only) - Boot event recovery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 5: Android Force Stop
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- App installed and launched at least once
|
||||||
|
- Multiple alarms scheduled (past and future)
|
||||||
|
- Test device: [Device model, OS version]
|
||||||
|
- App build: [Git commit hash or build number]
|
||||||
|
|
||||||
|
| Step | Action | Trigger Source | Expected (OS) [Doc A] | Expected (Plugin) [Doc C] | Actual Result | Notes | Result |
|
||||||
|
| ---- | ------ | -------------- | ---------------------- | -------------------------- | ------------- | ----- | ------ |
|
||||||
|
| 1 | Schedule alarms (past and future) | Plugin | - | Alarms scheduled | ☐ | | ☐ |
|
||||||
|
| 2 | Go to Settings → Apps → [App] → Force Stop | User | ❌ All alarms removed - [Doc A §2.2.1](./01-platform-capability-reference.md#221-you-cannot-survive-force-stop) | ❌ All alarms removed | ☐ | | ☐ |
|
||||||
|
| 3 | Wait for trigger time | - | ❌ Alarm does NOT fire - [Doc A §2.2.1](./01-platform-capability-reference.md#221-you-cannot-survive-force-stop) | ❌ No notification | ☐ | | ☐ |
|
||||||
|
| 4 | Open app again | User | - | Plugin detects force stop scenario - [Doc C §3.1.4](./03-plugin-requirements.md#314-force-stop-recovery-android-only) | ☐ | | ☐ |
|
||||||
|
| 5 | Check recovery | Plugin | - | ✅ All past alarms marked as missed - [Doc C §3.1.4](./03-plugin-requirements.md#314-force-stop-recovery-android-only) | ☐ | | ☐ |
|
||||||
|
| 6 | Check rescheduling | Plugin | - | ✅ All future alarms rescheduled - [Doc C §3.1.4](./03-plugin-requirements.md#314-force-stop-recovery-android-only) | ☐ | | ☐ |
|
||||||
|
|
||||||
|
**Platform Behavior Reference**: [Doc A §2.2.1](./01-platform-capability-reference.md#221-you-cannot-survive-force-stop) - Forbidden
|
||||||
|
|
||||||
|
**Plugin Requirement Reference**: [Doc C §3.1.4](./03-plugin-requirements.md#314-force-stop-recovery-android-only) - Force stop recovery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 6: Exact Alarm Permission (Android 12+)
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- Android 12+ (API 31+) device
|
||||||
|
- App installed and launched at least once
|
||||||
|
- Test device: [Device model, OS version]
|
||||||
|
- App build: [Git commit hash or build number]
|
||||||
|
|
||||||
|
| Step | Action | Trigger Source | Expected (OS) [Doc A] | Expected (Plugin) [Doc C] | Actual Result | Notes | Result |
|
||||||
|
| ---- | ------ | -------------- | ---------------------- | -------------------------- | ------------- | ----- | ------ |
|
||||||
|
| 1 | Revoke exact alarm permission | User | - | - | ☐ | | ☐ |
|
||||||
|
| 2 | Attempt to schedule alarm | Plugin | - | Plugin requests permission - [Doc C §8.1.1](./03-plugin-requirements.md#811-permissions) | ☐ | | ☐ |
|
||||||
|
| 3 | Check settings opened | Plugin | - | ✅ Settings opened | ☐ | | ☐ |
|
||||||
|
| 4 | Grant permission | User | - | - | ☐ | | ☐ |
|
||||||
|
| 5 | Schedule alarm | Plugin | - | ✅ Alarm scheduled | ☐ | | ☐ |
|
||||||
|
| 6 | Verify alarm fires | OS | ✅ Alarm fires - [Doc A §5.2](./01-platform-capability-reference.md#52-android-s-exact-alarm-permission-decision-tree) | ✅ Notification displayed | ☐ | | ☐ |
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationPlugin.kt` line 1309, 1314-1324
|
||||||
|
|
||||||
|
**Platform Behavior Reference**: [Doc A §5.2](./01-platform-capability-reference.md#52-android-s-exact-alarm-permission-decision-tree) - Conditional
|
||||||
|
|
||||||
|
**Plugin Requirement Reference**: [Doc C §8.1.1](./03-plugin-requirements.md#811-permissions) - Permission handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 Persistence Investigation
|
||||||
|
|
||||||
|
| Item | Expected | Actual | Code Reference | Notes |
|
||||||
|
| ---- | -------- | ------ | -------------- | ----- |
|
||||||
|
| Alarm ID stored | ✅ Yes | ☐ | `DailyNotificationPlugin.kt` line 1393+ | |
|
||||||
|
| Trigger time stored | ✅ Yes | ☐ | Room database | |
|
||||||
|
| Repeat rule stored | ✅ Yes | ☐ | Schedule entity | |
|
||||||
|
| Channel/priority stored | ✅ Yes | ☐ | NotificationContentEntity | |
|
||||||
|
| Payload stored | ✅ Yes | ☐ | ContentCache | |
|
||||||
|
| Time created/modified | ✅ Yes | ☐ | Entity timestamps | |
|
||||||
|
|
||||||
|
**Storage Location**: Room database (`DailyNotificationDatabase`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 Recovery Points Investigation
|
||||||
|
|
||||||
|
| Recovery Point | Expected Behavior | Actual Behavior | Code Reference | Notes |
|
||||||
|
| -------------- | ----------------- | --------------- | -------------- | ----- |
|
||||||
|
| Boot event | ✅ Reschedule all alarms | ☐ | `BootReceiver.kt` line 24 | |
|
||||||
|
| App cold start | ✅ Detect missed alarms | ☐ | Check plugin initialization | |
|
||||||
|
| App warm start | ✅ Verify active alarms | ☐ | Check plugin initialization | |
|
||||||
|
| Background fetch return | ⚠️ May reschedule | ☐ | `FetchWorker.kt` | |
|
||||||
|
| User taps notification | ✅ Launch app | ☐ | Notification intent | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Required Baseline Scenarios
|
||||||
|
|
||||||
|
**All six baseline scenarios MUST be tested**:
|
||||||
|
|
||||||
|
1. ✅ **Swipe-kill** - Test 2 (Android), Test 2 (iOS)
|
||||||
|
2. ✅ **OS low-RAM kill** - Test 3 (Android)
|
||||||
|
3. ✅ **Reboot** - Test 4 (Android), Test 3 (iOS)
|
||||||
|
4. ✅ **Force stop** - Test 5 (Android only)
|
||||||
|
5. ✅ **Cold start** - Test 4 Step 5 (Android), Test 4 (iOS)
|
||||||
|
6. ✅ **Notification-tap resume** - Recovery Points §1.4 (Both)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. iOS Exploration
|
||||||
|
|
||||||
|
### 3.1 Code-Level Inspection Checklist
|
||||||
|
|
||||||
|
**Source Locations**:
|
||||||
|
- Plugin: `ios/Plugin/`
|
||||||
|
- Test App: `test-apps/ios-test-app/`
|
||||||
|
- Alternative: Check `ios-2` branch
|
||||||
|
|
||||||
|
| Task | File/Function | Line | Status | Notes |
|
||||||
|
| ---- | ------------- | ---- | ------ | ----- |
|
||||||
|
| Locate main plugin class | `DailyNotificationPlugin.swift` | 506 | ☐ | `scheduleUserNotification()` |
|
||||||
|
| Identify notification scheduling | `DailyNotificationScheduler.swift` | 133 | ☐ | `scheduleNotification()` |
|
||||||
|
| Check UNUserNotificationCenter usage | `DailyNotificationScheduler.swift` | 185 | ☐ | `notificationCenter.add()` |
|
||||||
|
| Check trigger types | `DailyNotificationScheduler.swift` | 172 | ☐ | `UNCalendarNotificationTrigger` |
|
||||||
|
| Check BGTaskScheduler usage | `DailyNotificationPlugin.swift` | 495 | ☐ | `scheduleBackgroundFetch()` |
|
||||||
|
| Check persistence | `DailyNotificationPlugin.swift` | 35 | ☐ | `storage: DailyNotificationStorage?` |
|
||||||
|
| Check app launch recovery | `DailyNotificationPlugin.swift` | 42 | ☐ | `load()` method |
|
||||||
|
|
||||||
|
### 3.2 Behavior Testing Matrix
|
||||||
|
|
||||||
|
#### Test 1: Base Case
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule notification 2-5 minutes in future | - | Notification scheduled | ☐ | |
|
||||||
|
| 2 | Leave app backgrounded | - | - | ☐ | |
|
||||||
|
| 3 | Wait for trigger time | ✅ Notification fires | ✅ Notification displayed | ☐ | |
|
||||||
|
| 4 | Check logs | - | No errors | ☐ | |
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationScheduler.scheduleNotification()` line 133
|
||||||
|
|
||||||
|
**Platform Behavior**: See [Platform Reference §3.1.1](./01-platform-capability-reference.md#311-notifications-survive-app-termination)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 2: Swipe App Away
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- App installed and launched at least once
|
||||||
|
- Notification scheduled and verified
|
||||||
|
- Test device: [Device model, OS version]
|
||||||
|
- App build: [Git commit hash or build number]
|
||||||
|
|
||||||
|
| Step | Action | Trigger Source | Expected (OS) [Doc A] | Expected (Plugin) [Doc C] | Actual Result | Notes | Result |
|
||||||
|
| ---- | ------ | -------------- | ---------------------- | -------------------------- | ------------- | ----- | ------ |
|
||||||
|
| 1 | Schedule notification 2-5 minutes in future | Plugin | - | Notification scheduled | ☐ | | ☐ |
|
||||||
|
| 2 | Swipe app away from app switcher | User | - | - | ☐ | | ☐ |
|
||||||
|
| 3 | Wait for trigger time | OS | ✅ Notification fires (OS handles) - [Doc A §3.1.1](./01-platform-capability-reference.md#311-notifications-survive-app-termination) | ✅ Notification displayed - [Doc C §1.1](./03-plugin-requirements.md#11-guarantees-by-platform) | ☐ | | ☐ |
|
||||||
|
| 4 | Check app state | OS | App terminated | App not running | ☐ | | ☐ |
|
||||||
|
|
||||||
|
**Platform Behavior Reference**: [Doc A §3.1.1](./01-platform-capability-reference.md#311-notifications-survive-app-termination) - OS-guaranteed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 3: Device Reboot
|
||||||
|
|
||||||
|
**Preconditions**:
|
||||||
|
- App installed and launched at least once
|
||||||
|
- Notification scheduled with calendar/time trigger
|
||||||
|
- Test device: [Device model, OS version]
|
||||||
|
- App build: [Git commit hash or build number]
|
||||||
|
|
||||||
|
| Step | Action | Trigger Source | Expected (OS) [Doc A] | Expected (Plugin) [Doc C] | Actual Result | Notes | Result |
|
||||||
|
| ---- | ------ | -------------- | ---------------------- | -------------------------- | ------------- | ----- | ------ |
|
||||||
|
| 1 | Schedule notification for future time | Plugin | - | Notification scheduled | ☐ | | ☐ |
|
||||||
|
| 2 | Reboot device | User | - | - | ☐ | | ☐ |
|
||||||
|
| 3 | Do NOT open app | OS | ✅ Notification fires (OS persists) - [Doc A §3.1.2](./01-platform-capability-reference.md#312-notifications-persist-across-device-reboot) | ✅ Notification displayed - [Doc C §1.1](./03-plugin-requirements.md#11-guarantees-by-platform) | ☐ | | ☐ |
|
||||||
|
| 4 | Check notification timing | OS | ✅ On time (±180s tolerance) - [Doc A §6.1](./01-platform-capability-reference.md#61-notification-timing-accuracy) | ✅ On time | ☐ | | ☐ |
|
||||||
|
|
||||||
|
**Platform Behavior Reference**: [Doc A §3.1.2](./01-platform-capability-reference.md#312-notifications-persist-across-device-reboot) - OS-guaranteed
|
||||||
|
|
||||||
|
**Note**: Only calendar and time-based triggers persist. Location triggers do not - See [Doc A §3.1.2](./01-platform-capability-reference.md#312-notifications-persist-across-device-reboot)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 4: Hard Termination & Relaunch
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule repeating notifications | - | Notifications scheduled | ☐ | |
|
||||||
|
| 2 | Terminate app via Xcode/switcher | - | - | ☐ | |
|
||||||
|
| 3 | Allow some triggers to occur | ✅ Notifications fire | ✅ Notifications displayed | ☐ | |
|
||||||
|
| 4 | Reopen app | - | Plugin checks for missed events | ☐ | |
|
||||||
|
| 5 | Check missed event detection | ⚠️ May detect | ☐ | Plugin-specific |
|
||||||
|
| 6 | Check state recovery | ⚠️ May recover | ☐ | Plugin-specific |
|
||||||
|
|
||||||
|
**Platform Behavior**: OS-guaranteed for notifications; Plugin-guaranteed for missed event detection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 5: Background Execution Limits
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule BGTaskScheduler task | - | Task scheduled | ☐ | |
|
||||||
|
| 2 | Wait for system to execute | ⚠️ System-controlled | ⚠️ May not execute | ☐ | |
|
||||||
|
| 3 | Check execution timing | ⚠️ Not guaranteed | ⚠️ Not guaranteed | ☐ | |
|
||||||
|
| 4 | Check time budget | ⚠️ ~30 seconds | ⚠️ Limited time | ☐ | |
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationPlugin.scheduleBackgroundFetch()` line 495
|
||||||
|
|
||||||
|
**Platform Behavior**: Conditional (see [Platform Reference §3.1.3](./01-platform-capability-reference.md#313-background-tasks-for-prefetching))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 Persistence Investigation
|
||||||
|
|
||||||
|
| Item | Expected | Actual | Code Reference | Notes |
|
||||||
|
| ---- | -------- | ------ | -------------- | ----- |
|
||||||
|
| Notification ID stored | ✅ Yes (in UNUserNotificationCenter) | ☐ | `UNNotificationRequest` | |
|
||||||
|
| Plugin-side storage | ⚠️ May not exist | ☐ | `DailyNotificationStorage?` | |
|
||||||
|
| Trigger time stored | ✅ Yes (in trigger) | ☐ | `UNCalendarNotificationTrigger` | |
|
||||||
|
| Repeat rule stored | ✅ Yes (in trigger) | ☐ | `repeats: true/false` | |
|
||||||
|
| Payload stored | ✅ Yes (in userInfo) | ☐ | `notificationContent.userInfo` | |
|
||||||
|
|
||||||
|
**Storage Location**:
|
||||||
|
- Primary: UNUserNotificationCenter (OS-managed)
|
||||||
|
- Secondary: Plugin storage (if implemented)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 Recovery Points Investigation
|
||||||
|
|
||||||
|
| Recovery Point | Expected Behavior | Actual Behavior | Code Reference | Notes |
|
||||||
|
| -------------- | ----------------- | --------------- | -------------- | ----- |
|
||||||
|
| Boot event | ✅ Notifications fire automatically | ☐ | OS handles | |
|
||||||
|
| App cold start | ⚠️ May detect missed notifications | ☐ | Check `load()` method | |
|
||||||
|
| App warm start | ⚠️ May verify pending notifications | ☐ | Check plugin initialization | |
|
||||||
|
| Background fetch | ⚠️ May reschedule | ☐ | `BGTaskScheduler` | |
|
||||||
|
| User taps notification | ✅ App launched | ☐ | Notification action | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Cross-Platform Comparison
|
||||||
|
|
||||||
|
### 3.1 Observed Behavior Summary
|
||||||
|
|
||||||
|
| Scenario | Android (Observed) | iOS (Observed) | Platform Difference |
|
||||||
|
| -------- | ------------------ | -------------- | ------------------- |
|
||||||
|
| Swipe/termination | ☐ | ☐ | Both should work |
|
||||||
|
| Reboot | ☐ | ☐ | iOS auto, Android manual |
|
||||||
|
| Force stop | ☐ | N/A | Android only |
|
||||||
|
| App code on trigger | ☐ | ☐ | Android yes, iOS no |
|
||||||
|
| Background execution | ☐ | ☐ | Android more flexible |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Findings & Gaps
|
||||||
|
|
||||||
|
### 4.1 Android Gaps
|
||||||
|
|
||||||
|
| Gap | Severity | Description | Recommendation |
|
||||||
|
| --- | -------- | ----------- | -------------- |
|
||||||
|
| Boot recovery | ☐ Critical/Major/Minor/Expected | Does plugin reschedule on boot? | Implement if missing |
|
||||||
|
| Missed alarm detection | ☐ Critical/Major/Minor/Expected | Does plugin detect missed alarms? | Implement if missing |
|
||||||
|
| Force stop recovery | ☐ Critical/Major/Minor/Expected | Does plugin recover after force stop? | Implement if missing |
|
||||||
|
| Persistence completeness | ☐ Critical/Major/Minor/Expected | Are all required fields persisted? | Verify and add if missing |
|
||||||
|
|
||||||
|
**Severity Classification**:
|
||||||
|
- **Critical**: Breaks plugin guarantee (see [Doc C §1.1](./03-plugin-requirements.md#11-guarantees-by-platform))
|
||||||
|
- **Major**: Unexpected but recoverable (plugin works but behavior differs from expected)
|
||||||
|
- **Minor**: Non-blocking deviation (cosmetic or edge case)
|
||||||
|
- **Expected**: Platform limitation (documented in [Doc A](./01-platform-capability-reference.md))
|
||||||
|
|
||||||
|
### 4.2 iOS Gaps
|
||||||
|
|
||||||
|
| Gap | Severity | Description | Recommendation |
|
||||||
|
| --- | -------- | ----------- | -------------- |
|
||||||
|
| Missed notification detection | ☐ Critical/Major/Minor/Expected | Does plugin detect missed notifications? | Implement if missing |
|
||||||
|
| Plugin-side persistence | ☐ Critical/Major/Minor/Expected | Does plugin persist state separately? | Consider if needed |
|
||||||
|
| Background task reliability | ☐ Critical/Major/Minor/Expected | Can plugin rely on BGTaskScheduler? | Document limitations |
|
||||||
|
|
||||||
|
**Severity Classification**: Same as Android (see above).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Deliverables from This Exploration
|
||||||
|
|
||||||
|
After completing this exploration, generate:
|
||||||
|
|
||||||
|
1. **Completed test results** - All checkboxes filled, actual results documented
|
||||||
|
2. **Gap analysis** - Documented limitations and gaps
|
||||||
|
3. **Annotated code pointers** - Code locations with findings
|
||||||
|
4. **Open Questions / TODOs** - Unresolved issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Platform Capability Reference](./01-platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md) - Requirements based on findings
|
||||||
|
- [Unified Alarm Directive](./000-UNIFIED-ALARM-DIRECTIVE.md) - Master coordination document
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for Explorers
|
||||||
|
|
||||||
|
* Fill in checkboxes (☐) as you complete each test
|
||||||
|
* Document actual results in "Actual Result" columns
|
||||||
|
* Add notes for any unexpected behavior
|
||||||
|
* Reference code locations when documenting findings
|
||||||
|
* Update "Findings & Gaps" section as you discover issues
|
||||||
|
* Use platform capability reference to understand expected OS behavior
|
||||||
|
* Link to Platform Reference sections instead of duplicating platform facts
|
||||||
|
|
||||||
1047
docs/alarms/03-plugin-requirements.md
Normal file
1047
docs/alarms/03-plugin-requirements.md
Normal file
File diff suppressed because it is too large
Load Diff
319
docs/alarms/ACTIVATION-GUIDE.md
Normal file
319
docs/alarms/ACTIVATION-GUIDE.md
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
# Activation Guide: How to Use the Alarm Directive System
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Activation Guide
|
||||||
|
**Version**: 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This guide explains how to **activate and use** the unified alarm directive system for implementation work. It provides step-by-step instructions for developers to follow the documentation workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites Check
|
||||||
|
|
||||||
|
**Before starting any implementation work**, verify these conditions are met:
|
||||||
|
|
||||||
|
### ✅ Documentation Status
|
||||||
|
|
||||||
|
Check [Unified Directive §11 - Status Matrix](./000-UNIFIED-ALARM-DIRECTIVE.md#11-status-matrix):
|
||||||
|
|
||||||
|
- [x] **Doc A** (Platform Facts) - ✅ Drafted, ✅ Cleaned, ✅ In Use
|
||||||
|
- [x] **Doc B** (Exploration) - ✅ Drafted, ✅ Cleaned, ✅ In Use (drives emulator test harness)
|
||||||
|
- [x] **Doc C** (Requirements) - ✅ Drafted, ✅ Cleaned, ✅ In Use
|
||||||
|
- [x] **Phase 1** (Cold Start) - ✅ Drafted, ✅ Cleaned, ✅ In Use (implemented in plugin v1.1.0, emulator-verified via `test-phase1.sh`)
|
||||||
|
- [x] **Phase 2** (Force Stop) - ✅ Drafted, ✅ Implemented, ☐ Emulator-tested (`test-phase2.sh` + `PHASE2-EMULATOR-TESTING.md`)
|
||||||
|
- [x] **Phase 3** (Boot Recovery) - ✅ Drafted, ✅ Implemented, ☐ Emulator-tested (`test-phase3.sh` + `PHASE3-EMULATOR-TESTING.md`)
|
||||||
|
|
||||||
|
**Status**: ✅ **All prerequisites met** – Phase 1 implementation is complete and emulator-verified; Phase 2 and Phase 3 are implemented and ready for emulator testing; ready for broader device testing and rollout.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Activation Workflow
|
||||||
|
|
||||||
|
### Step 1: Choose Your Starting Point
|
||||||
|
|
||||||
|
**For New Implementation Work**:
|
||||||
|
- Start with **Phase 1** (Cold Start Recovery) - See [Phase 1 Directive](../android-implementation-directive-phase1.md)
|
||||||
|
- This is the minimal viable recovery that unblocks other work
|
||||||
|
|
||||||
|
**For Testing/Exploration**:
|
||||||
|
- Start with **Doc B** (Exploration) - See [Plugin Behavior Exploration](./02-plugin-behavior-exploration.md)
|
||||||
|
- Fill in test scenarios as you validate current behavior
|
||||||
|
|
||||||
|
**For Understanding Requirements**:
|
||||||
|
- Start with **Doc C** (Requirements) - See [Plugin Requirements](./03-plugin-requirements.md)
|
||||||
|
- Review guarantees, limitations, and API contract
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Activation: Phase 1
|
||||||
|
|
||||||
|
### 1.1 Read the Phase Directive
|
||||||
|
|
||||||
|
**Start Here**: [Phase 1: Cold Start Recovery](../android-implementation-directive-phase1.md)
|
||||||
|
|
||||||
|
**Key Sections to Read**:
|
||||||
|
1. **Purpose** (§0) - Understand what Phase 1 implements
|
||||||
|
2. **Acceptance Criteria** (§1) - Definition of done
|
||||||
|
3. **Implementation** (§2) - Step-by-step code changes
|
||||||
|
4. **Testing Requirements** (§8) - How to validate
|
||||||
|
|
||||||
|
### 1.2 Reference Supporting Documents
|
||||||
|
|
||||||
|
**During Implementation, Keep These Open**:
|
||||||
|
|
||||||
|
1. **Doc A** - [Platform Capability Reference](./01-platform-capability-reference.md)
|
||||||
|
- Use for: Understanding OS behavior, API constraints, permissions
|
||||||
|
- Example: "Can I rely on AlarmManager to persist alarms?" → See Doc A §2.1.1
|
||||||
|
|
||||||
|
2. **Doc C** - [Plugin Requirements](./03-plugin-requirements.md)
|
||||||
|
- Use for: Understanding what the plugin MUST guarantee
|
||||||
|
- Example: "What should happen on cold start?" → See Doc C §3.1.2
|
||||||
|
|
||||||
|
3. **Doc B** - [Plugin Behavior Exploration](./02-plugin-behavior-exploration.md)
|
||||||
|
- Use for: Test scenarios to validate your implementation
|
||||||
|
- Example: "How do I test cold start recovery?" → See Doc B Test 4
|
||||||
|
|
||||||
|
### 1.3 Follow the Implementation Steps
|
||||||
|
|
||||||
|
**Phase 1 Implementation Checklist** (from Phase 1 directive):
|
||||||
|
|
||||||
|
- [ ] Create `ReactivationManager.kt` file
|
||||||
|
- [ ] Implement `detectMissedNotifications()` method
|
||||||
|
- [ ] Implement `markMissedNotifications()` method
|
||||||
|
- [ ] Implement `verifyAndRescheduleFutureAlarms()` method
|
||||||
|
- [ ] Integrate into `DailyNotificationPlugin.load()`
|
||||||
|
- [ ] Add logging and error handling
|
||||||
|
- [ ] Write unit tests
|
||||||
|
- [ ] Test on physical device
|
||||||
|
|
||||||
|
**Reference**: See [Phase 1 §2 - Implementation](../android-implementation-directive-phase1.md#2-implementation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Activation: Doc B
|
||||||
|
|
||||||
|
### 2.1 Execute Test Scenarios
|
||||||
|
|
||||||
|
**Start Here**: [Plugin Behavior Exploration](./02-plugin-behavior-exploration.md)
|
||||||
|
|
||||||
|
**Workflow**:
|
||||||
|
1. Choose a test scenario (e.g., "Test 4: Device Reboot")
|
||||||
|
2. Follow the **Steps** column exactly
|
||||||
|
3. Fill in **Actual Result** column with observed behavior
|
||||||
|
4. Mark **Result** column (Pass/Fail)
|
||||||
|
5. Add **Notes** for any unexpected behavior
|
||||||
|
|
||||||
|
### 2.2 Update Test Results
|
||||||
|
|
||||||
|
**As You Test**:
|
||||||
|
- Update checkboxes (☐ → ✅) when tests pass
|
||||||
|
- Document actual vs expected differences
|
||||||
|
- Add findings to "Findings & Gaps" section (§4)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```markdown
|
||||||
|
| Step | Action | Expected | Actual Result | Notes | Result |
|
||||||
|
| ---- | ------ | -------- | ------------- | ----- | ------ |
|
||||||
|
| 5 | Launch app | Plugin detects missed alarm | ✅ Missed alarm detected | Logs show "DNP-REACTIVATION: Detected 1 missed alarm" | ✅ Pass |
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Maintenance During Work
|
||||||
|
|
||||||
|
### 3.1 Update Status Matrix
|
||||||
|
|
||||||
|
**When You Complete Work**:
|
||||||
|
|
||||||
|
1. Open [Unified Directive §11](./000-UNIFIED-ALARM-DIRECTIVE.md#11-status-matrix)
|
||||||
|
2. Update the relevant row:
|
||||||
|
- Mark "In Use?" = ✅ when implementation is deployed
|
||||||
|
- Update "Notes" with completion status
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```markdown
|
||||||
|
| P1 | `../android-implementation-directive-phase1.md` | Impl – Cold start | ✅ | ✅ | ✅ | **Implemented and deployed** - See commit abc123 |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Update Doc B with Test Results
|
||||||
|
|
||||||
|
**After Testing**:
|
||||||
|
- Fill in actual results in test matrices
|
||||||
|
- Document any gaps or unexpected behavior
|
||||||
|
- Update severity classifications if issues found
|
||||||
|
|
||||||
|
### 3.3 Follow Change Control Rules
|
||||||
|
|
||||||
|
**When Modifying Docs A, B, or C**:
|
||||||
|
|
||||||
|
1. **Update version header** in the document
|
||||||
|
2. **Update status matrix** (Section 11) in unified directive
|
||||||
|
3. **Use commit message tag**: `[ALARM-DOCS]` prefix
|
||||||
|
4. **Notify in CHANGELOG** if JS/TS-visible behavior changes
|
||||||
|
|
||||||
|
**Reference**: See [Unified Directive §10 - Change Control](./000-UNIFIED-ALARM-DIRECTIVE.md#10-change-control-rules)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 1. Read Phase Directive (P1/P2/P3) │
|
||||||
|
│ Understand acceptance criteria │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 2. Reference Doc A (Platform Facts) │
|
||||||
|
│ Understand OS constraints │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 3. Reference Doc C (Requirements) │
|
||||||
|
│ Understand plugin guarantees │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 4. Implement Code (Phase Directive) │
|
||||||
|
│ Follow step-by-step instructions │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 5. Test (Doc B Scenarios) │
|
||||||
|
│ Execute test matrices │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 6. Update Documentation │
|
||||||
|
│ - Status matrix │
|
||||||
|
│ - Test results (Doc B) │
|
||||||
|
│ - Version numbers │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Activation Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Starting Phase 1 Implementation
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. ✅ Verify prerequisites (all docs exist - **DONE**)
|
||||||
|
2. Read [Phase 1 Directive](../android-implementation-directive-phase1.md) §1 (Acceptance Criteria)
|
||||||
|
3. Read [Doc C §3.1.2](./03-plugin-requirements.md#312-app-cold-start) (Cold Start Requirements)
|
||||||
|
4. Read [Doc A §2.1.4](./01-platform-capability-reference.md#214-alarms-can-be-restored-after-app-restart) (Platform Capability)
|
||||||
|
5. Follow [Phase 1 §2](../android-implementation-directive-phase1.md#2-implementation) (Implementation Steps)
|
||||||
|
6. Test using [Doc B Test 4](./02-plugin-behavior-exploration.md#test-4-device-reboot) (Cold Start Scenario)
|
||||||
|
7. Update status matrix when complete
|
||||||
|
|
||||||
|
### Scenario 2: Testing Current Behavior
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Open [Doc B](./02-plugin-behavior-exploration.md)
|
||||||
|
2. Choose a test scenario (e.g., "Test 2: Swipe from Recents")
|
||||||
|
3. Follow the **Steps** column
|
||||||
|
4. Fill in **Actual Result** column
|
||||||
|
5. Compare with **Expected (OS)** and **Expected (Plugin)** columns
|
||||||
|
6. Document findings in **Notes** column
|
||||||
|
7. Update "Findings & Gaps" section if issues found
|
||||||
|
|
||||||
|
### Scenario 3: Understanding a Requirement
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Open [Doc C](./03-plugin-requirements.md)
|
||||||
|
2. Find the relevant section (e.g., "Missed Alarm Handling" §4)
|
||||||
|
3. Read the requirement and acceptance criteria
|
||||||
|
4. Follow cross-references to:
|
||||||
|
- **Doc A** for platform constraints
|
||||||
|
- **Doc B** for test scenarios
|
||||||
|
- **Phase docs** for implementation details
|
||||||
|
|
||||||
|
### Scenario 4: Adding iOS Support
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. ✅ Verify iOS parity milestone conditions (see [Unified Directive §9](./000-UNIFIED-ALARM-DIRECTIVE.md#9-next-steps))
|
||||||
|
2. Ensure Doc A has iOS matrix complete
|
||||||
|
3. Ensure Doc C has iOS guarantees defined
|
||||||
|
4. Create iOS implementation following Android phase patterns
|
||||||
|
5. Test using Doc B iOS scenarios
|
||||||
|
6. Update status matrix
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocking Rules
|
||||||
|
|
||||||
|
**⚠️ DO NOT PROCEED** if:
|
||||||
|
|
||||||
|
1. **Prerequisites not met** - See [Unified Directive §12](./000-UNIFIED-ALARM-DIRECTIVE.md#12-single-instruction-for-team)
|
||||||
|
- Doc A, B, C must exist
|
||||||
|
- Status matrix must be updated
|
||||||
|
- Deprecated files must be marked
|
||||||
|
|
||||||
|
2. **iOS work without parity milestone** - See [Unified Directive §9](./000-UNIFIED-ALARM-DIRECTIVE.md#9-next-steps)
|
||||||
|
- Doc A must have iOS matrix
|
||||||
|
- Doc C must define iOS guarantees
|
||||||
|
- Phase docs must not assume Android-only
|
||||||
|
|
||||||
|
3. **Phase 2/3 without Phase 1** - See Phase directives
|
||||||
|
- Phase 2 requires Phase 1 complete
|
||||||
|
- Phase 3 requires Phase 1 & 2 complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Document Roles
|
||||||
|
|
||||||
|
| Doc | Purpose | When to Use |
|
||||||
|
|-----|---------|-------------|
|
||||||
|
| **Unified Directive** | Master coordination | Understanding system structure, change control |
|
||||||
|
| **Doc A** | Platform facts | Understanding OS behavior, API constraints |
|
||||||
|
| **Doc B** | Test scenarios | Testing, exploration, validation |
|
||||||
|
| **Doc C** | Requirements | Understanding guarantees, API contract |
|
||||||
|
| **Phase 1-3** | Implementation | Writing code, step-by-step instructions |
|
||||||
|
|
||||||
|
### Key Sections
|
||||||
|
|
||||||
|
- **Status Matrix**: [Unified Directive §11](./000-UNIFIED-ALARM-DIRECTIVE.md#11-status-matrix)
|
||||||
|
- **Change Control**: [Unified Directive §10](./000-UNIFIED-ALARM-DIRECTIVE.md#10-change-control-rules)
|
||||||
|
- **Phase 1 Start**: [Phase 1 Directive](../android-implementation-directive-phase1.md)
|
||||||
|
- **Test Scenarios**: [Doc B Test Matrices](./02-plugin-behavior-exploration.md#12-behavior-testing-matrix)
|
||||||
|
- **Requirements**: [Doc C Guarantees](./03-plugin-requirements.md#1-plugin-behavior-guarantees--limitations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
**You're Ready To**:
|
||||||
|
|
||||||
|
1. ✅ **Start Phase 1 Implementation** - All prerequisites met
|
||||||
|
2. ✅ **Begin Testing** - Doc B scenarios ready
|
||||||
|
3. ✅ **Reference Documentation** - All docs complete and cross-referenced
|
||||||
|
|
||||||
|
**Recommended First Action**:
|
||||||
|
- Read [Phase 1: Cold Start Recovery](../android-implementation-directive-phase1.md) §1 (Acceptance Criteria)
|
||||||
|
- Then proceed to §2 (Implementation) when ready to code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Unified Alarm Directive](./000-UNIFIED-ALARM-DIRECTIVE.md) - Master coordination document
|
||||||
|
- [Phase 1: Cold Start Recovery](../android-implementation-directive-phase1.md) - Start here for implementation
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md) - What the plugin must guarantee
|
||||||
|
- [Platform Capability Reference](./01-platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Plugin Behavior Exploration](./02-plugin-behavior-exploration.md) - Test scenarios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Ready for activation
|
||||||
|
**Last Updated**: November 2025
|
||||||
|
|
||||||
686
docs/alarms/PHASE1-EMULATOR-TESTING.md
Normal file
686
docs/alarms/PHASE1-EMULATOR-TESTING.md
Normal file
@@ -0,0 +1,686 @@
|
|||||||
|
# Phase 1 Emulator Testing Guide
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Testing Guide
|
||||||
|
**Version**: 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This guide provides step-by-step instructions for testing Phase 1 (Cold Start Recovery) implementation on an Android emulator. All Phase 1 tests can be run entirely on an emulator using ADB commands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Latest Known Good Run (Emulator)
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
|
||||||
|
- Device: Android Emulator – Pixel 8 API 34
|
||||||
|
- App ID: `com.timesafari.dailynotification`
|
||||||
|
- Build: Debug APK from `test-apps/android-test-app`
|
||||||
|
- Script: `./test-phase1.sh`
|
||||||
|
- Date: 27 November 2025
|
||||||
|
|
||||||
|
**Observed Results**
|
||||||
|
|
||||||
|
- ✅ TEST 1: Cold Start Missed Detection
|
||||||
|
- Logs show:
|
||||||
|
- `Marked missed notification: daily_<id>`
|
||||||
|
- `Cold start recovery complete: missed=1, rescheduled=0, verified=0, errors=0`
|
||||||
|
- "Stored notification content in database" present in logs
|
||||||
|
- Alarm present in `dumpsys alarm` before kill
|
||||||
|
|
||||||
|
- ✅ TEST 2: Future Alarm Verification / Rescheduling
|
||||||
|
- Logs show:
|
||||||
|
- `Rescheduled alarm: daily_<id> for <time>`
|
||||||
|
- `Rescheduled missing alarm: daily_<id> at <time>`
|
||||||
|
- `Cold start recovery complete: missed=1, rescheduled=1, verified=0, errors=0`
|
||||||
|
- Script output:
|
||||||
|
- `✅ TEST 2 PASSED: Missing future alarms were detected and rescheduled (rescheduled=1)!`
|
||||||
|
|
||||||
|
- ✅ TEST 3: Recovery Timeout
|
||||||
|
- Timeout protection confirmed at **2 seconds**
|
||||||
|
- No blocking of app startup
|
||||||
|
|
||||||
|
- ✅ TEST 4: Invalid Data Handling
|
||||||
|
- Confirmed in code review:
|
||||||
|
- Reactivation code safely skips invalid IDs
|
||||||
|
- Errors are logged but do not crash recovery
|
||||||
|
|
||||||
|
**Conclusion:**
|
||||||
|
Phase 1 cold-start recovery behavior is **successfully verified on emulator** using `test-phase1.sh`. This run is the reference baseline for future regressions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Required Software
|
||||||
|
|
||||||
|
- **Android SDK** with command line tools
|
||||||
|
- **Android Emulator** (`emulator` command)
|
||||||
|
- **ADB** (Android Debug Bridge)
|
||||||
|
- **Gradle** (via Gradle Wrapper)
|
||||||
|
- **Java** (JDK 11+)
|
||||||
|
|
||||||
|
### Emulator Setup
|
||||||
|
|
||||||
|
1. **List available emulators**:
|
||||||
|
```bash
|
||||||
|
emulator -list-avds
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start emulator** (choose one):
|
||||||
|
```bash
|
||||||
|
# Start in background (recommended)
|
||||||
|
emulator -avd Pixel8_API34 -no-snapshot-load &
|
||||||
|
|
||||||
|
# Or start in foreground
|
||||||
|
emulator -avd Pixel8_API34
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Wait for emulator to boot**:
|
||||||
|
```bash
|
||||||
|
adb wait-for-device
|
||||||
|
adb shell getprop sys.boot_completed
|
||||||
|
# Wait until returns "1"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify emulator is connected**:
|
||||||
|
```bash
|
||||||
|
adb devices
|
||||||
|
# Should show: emulator-5554 device
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build and Install Test App
|
||||||
|
|
||||||
|
### Option 1: Android Test App (Simpler)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to test app directory
|
||||||
|
cd test-apps/android-test-app
|
||||||
|
|
||||||
|
# Build debug APK (builds plugin automatically)
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# Install on emulator
|
||||||
|
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
adb shell pm list packages | grep timesafari
|
||||||
|
# Should show: package:com.timesafari.dailynotification
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Vue Test App (More Features)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to Vue test app
|
||||||
|
cd test-apps/daily-notification-test
|
||||||
|
|
||||||
|
# Build Vue app
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Sync with Capacitor
|
||||||
|
npx cap sync android
|
||||||
|
|
||||||
|
# Build Android APK
|
||||||
|
cd android
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# Install on emulator
|
||||||
|
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Setup
|
||||||
|
|
||||||
|
### 1. Clear Logs Before Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear logcat buffer
|
||||||
|
adb logcat -c
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Monitor Logs in Separate Terminal
|
||||||
|
|
||||||
|
**Keep this running in a separate terminal window**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Monitor all plugin-related logs
|
||||||
|
adb logcat | grep -E "DNP-REACTIVATION|DNP-PLUGIN|DNP-NOTIFY|DailyNotification"
|
||||||
|
|
||||||
|
# Or monitor just recovery logs
|
||||||
|
adb logcat -s DNP-REACTIVATION
|
||||||
|
|
||||||
|
# Or save logs to file
|
||||||
|
adb logcat -s DNP-REACTIVATION > recovery_test.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Launch App Once (Initial Setup)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch app to initialize database
|
||||||
|
adb shell am start -n com.timesafari.dailynotification/.MainActivity
|
||||||
|
|
||||||
|
# Wait a few seconds for initialization
|
||||||
|
sleep 3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 1: Cold Start Missed Detection
|
||||||
|
|
||||||
|
**Purpose**: Verify missed notifications are detected and marked.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clear logs
|
||||||
|
adb logcat -c
|
||||||
|
|
||||||
|
# 2. Launch app
|
||||||
|
adb shell am start -n com.timesafari.dailynotification/.MainActivity
|
||||||
|
|
||||||
|
# 3. Schedule notification for 2 minutes in future
|
||||||
|
# (Use app UI or API - see "Scheduling Notifications" below)
|
||||||
|
|
||||||
|
# 4. Wait for app to schedule (check logs)
|
||||||
|
adb logcat -d | grep "DN|SCHEDULE\|DN|ALARM"
|
||||||
|
# Should show alarm scheduled
|
||||||
|
|
||||||
|
# 5. Verify alarm is scheduled
|
||||||
|
adb shell dumpsys alarm | grep -i timesafari
|
||||||
|
# Should show scheduled alarm
|
||||||
|
|
||||||
|
# 6. Kill app process (simulates OS kill, NOT force stop)
|
||||||
|
adb shell am kill com.timesafari.dailynotification
|
||||||
|
|
||||||
|
# 7. Verify app is killed
|
||||||
|
adb shell ps | grep timesafari
|
||||||
|
# Should return nothing
|
||||||
|
|
||||||
|
# 8. Wait 5 minutes (past scheduled time)
|
||||||
|
# Use: sleep 300 (or wait manually)
|
||||||
|
# Or: Set system time forward (see "Time Manipulation" below)
|
||||||
|
|
||||||
|
# 9. Launch app (cold start)
|
||||||
|
adb shell am start -n com.timesafari.dailynotification/.MainActivity
|
||||||
|
|
||||||
|
# 10. Check recovery logs immediately
|
||||||
|
adb logcat -d | grep DNP-REACTIVATION
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Log Output
|
||||||
|
|
||||||
|
```
|
||||||
|
DNP-REACTIVATION: Starting app launch recovery (Phase 1: cold start only)
|
||||||
|
DNP-REACTIVATION: Cold start recovery: checking for missed notifications
|
||||||
|
DNP-REACTIVATION: Marked missed notification: <id>
|
||||||
|
DNP-REACTIVATION: Cold start recovery complete: missed=1, rescheduled=0, verified=0, errors=0
|
||||||
|
DNP-REACTIVATION: App launch recovery completed: missed=1, rescheduled=0, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check database (requires root or debug build)
|
||||||
|
adb shell run-as com.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db \
|
||||||
|
"SELECT id, delivery_status, scheduled_time FROM notification_content WHERE delivery_status = 'missed';"
|
||||||
|
|
||||||
|
# Or check history table
|
||||||
|
adb shell run-as com.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db \
|
||||||
|
"SELECT * FROM history WHERE kind = 'recovery' ORDER BY occurredAt DESC LIMIT 1;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pass Criteria
|
||||||
|
|
||||||
|
- ✅ Log shows "Cold start recovery: checking for missed notifications"
|
||||||
|
- ✅ Log shows "Marked missed notification: <id>"
|
||||||
|
- ✅ Database shows `delivery_status = 'missed'`
|
||||||
|
- ✅ History table has recovery entry
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 2: Future Alarm Rescheduling
|
||||||
|
|
||||||
|
**Purpose**: Verify missing future alarms are rescheduled.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clear logs
|
||||||
|
adb logcat -c
|
||||||
|
|
||||||
|
# 2. Launch app
|
||||||
|
adb shell am start -n com.timesafari.dailynotification/.MainActivity
|
||||||
|
|
||||||
|
# 3. Schedule notification for 10 minutes in future
|
||||||
|
# (Use app UI or API)
|
||||||
|
|
||||||
|
# 4. Verify alarm is scheduled
|
||||||
|
adb shell dumpsys alarm | grep -i timesafari
|
||||||
|
# Note the request code or trigger time
|
||||||
|
|
||||||
|
# 5. Manually cancel alarm (simulate missing alarm)
|
||||||
|
# Find the alarm request code from dumpsys output
|
||||||
|
# Then cancel using PendingIntent (requires root or app code)
|
||||||
|
# OR: Use app UI to cancel if available
|
||||||
|
|
||||||
|
# Alternative: Use app code to cancel
|
||||||
|
# (This test may require app modification to add cancel button)
|
||||||
|
|
||||||
|
# 6. Verify alarm is cancelled
|
||||||
|
adb shell dumpsys alarm | grep -i timesafari
|
||||||
|
# Should show no alarms (or fewer alarms)
|
||||||
|
|
||||||
|
# 7. Launch app (triggers recovery)
|
||||||
|
adb shell am start -n com.timesafari.dailynotification/.MainActivity
|
||||||
|
|
||||||
|
# 8. Check recovery logs
|
||||||
|
adb logcat -d | grep DNP-REACTIVATION
|
||||||
|
|
||||||
|
# 9. Verify alarm is rescheduled
|
||||||
|
adb shell dumpsys alarm | grep -i timesafari
|
||||||
|
# Should show rescheduled alarm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Log Output
|
||||||
|
|
||||||
|
```
|
||||||
|
DNP-REACTIVATION: Starting app launch recovery (Phase 1: cold start only)
|
||||||
|
DNP-REACTIVATION: Cold start recovery: checking for missed notifications
|
||||||
|
DNP-REACTIVATION: Rescheduled missing alarm: <id> at <timestamp>
|
||||||
|
DNP-REACTIVATION: Cold start recovery complete: missed=0, rescheduled=1, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pass Criteria
|
||||||
|
|
||||||
|
- ✅ Log shows "Rescheduled missing alarm: <id>"
|
||||||
|
- ✅ AlarmManager shows rescheduled alarm
|
||||||
|
- ✅ No duplicate alarms created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 3: Recovery Timeout
|
||||||
|
|
||||||
|
**Purpose**: Verify recovery times out gracefully.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clear logs
|
||||||
|
adb logcat -c
|
||||||
|
|
||||||
|
# 2. Create large number of schedules (100+)
|
||||||
|
# This requires app modification or database manipulation
|
||||||
|
# See "Database Manipulation" section below
|
||||||
|
|
||||||
|
# 3. Launch app
|
||||||
|
adb shell am start -n com.timesafari.dailynotification/.MainActivity
|
||||||
|
|
||||||
|
# 4. Check logs immediately
|
||||||
|
adb logcat -d | grep DNP-REACTIVATION
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Behavior
|
||||||
|
|
||||||
|
- ✅ Recovery completes within 2 seconds OR times out
|
||||||
|
- ✅ App doesn't crash
|
||||||
|
- ✅ Partial recovery logged if timeout occurs
|
||||||
|
|
||||||
|
### Pass Criteria
|
||||||
|
|
||||||
|
- ✅ Recovery doesn't block app launch
|
||||||
|
- ✅ No app crash
|
||||||
|
- ✅ Timeout logged if occurs
|
||||||
|
|
||||||
|
**Note**: This test may be difficult to execute without creating many schedules. Consider testing with smaller numbers first (10, 50 schedules) to verify behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 4: Invalid Data Handling
|
||||||
|
|
||||||
|
**Purpose**: Verify invalid data doesn't crash recovery.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clear logs
|
||||||
|
adb logcat -c
|
||||||
|
|
||||||
|
# 2. Manually insert invalid notification (empty ID) into database
|
||||||
|
# See "Database Manipulation" section below
|
||||||
|
|
||||||
|
# 3. Launch app
|
||||||
|
adb shell am start -n com.timesafari.dailynotification/.MainActivity
|
||||||
|
|
||||||
|
# 4. Check logs
|
||||||
|
adb logcat -d | grep DNP-REACTIVATION
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Log Output
|
||||||
|
|
||||||
|
```
|
||||||
|
DNP-REACTIVATION: Starting app launch recovery (Phase 1: cold start only)
|
||||||
|
DNP-REACTIVATION: Cold start recovery: checking for missed notifications
|
||||||
|
DNP-REACTIVATION: Skipping invalid notification: empty ID
|
||||||
|
DNP-REACTIVATION: Cold start recovery complete: missed=0, rescheduled=0, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pass Criteria
|
||||||
|
|
||||||
|
- ✅ Invalid notification skipped
|
||||||
|
- ✅ Warning logged
|
||||||
|
- ✅ Recovery continues normally
|
||||||
|
- ✅ App doesn't crash
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Helper Scripts and Commands
|
||||||
|
|
||||||
|
### Scheduling Notifications
|
||||||
|
|
||||||
|
**Option 1: Use App UI**
|
||||||
|
- Launch app
|
||||||
|
- Use "Schedule Notification" button
|
||||||
|
- Set time to 2-5 minutes in future
|
||||||
|
|
||||||
|
**Option 2: Use Capacitor API (if test app has console)**
|
||||||
|
```javascript
|
||||||
|
// In browser console or test app
|
||||||
|
const { DailyNotification } = Plugins.DailyNotification;
|
||||||
|
await DailyNotification.scheduleDailyNotification({
|
||||||
|
schedule: "*/2 * * * *", // Every 2 minutes
|
||||||
|
title: "Test Notification",
|
||||||
|
body: "Testing Phase 1 recovery"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 3: Direct Database Insert (Advanced)**
|
||||||
|
```bash
|
||||||
|
# See "Database Manipulation" section
|
||||||
|
```
|
||||||
|
|
||||||
|
### Time Manipulation (Emulator)
|
||||||
|
|
||||||
|
**Fast-forward system time** (for testing without waiting):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get current time
|
||||||
|
adb shell date +%s
|
||||||
|
|
||||||
|
# Set time forward (e.g., 5 minutes)
|
||||||
|
adb shell date -s @$(($(adb shell date +%s) + 300))
|
||||||
|
|
||||||
|
# Or set specific time
|
||||||
|
adb shell date -s "2025-11-15 14:30:00"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Some emulators may not support time changes. Test with actual waiting if time manipulation doesn't work.
|
||||||
|
|
||||||
|
### Database Manipulation
|
||||||
|
|
||||||
|
**Access database** (requires root or debug build):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if app is debuggable
|
||||||
|
adb shell dumpsys package com.timesafari.dailynotification | grep debuggable
|
||||||
|
|
||||||
|
# Access database
|
||||||
|
adb shell run-as com.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db
|
||||||
|
|
||||||
|
# Example: Insert test notification
|
||||||
|
sqlite> INSERT INTO notification_content (
|
||||||
|
id, plugin_version, title, body, scheduled_time,
|
||||||
|
delivery_status, delivery_attempts, last_delivery_attempt,
|
||||||
|
created_at, updated_at, ttl_seconds, priority,
|
||||||
|
vibration_enabled, sound_enabled
|
||||||
|
) VALUES (
|
||||||
|
'test_notification_1', '1.1.0', 'Test', 'Test body',
|
||||||
|
$(($(date +%s) * 1000 - 300000)), -- 5 minutes ago
|
||||||
|
'pending', 0, 0,
|
||||||
|
$(date +%s) * 1000, $(date +%s) * 1000,
|
||||||
|
604800, 0, 1, 1
|
||||||
|
);
|
||||||
|
|
||||||
|
# Example: Insert invalid notification (empty ID)
|
||||||
|
sqlite> INSERT INTO notification_content (
|
||||||
|
id, plugin_version, title, body, scheduled_time,
|
||||||
|
delivery_status, delivery_attempts, last_delivery_attempt,
|
||||||
|
created_at, updated_at, ttl_seconds, priority,
|
||||||
|
vibration_enabled, sound_enabled
|
||||||
|
) VALUES (
|
||||||
|
'', '1.1.0', 'Invalid', 'Invalid body',
|
||||||
|
$(($(date +%s) * 1000 - 300000)),
|
||||||
|
'pending', 0, 0,
|
||||||
|
$(date +%s) * 1000, $(date +%s) * 1000,
|
||||||
|
604800, 0, 1, 1
|
||||||
|
);
|
||||||
|
|
||||||
|
# Example: Create many schedules (for timeout test)
|
||||||
|
sqlite> .read create_many_schedules.sql
|
||||||
|
# (Create SQL file with 100+ INSERT statements)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Filtering
|
||||||
|
|
||||||
|
**Useful log filters**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Recovery-specific logs
|
||||||
|
adb logcat -s DNP-REACTIVATION
|
||||||
|
|
||||||
|
# All plugin logs
|
||||||
|
adb logcat | grep -E "DNP-|DailyNotification"
|
||||||
|
|
||||||
|
# Recovery + scheduling logs
|
||||||
|
adb logcat | grep -E "DNP-REACTIVATION|DN|SCHEDULE"
|
||||||
|
|
||||||
|
# Save logs to file
|
||||||
|
adb logcat -d > phase1_test_$(date +%Y%m%d_%H%M%S).log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Test Sequence
|
||||||
|
|
||||||
|
**Run all tests in sequence**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Phase 1 Complete Test Sequence
|
||||||
|
|
||||||
|
PACKAGE="com.timesafari.dailynotification"
|
||||||
|
ACTIVITY="${PACKAGE}/.MainActivity"
|
||||||
|
|
||||||
|
echo "=== Phase 1 Testing on Emulator ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
echo "1. Setting up emulator..."
|
||||||
|
adb wait-for-device
|
||||||
|
adb logcat -c
|
||||||
|
|
||||||
|
# Test 1: Cold Start Missed Detection
|
||||||
|
echo ""
|
||||||
|
echo "=== Test 1: Cold Start Missed Detection ==="
|
||||||
|
echo "1. Launch app and schedule notification for 2 minutes"
|
||||||
|
adb shell am start -n $ACTIVITY
|
||||||
|
echo " (Use app UI to schedule notification)"
|
||||||
|
read -p "Press Enter after scheduling notification..."
|
||||||
|
|
||||||
|
echo "2. Killing app process..."
|
||||||
|
adb shell am kill $PACKAGE
|
||||||
|
|
||||||
|
echo "3. Waiting 5 minutes (or set time forward)..."
|
||||||
|
echo " (You can set time forward: adb shell date -s ...)"
|
||||||
|
read -p "Press Enter after waiting 5 minutes..."
|
||||||
|
|
||||||
|
echo "4. Launching app (cold start)..."
|
||||||
|
adb shell am start -n $ACTIVITY
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "5. Checking recovery logs..."
|
||||||
|
adb logcat -d | grep DNP-REACTIVATION
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Test 1 Complete ==="
|
||||||
|
read -p "Press Enter to continue to Test 2..."
|
||||||
|
|
||||||
|
# Test 2: Future Alarm Rescheduling
|
||||||
|
echo ""
|
||||||
|
echo "=== Test 2: Future Alarm Rescheduling ==="
|
||||||
|
echo "1. Schedule notification for 10 minutes"
|
||||||
|
adb shell am start -n $ACTIVITY
|
||||||
|
echo " (Use app UI to schedule notification)"
|
||||||
|
read -p "Press Enter after scheduling..."
|
||||||
|
|
||||||
|
echo "2. Verify alarm scheduled..."
|
||||||
|
adb shell dumpsys alarm | grep -i timesafari
|
||||||
|
|
||||||
|
echo "3. Cancel alarm (use app UI or see Database Manipulation)"
|
||||||
|
read -p "Press Enter after cancelling alarm..."
|
||||||
|
|
||||||
|
echo "4. Launch app (triggers recovery)..."
|
||||||
|
adb shell am start -n $ACTIVITY
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "5. Check recovery logs..."
|
||||||
|
adb logcat -d | grep DNP-REACTIVATION
|
||||||
|
|
||||||
|
echo "6. Verify alarm rescheduled..."
|
||||||
|
adb shell dumpsys alarm | grep -i timesafari
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Test 2 Complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "=== All Tests Complete ==="
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Emulator Issues
|
||||||
|
|
||||||
|
**Emulator won't start**:
|
||||||
|
```bash
|
||||||
|
# Check available AVDs
|
||||||
|
emulator -list-avds
|
||||||
|
|
||||||
|
# Kill existing emulator
|
||||||
|
pkill -f emulator
|
||||||
|
|
||||||
|
# Start with verbose logging
|
||||||
|
emulator -avd Pixel8_API34 -verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
**Emulator is slow**:
|
||||||
|
```bash
|
||||||
|
# Use hardware acceleration
|
||||||
|
emulator -avd Pixel8_API34 -accel on -gpu host
|
||||||
|
|
||||||
|
# Allocate more RAM
|
||||||
|
emulator -avd Pixel8_API34 -memory 4096
|
||||||
|
```
|
||||||
|
|
||||||
|
### ADB Issues
|
||||||
|
|
||||||
|
**ADB not detecting emulator**:
|
||||||
|
```bash
|
||||||
|
# Restart ADB server
|
||||||
|
adb kill-server
|
||||||
|
adb start-server
|
||||||
|
|
||||||
|
# Check devices
|
||||||
|
adb devices
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permission denied for database access**:
|
||||||
|
```bash
|
||||||
|
# Check if app is debuggable
|
||||||
|
adb shell dumpsys package com.timesafari.dailynotification | grep debuggable
|
||||||
|
|
||||||
|
# If not debuggable, rebuild with debug signing
|
||||||
|
cd test-apps/android-test-app
|
||||||
|
./gradlew assembleDebug
|
||||||
|
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### App Issues
|
||||||
|
|
||||||
|
**App won't launch**:
|
||||||
|
```bash
|
||||||
|
# Check if app is installed
|
||||||
|
adb shell pm list packages | grep timesafari
|
||||||
|
|
||||||
|
# Uninstall and reinstall
|
||||||
|
adb uninstall com.timesafari.dailynotification
|
||||||
|
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
**No logs appearing**:
|
||||||
|
```bash
|
||||||
|
# Check logcat buffer size
|
||||||
|
adb logcat -G 10M
|
||||||
|
|
||||||
|
# Clear and monitor
|
||||||
|
adb logcat -c
|
||||||
|
adb logcat -s DNP-REACTIVATION
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Test Results Summary
|
||||||
|
|
||||||
|
| Test | Expected Outcome | Verification Method |
|
||||||
|
|------|------------------|---------------------|
|
||||||
|
| **Test 1** | Missed notification detected and marked | Logs + Database query |
|
||||||
|
| **Test 2** | Missing alarm rescheduled | Logs + AlarmManager check |
|
||||||
|
| **Test 3** | Recovery times out gracefully | Logs (timeout message) |
|
||||||
|
| **Test 4** | Invalid data skipped | Logs (warning message) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Essential Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start emulator
|
||||||
|
emulator -avd Pixel8_API34 &
|
||||||
|
|
||||||
|
# Build and install
|
||||||
|
cd test-apps/android-test-app
|
||||||
|
./gradlew assembleDebug
|
||||||
|
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
|
# Launch app
|
||||||
|
adb shell am start -n com.timesafari.dailynotification/.MainActivity
|
||||||
|
|
||||||
|
# Kill app
|
||||||
|
adb shell am kill com.timesafari.dailynotification
|
||||||
|
|
||||||
|
# Monitor logs
|
||||||
|
adb logcat -s DNP-REACTIVATION
|
||||||
|
|
||||||
|
# Check alarms
|
||||||
|
adb shell dumpsys alarm | grep -i timesafari
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Phase 1 Directive](../android-implementation-directive-phase1.md) - Implementation details
|
||||||
|
- [Phase 1 Verification](./PHASE1-VERIFICATION.md) - Verification report
|
||||||
|
- [Activation Guide](./ACTIVATION-GUIDE.md) - How to use directives
|
||||||
|
- [Standalone Emulator Guide](../standalone-emulator-guide.md) - Emulator setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Emulator-verified (test-phase1.sh)
|
||||||
|
**Last Updated**: 27 November 2025
|
||||||
|
|
||||||
259
docs/alarms/PHASE1-VERIFICATION.md
Normal file
259
docs/alarms/PHASE1-VERIFICATION.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# Phase 1 Verification Report
|
||||||
|
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Verification Complete
|
||||||
|
**Phase**: Phase 1 - Cold Start Recovery
|
||||||
|
|
||||||
|
## Verification Summary
|
||||||
|
|
||||||
|
**Overall Status**: ✅ **VERIFIED** – Phase 1 is complete, aligned, implemented in plugin v1.1.0, and emulator-tested via `test-phase1.sh` on a Pixel 8 API 34 emulator.
|
||||||
|
|
||||||
|
**Verification Method**:
|
||||||
|
- Automated emulator run using `PHASE1-EMULATOR-TESTING.md` + `test-phase1.sh`
|
||||||
|
- All four Phase 1 tests (missed detection, future alarm verification/rescheduling, timeout, invalid data handling) passed with `errors=0`.
|
||||||
|
|
||||||
|
**Issues Found**: 2 minor documentation improvements recommended (resolved)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Alignment with Doc C (Requirements)
|
||||||
|
|
||||||
|
### ✅ Required Actions Check
|
||||||
|
|
||||||
|
**Doc C §3.1.2 - App Cold Start** requires:
|
||||||
|
|
||||||
|
| Required Action | Phase 1 Implementation | Status |
|
||||||
|
|----------------|------------------------|--------|
|
||||||
|
| 1. Load all enabled alarms from persistent storage | ✅ `db.scheduleDao().getEnabled()` | ✅ Complete |
|
||||||
|
| 2. Verify active alarms match stored alarms | ✅ `NotifyReceiver.isAlarmScheduled()` check | ✅ Complete |
|
||||||
|
| 3. Detect missed alarms (trigger_time < now) | ✅ `getNotificationsReadyForDelivery(currentTime)` | ✅ Complete |
|
||||||
|
| 4. Reschedule future alarms | ✅ `rescheduleAlarm()` method | ✅ Complete |
|
||||||
|
| 5. Generate missed alarm events/notifications | ⚠️ Deferred to Phase 2 | ✅ **OK** (explicitly out of scope) |
|
||||||
|
| 6. Log recovery actions | ✅ Extensive logging with `DNP-REACTIVATION` tag | ✅ Complete |
|
||||||
|
|
||||||
|
**Result**: ✅ **All in-scope requirements implemented**
|
||||||
|
|
||||||
|
### ✅ Acceptance Criteria Check
|
||||||
|
|
||||||
|
**Doc C §3.1.2 Acceptance Criteria**:
|
||||||
|
- ✅ Test scenario matches Phase 1 Test 1
|
||||||
|
- ✅ Expected behavior matches Phase 1 implementation
|
||||||
|
- ✅ Pass criteria align with Phase 1 success metrics
|
||||||
|
|
||||||
|
**Result**: ✅ **Acceptance criteria aligned**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Alignment with Doc A (Platform Facts)
|
||||||
|
|
||||||
|
### ✅ Platform Reference Check
|
||||||
|
|
||||||
|
**Doc A §2.1.4 - Alarms can be restored after app restart**:
|
||||||
|
- ✅ Phase 1 references this capability correctly
|
||||||
|
- ✅ Implementation uses AlarmManager APIs as documented
|
||||||
|
- ✅ No platform assumptions beyond Doc A
|
||||||
|
|
||||||
|
**Missing**: Phase 1 doesn't explicitly cite Doc A §2.1.4 in the implementation section (minor)
|
||||||
|
|
||||||
|
**Recommendation**: Add explicit reference to Doc A §2.1.4 in Phase 1 §2 (Implementation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Alignment with Doc B (Test Scenarios)
|
||||||
|
|
||||||
|
### ✅ Test Scenario Check
|
||||||
|
|
||||||
|
**Doc B Test 4 - Device Reboot** (Step 5: Cold Start):
|
||||||
|
- ✅ Phase 1 Test 1 matches Doc B scenario
|
||||||
|
- ✅ Test steps align
|
||||||
|
- ✅ Expected results match
|
||||||
|
|
||||||
|
**Result**: ✅ **Test scenarios aligned**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Cross-Reference Verification
|
||||||
|
|
||||||
|
### ✅ Cross-References Present
|
||||||
|
|
||||||
|
| Reference | Location | Status |
|
||||||
|
|-----------|----------|--------|
|
||||||
|
| Doc C §3.1.2 | Phase 1 line 9 | ✅ Correct |
|
||||||
|
| Doc A (general) | Phase 1 line 19 | ✅ Present |
|
||||||
|
| Doc C (general) | Phase 1 line 18 | ✅ Present |
|
||||||
|
| Phase 2/3 | Phase 1 lines 21-22 | ✅ Present |
|
||||||
|
|
||||||
|
### ⚠️ Missing Cross-References
|
||||||
|
|
||||||
|
| Missing Reference | Should Be Added | Priority |
|
||||||
|
|-------------------|-----------------|----------|
|
||||||
|
| Doc A §2.1.4 | In §2 (Implementation) | Minor |
|
||||||
|
| Doc B Test 4 | In §8 (Testing) | Minor |
|
||||||
|
|
||||||
|
**Result**: ✅ **Core references present**, minor improvements recommended
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Structure Verification
|
||||||
|
|
||||||
|
### ✅ Required Sections Present
|
||||||
|
|
||||||
|
| Section | Present | Notes |
|
||||||
|
|---------|---------|-------|
|
||||||
|
| Purpose | ✅ | Clear scope definition |
|
||||||
|
| Acceptance Criteria | ✅ | Detailed with metrics |
|
||||||
|
| Implementation | ✅ | Step-by-step with code |
|
||||||
|
| Data Integrity | ✅ | Validation rules defined |
|
||||||
|
| Rollback Safety | ✅ | No-crash guarantee |
|
||||||
|
| Testing Requirements | ✅ | 4 test scenarios |
|
||||||
|
| Implementation Checklist | ✅ | Complete checklist |
|
||||||
|
| Code References | ✅ | Existing code listed |
|
||||||
|
|
||||||
|
**Result**: ✅ **All required sections present**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Scope Verification
|
||||||
|
|
||||||
|
### ✅ Out of Scope Items Correctly Deferred
|
||||||
|
|
||||||
|
| Item | Phase 1 Status | Correct? |
|
||||||
|
|------|----------------|----------|
|
||||||
|
| Force stop detection | ❌ Deferred to Phase 2 | ✅ Correct |
|
||||||
|
| Warm start optimization | ❌ Deferred to Phase 2 | ✅ Correct |
|
||||||
|
| Boot receiver handling | ❌ Deferred to Phase 3 | ✅ Correct |
|
||||||
|
| Callback events | ❌ Deferred to Phase 2 | ✅ Correct |
|
||||||
|
| Fetch work recovery | ❌ Deferred to Phase 2 | ✅ Correct |
|
||||||
|
|
||||||
|
**Result**: ✅ **Scope boundaries correctly defined**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Code Quality Verification
|
||||||
|
|
||||||
|
### ✅ Implementation Quality
|
||||||
|
|
||||||
|
| Aspect | Status | Notes |
|
||||||
|
|--------|--------|-------|
|
||||||
|
| Error handling | ✅ | All exceptions caught |
|
||||||
|
| Timeout protection | ✅ | 2-second timeout |
|
||||||
|
| Data validation | ✅ | Integrity checks present |
|
||||||
|
| Logging | ✅ | Comprehensive logging |
|
||||||
|
| Non-blocking | ✅ | Async with coroutines |
|
||||||
|
| Rollback safety | ✅ | No-crash guarantee |
|
||||||
|
|
||||||
|
**Result**: ✅ **Code quality meets requirements**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Testing Verification
|
||||||
|
|
||||||
|
### ✅ Test Coverage
|
||||||
|
|
||||||
|
| Test Scenario | Present | Aligned with Doc B? |
|
||||||
|
|---------------|---------|---------------------|
|
||||||
|
| Cold start missed detection | ✅ | ✅ Yes |
|
||||||
|
| Future alarm rescheduling | ✅ | ✅ Yes |
|
||||||
|
| Recovery timeout | ✅ | ✅ Yes |
|
||||||
|
| Invalid data handling | ✅ | ✅ Yes |
|
||||||
|
|
||||||
|
**Result**: ✅ **Test coverage complete**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issues Found
|
||||||
|
|
||||||
|
### Issue 1: Missing Explicit Doc A Reference (Minor)
|
||||||
|
|
||||||
|
**Location**: Phase 1 §2 (Implementation)
|
||||||
|
|
||||||
|
**Problem**: Implementation doesn't explicitly cite Doc A §2.1.4
|
||||||
|
|
||||||
|
**Recommendation**: Add reference in §2.3 (Cold Start Recovery):
|
||||||
|
```markdown
|
||||||
|
**Platform Reference**: [Android §2.1.4](./alarms/01-platform-capability-reference.md#214-alarms-can-be-restored-after-app-restart)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Priority**: Minor (documentation improvement)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 2: Related Documentation Section (Minor)
|
||||||
|
|
||||||
|
**Location**: Phase 1 §11 (Related Documentation)
|
||||||
|
|
||||||
|
**Problem**: References old documentation files instead of unified docs
|
||||||
|
|
||||||
|
**Current**:
|
||||||
|
```markdown
|
||||||
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope (all phases)
|
||||||
|
- [Exploration Findings](./exploration-findings-initial.md) - Gap analysis
|
||||||
|
- [Plugin Requirements](./plugin-requirements-implementation.md) - Requirements
|
||||||
|
```
|
||||||
|
|
||||||
|
**Should Be**:
|
||||||
|
```markdown
|
||||||
|
- [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) - Master coordination document
|
||||||
|
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this phase implements
|
||||||
|
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Plugin Behavior Exploration](./alarms/02-plugin-behavior-exploration.md) - Test scenarios
|
||||||
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope (all phases)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Priority**: Minor (documentation improvement)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
- [x] Phase 1 implements all required actions from Doc C §3.1.2
|
||||||
|
- [x] Acceptance criteria align with Doc C
|
||||||
|
- [x] Platform facts referenced (implicitly, could be explicit)
|
||||||
|
- [x] Test scenarios align with Doc B
|
||||||
|
- [x] Cross-references to Doc C present and correct
|
||||||
|
- [x] Scope boundaries correctly defined
|
||||||
|
- [x] Implementation quality meets requirements
|
||||||
|
- [x] Testing requirements complete
|
||||||
|
- [x] Code structure follows best practices
|
||||||
|
- [x] Error handling comprehensive
|
||||||
|
- [x] Rollback safety guaranteed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Verdict
|
||||||
|
|
||||||
|
**Status**: ✅ **VERIFIED AND READY**
|
||||||
|
|
||||||
|
Phase 1 is:
|
||||||
|
- ✅ Complete and well-structured
|
||||||
|
- ✅ Aligned with Doc C requirements
|
||||||
|
- ✅ Properly scoped (cold start only)
|
||||||
|
- ✅ Ready for implementation
|
||||||
|
- ⚠️ Minor documentation improvements recommended (non-blocking)
|
||||||
|
|
||||||
|
**Recommendation**: Proceed with implementation. Apply minor documentation improvements during implementation or in a follow-up commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ **Begin Implementation** - Phase 1 is verified and ready
|
||||||
|
2. ⚠️ **Apply Minor Fixes** (optional) - Add explicit Doc A reference, update Related Documentation
|
||||||
|
3. ✅ **Follow Testing Requirements** - Use Phase 1 §8 test scenarios
|
||||||
|
4. ✅ **Update Status Matrix** - Mark Phase 1 as "In Use" when deployed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Phase 1 Directive](../android-implementation-directive-phase1.md) - Implementation guide
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md#312-app-cold-start) - Requirements
|
||||||
|
- [Platform Capability Reference](./01-platform-capability-reference.md#214-alarms-can-be-restored-after-app-restart) - OS facts
|
||||||
|
- [Activation Guide](./ACTIVATION-GUIDE.md) - How to use directives
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Verification Date**: November 2025
|
||||||
|
**Verified By**: Documentation Review
|
||||||
|
**Status**: Complete
|
||||||
|
|
||||||
358
docs/alarms/PHASE2-EMULATOR-TESTING.md
Normal file
358
docs/alarms/PHASE2-EMULATOR-TESTING.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# PHASE 2 – EMULATOR TESTING
|
||||||
|
|
||||||
|
**Force Stop Detection & Recovery**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
Phase 2 verifies that the Daily Notification Plugin correctly:
|
||||||
|
|
||||||
|
1. Detects **force stop** scenarios (where alarms may be cleared by the OS).
|
||||||
|
2. **Reschedules** future notifications when alarms are missing but schedules remain in the database.
|
||||||
|
3. **Avoids heavy recovery** when alarms are still intact.
|
||||||
|
4. **Does not misfire** force-stop recovery on first launch / empty database.
|
||||||
|
|
||||||
|
This document defines the emulator test procedure for Phase 2 using the script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test-apps/android-test-app/test-phase2.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Prerequisites
|
||||||
|
|
||||||
|
* Android emulator or device, e.g.:
|
||||||
|
* Pixel 8 / API 34 (recommended baseline)
|
||||||
|
* ADB available in `PATH` (or `ADB_BIN` exported)
|
||||||
|
* Project built with:
|
||||||
|
* Daily Notification Plugin integrated
|
||||||
|
* Test app at: `test-apps/android-test-app`
|
||||||
|
* Debug APK path:
|
||||||
|
* `app/build/outputs/apk/debug/app-debug.apk`
|
||||||
|
* Phase 1 behavior already implemented and verified:
|
||||||
|
* Cold start detection
|
||||||
|
* Missed notification marking
|
||||||
|
|
||||||
|
> **Note:** Some OS/device combinations do not clear alarms on `am force-stop`. In such cases, TEST 1 may partially skip or only verify scenario logging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. How to Run
|
||||||
|
|
||||||
|
From the `android-test-app` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd test-apps/android-test-app
|
||||||
|
chmod +x test-phase2.sh # first time only
|
||||||
|
./test-phase2.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
|
||||||
|
1. Perform pre-flight checks (ADB/emulator).
|
||||||
|
2. Build and install the debug APK.
|
||||||
|
3. Guide you through three tests:
|
||||||
|
* TEST 1: Force stop – alarms cleared
|
||||||
|
* TEST 2: Force stop / process stop – alarms intact
|
||||||
|
* TEST 3: First launch / empty DB safeguard
|
||||||
|
4. Print parsed recovery summaries from `DNP-REACTIVATION` logs.
|
||||||
|
|
||||||
|
You will be prompted for **UI actions** at each step (e.g., configuring the plugin, pressing "Test Notification").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Test Cases
|
||||||
|
|
||||||
|
### 4.1 TEST 1 – Force Stop with Cleared Alarms
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
|
||||||
|
Verify that when a force stop clears alarms, the plugin:
|
||||||
|
|
||||||
|
* Detects the **FORCE_STOP** scenario.
|
||||||
|
* Reschedules future notifications.
|
||||||
|
* Completes recovery with `errors=0`.
|
||||||
|
|
||||||
|
**Steps (Script Flow):**
|
||||||
|
|
||||||
|
1. **Launch & configure plugin**
|
||||||
|
* Script launches the app.
|
||||||
|
* In the app UI, confirm:
|
||||||
|
* `⚙️ Plugin Settings: ✅ Configured`
|
||||||
|
* `🔌 Native Fetcher: ✅ Configured`
|
||||||
|
* If not, press **Configure Plugin** and wait until both are ✅.
|
||||||
|
|
||||||
|
2. **Schedule a future notification**
|
||||||
|
* Click **Test Notification** (or equivalent) to schedule a notification a few minutes in the future.
|
||||||
|
|
||||||
|
3. **Verify alarms are scheduled**
|
||||||
|
* Script runs:
|
||||||
|
```bash
|
||||||
|
adb shell dumpsys alarm | grep com.timesafari.dailynotification
|
||||||
|
```
|
||||||
|
* Confirm at least one `RTC_WAKEUP` alarm for `com.timesafari.dailynotification`.
|
||||||
|
|
||||||
|
4. **Force stop the app**
|
||||||
|
* Script executes:
|
||||||
|
```bash
|
||||||
|
adb shell am force-stop com.timesafari.dailynotification
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Confirm alarms after force stop**
|
||||||
|
* Script re-runs `dumpsys alarm`.
|
||||||
|
* Ideal test case: **0** alarms for `com.timesafari.dailynotification` (alarms cleared).
|
||||||
|
|
||||||
|
6. **Trigger recovery**
|
||||||
|
* Script clears logcat and launches the app.
|
||||||
|
* Wait ~5 seconds for recovery.
|
||||||
|
|
||||||
|
7. **Collect and inspect logs**
|
||||||
|
* Script collects `DNP-REACTIVATION` logs and parses:
|
||||||
|
* `scenario=<value>`
|
||||||
|
* `missed=<n>`
|
||||||
|
* `rescheduled=<n>`
|
||||||
|
* `verified=<n>`
|
||||||
|
* `errors=<n>`
|
||||||
|
|
||||||
|
**Expected Logs (Ideal Case):**
|
||||||
|
|
||||||
|
* Scenario:
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Detected scenario: FORCE_STOP
|
||||||
|
```
|
||||||
|
|
||||||
|
* Alarm handling:
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <time>
|
||||||
|
DNP-REACTIVATION: Rescheduled missing alarm: daily_<id> at <time>
|
||||||
|
```
|
||||||
|
|
||||||
|
* Summary:
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Force stop recovery completed: missed=1, rescheduled=1, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pass Criteria:**
|
||||||
|
|
||||||
|
* `scenario=FORCE_STOP`
|
||||||
|
* `rescheduled > 0`
|
||||||
|
* `errors = 0`
|
||||||
|
* Script prints:
|
||||||
|
> `✅ TEST 1 PASSED: Force stop detected and alarms rescheduled (scenario=FORCE_STOP, rescheduled=1).`
|
||||||
|
|
||||||
|
**Partial / Edge Cases:**
|
||||||
|
|
||||||
|
* If alarms remain after `force-stop` on this device:
|
||||||
|
* Script warns that FORCE_STOP may not fully trigger.
|
||||||
|
* Mark this as **environment-limited** rather than a plugin failure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 TEST 2 – Force Stop / Process Stop with Intact Alarms
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
|
||||||
|
Ensure we **do not run heavy force-stop recovery** when alarms are still intact.
|
||||||
|
|
||||||
|
**Steps (Script Flow):**
|
||||||
|
|
||||||
|
1. **Launch & configure plugin**
|
||||||
|
* Same as TEST 1 (ensure both plugin statuses are ✅).
|
||||||
|
|
||||||
|
2. **Schedule another future notification**
|
||||||
|
* Click **Test Notification** again to create a second schedule.
|
||||||
|
|
||||||
|
3. **Verify alarms are scheduled**
|
||||||
|
* Confirm multiple alarms for `com.timesafari.dailynotification` via `dumpsys alarm`.
|
||||||
|
|
||||||
|
4. **Simulate a "soft stop"**
|
||||||
|
* Script runs:
|
||||||
|
```bash
|
||||||
|
adb shell am kill com.timesafari.dailynotification
|
||||||
|
```
|
||||||
|
* Intent: stop the process but **not** clear alarms (actual behavior may vary by OS).
|
||||||
|
|
||||||
|
5. **Check alarms after soft stop**
|
||||||
|
* Ensure alarms are still present in `dumpsys alarm`.
|
||||||
|
|
||||||
|
6. **Trigger recovery**
|
||||||
|
* Script clears logcat and relaunches the app.
|
||||||
|
* Wait ~5 seconds.
|
||||||
|
|
||||||
|
7. **Collect logs**
|
||||||
|
* Script parses `DNP-REACTIVATION` logs for:
|
||||||
|
* `scenario`
|
||||||
|
* `rescheduled`
|
||||||
|
* `errors`
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
|
||||||
|
* `scenario` should **not** be `FORCE_STOP` (e.g., `COLD_START` or another non-force-stop scenario, depending on your implementation).
|
||||||
|
* `rescheduled = 0`.
|
||||||
|
* `errors = 0`.
|
||||||
|
|
||||||
|
**Pass Criteria:**
|
||||||
|
|
||||||
|
* Alarms still present after soft stop.
|
||||||
|
* Recovery summary indicates **no force-stop-level rescheduling** when alarms are intact:
|
||||||
|
* `scenario != FORCE_STOP`
|
||||||
|
* `rescheduled = 0`
|
||||||
|
* Script prints:
|
||||||
|
> `✅ TEST 2 PASSED: No heavy force-stop recovery when alarms intact (scenario=<value>, rescheduled=0).`
|
||||||
|
|
||||||
|
If `scenario=FORCE_STOP` and `rescheduled>0` here, treat this as a **warning** and review scenario detection logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 TEST 3 – First Launch / Empty DB Safeguard
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
|
||||||
|
Ensure **force-stop recovery is not mis-triggered** when the app is freshly installed with **no schedules**.
|
||||||
|
|
||||||
|
**Steps (Script Flow):**
|
||||||
|
|
||||||
|
1. **Clear state**
|
||||||
|
* Script uninstalls the app to clear DB/state:
|
||||||
|
```bash
|
||||||
|
adb uninstall com.timesafari.dailynotification
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Reinstall APK**
|
||||||
|
* Script reinstalls `app-debug.apk`.
|
||||||
|
|
||||||
|
3. **Launch app without scheduling anything**
|
||||||
|
* Script launches the app.
|
||||||
|
* Do **not** schedule notifications or configure plugin beyond initial display.
|
||||||
|
|
||||||
|
4. **Collect logs**
|
||||||
|
* Script grabs `DNP-REACTIVATION` logs and parses:
|
||||||
|
* `scenario`
|
||||||
|
* `rescheduled`
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
|
||||||
|
* Either:
|
||||||
|
* No `DNP-REACTIVATION` logs at all (no recovery run), **or**
|
||||||
|
* A specific "no schedules" scenario, e.g.:
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: No schedules present — skipping recovery (first launch)
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Detected scenario: NONE
|
||||||
|
```
|
||||||
|
|
||||||
|
* In both cases:
|
||||||
|
* `rescheduled = 0`.
|
||||||
|
|
||||||
|
**Pass Criteria:**
|
||||||
|
|
||||||
|
* If **no logs**:
|
||||||
|
* Pass: recovery correctly doesn't run at all on empty DB.
|
||||||
|
* If logs present:
|
||||||
|
* `scenario=NONE` (or equivalent) **and** `rescheduled=0`.
|
||||||
|
|
||||||
|
Script will report success when:
|
||||||
|
|
||||||
|
* `scenario == NONE_SCENARIO_VALUE` and `rescheduled=0`, or
|
||||||
|
* No recovery logs are found.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Latest Known Good Run (Template)
|
||||||
|
|
||||||
|
Fill this in after your first successful emulator run.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
## Latest Known Good Run (Emulator)
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
|
||||||
|
- Device: Pixel 8 API 34 (Android 14)
|
||||||
|
- App ID: `com.timesafari.dailynotification`
|
||||||
|
- Build: Debug APK (`app-debug.apk`) from commit `<GIT_HASH>`
|
||||||
|
- Script: `./test-phase2.sh`
|
||||||
|
- Date: 2025-11-XX
|
||||||
|
|
||||||
|
**Results**
|
||||||
|
|
||||||
|
- ✅ TEST 1: Force Stop – Alarms Cleared
|
||||||
|
- `scenario=FORCE_STOP`
|
||||||
|
- `missed=1, rescheduled=1, verified=0, errors=0`
|
||||||
|
|
||||||
|
- ✅ TEST 2: Force Stop / Process Stop – Alarms Intact
|
||||||
|
- `scenario=COLD_START` (or equivalent non-force-stop scenario)
|
||||||
|
- `rescheduled=0, errors=0`
|
||||||
|
|
||||||
|
- ✅ TEST 3: First Launch / No Schedules
|
||||||
|
- `scenario=NONE` (or no logs)
|
||||||
|
- `rescheduled=0`
|
||||||
|
|
||||||
|
**Conclusion:**
|
||||||
|
Phase 2 **Force Stop Detection & Recovery** is verified on the emulator using `test-phase2.sh`. This run is the canonical reference for future regression testing.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Troubleshooting
|
||||||
|
|
||||||
|
### Alarms Not Cleared on Force Stop
|
||||||
|
|
||||||
|
**Symptom**: `am force-stop` doesn't clear alarms in AlarmManager
|
||||||
|
|
||||||
|
**Cause**: Some Android versions/emulators don't clear alarms on force stop
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- This is expected behavior on some systems
|
||||||
|
- TEST 1 will run as Phase 1 (cold start) recovery
|
||||||
|
- For full force stop validation, test on a device/OS that clears alarms
|
||||||
|
- Script will report this as an environment limitation, not a failure
|
||||||
|
|
||||||
|
### Scenario Not Detected as FORCE_STOP
|
||||||
|
|
||||||
|
**Symptom**: Logs show `COLD_START` even when alarms were cleared
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. Scenario detection logic not implemented (Phase 2 not complete)
|
||||||
|
2. Alarm count check failing (`alarmsExist()` returning true when it shouldn't)
|
||||||
|
3. Database query timing issue
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Verify Phase 2 implementation is complete
|
||||||
|
- Check `ReactivationManager.detectScenario()` implementation
|
||||||
|
- Review logs for alarm existence checks
|
||||||
|
- Verify `alarmsExist()` uses PendingIntent check (not `nextAlarmClock`)
|
||||||
|
|
||||||
|
### Recovery Doesn't Reschedule Alarms
|
||||||
|
|
||||||
|
**Symptom**: `rescheduled=0` even when alarms were cleared
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. Schedules not in database
|
||||||
|
2. Reschedule logic failing
|
||||||
|
3. Alarm scheduling permissions missing
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Verify schedules exist in database
|
||||||
|
- Check logs for reschedule errors
|
||||||
|
- Verify exact alarm permission is granted
|
||||||
|
- Review `performForceStopRecovery()` implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Related Documentation
|
||||||
|
|
||||||
|
- [Phase 2 Directive](../android-implementation-directive-phase2.md) - Implementation details
|
||||||
|
- [Phase 2 Verification](./PHASE2-VERIFICATION.md) - Verification report
|
||||||
|
- [Phase 1 Testing Guide](./PHASE1-EMULATOR-TESTING.md) - Prerequisite testing
|
||||||
|
- [Activation Guide](./ACTIVATION-GUIDE.md) - How to use directives
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md) - Requirements Phase 2 implements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Ready for testing (Phase 2 implementation pending)
|
||||||
|
**Last Updated**: November 2025
|
||||||
195
docs/alarms/PHASE2-VERIFICATION.md
Normal file
195
docs/alarms/PHASE2-VERIFICATION.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Phase 2 – Force Stop Recovery Verification
|
||||||
|
|
||||||
|
**Plugin:** Daily Notification Plugin
|
||||||
|
**Scope:** Force stop detection & recovery (App ID: `com.timesafari.dailynotification`)
|
||||||
|
**Related Docs:**
|
||||||
|
|
||||||
|
- `android-implementation-directive-phase2.md`
|
||||||
|
- `03-plugin-requirements.md` (Force Stop & App Termination behavior)
|
||||||
|
- `PHASE2-EMULATOR-TESTING.md`
|
||||||
|
- `000-UNIFIED-ALARM-DIRECTIVE.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objective
|
||||||
|
|
||||||
|
Phase 2 verifies that the Daily Notification Plugin:
|
||||||
|
|
||||||
|
1. Correctly detects **force stop** conditions where alarms may have been cleared.
|
||||||
|
2. **Reschedules** future notifications when schedules exist in the database but alarms are missing.
|
||||||
|
3. **Avoids heavy recovery** when alarms are intact (no false positives).
|
||||||
|
4. **Does not misfire** force-stop recovery on first launch / empty database.
|
||||||
|
|
||||||
|
Phase 2 builds on Phase 1, which already covers:
|
||||||
|
|
||||||
|
- Cold start detection
|
||||||
|
- Missed notification marking
|
||||||
|
- Basic alarm verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Test Method
|
||||||
|
|
||||||
|
Verification is performed using the emulator test harness:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd test-apps/android-test-app
|
||||||
|
./test-phase2.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script:
|
||||||
|
|
||||||
|
* Builds and installs the debug APK (`app/build/outputs/apk/debug/app-debug.apk`).
|
||||||
|
* Guides the tester through UI steps for scheduling notifications and configuring the plugin.
|
||||||
|
* Simulates:
|
||||||
|
* `force-stop` behavior via `adb shell am force-stop ...`
|
||||||
|
* "Soft stop" / process kill via `adb shell am kill ...`
|
||||||
|
* First launch / empty DB via uninstall + reinstall
|
||||||
|
* Collects and parses `DNP-REACTIVATION` log lines, extracting:
|
||||||
|
* `scenario`
|
||||||
|
* `missed`
|
||||||
|
* `rescheduled`
|
||||||
|
* `verified`
|
||||||
|
* `errors`
|
||||||
|
|
||||||
|
Detailed steps and expectations are documented in `PHASE2-EMULATOR-TESTING.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Test Matrix
|
||||||
|
|
||||||
|
| ID | Scenario | Method / Script Step | Expected Behavior | Result | Notes |
|
||||||
|
| --- | ---------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------ | ----- |
|
||||||
|
| 2.1 | Force stop clears alarms | `test-phase2.sh` – TEST 1: Force Stop – Alarms Cleared | `scenario=FORCE_STOP`, `rescheduled>0`, `errors=0` | ☐ | |
|
||||||
|
| 2.2 | Force stop / process stop with alarms intact | `test-phase2.sh` – TEST 2: Soft Stop – Alarms Intact | `scenario != FORCE_STOP`, `rescheduled=0`, `errors=0` | ☐ | |
|
||||||
|
| 2.3 | First launch / empty DB (no schedules present) | `test-phase2.sh` – TEST 3: First Launch / No Schedules | Either no recovery logs **or** `scenario=NONE` (or equivalent) and `rescheduled=0`, `errors=0` | ☐ | |
|
||||||
|
|
||||||
|
> Fill in **Result** and **Notes** after executing the script on your baseline emulator/device.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Expected Log Patterns
|
||||||
|
|
||||||
|
### 4.1 Force Stop – Alarms Cleared (Test 2.1)
|
||||||
|
|
||||||
|
Typical expected `DNP-REACTIVATION` log patterns:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Detected scenario: FORCE_STOP
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <time>
|
||||||
|
DNP-REACTIVATION: Rescheduled missing alarm: daily_<id> at <time>
|
||||||
|
DNP-REACTIVATION: Force stop recovery completed: missed=1, rescheduled=1, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
The **script** will report:
|
||||||
|
|
||||||
|
```text
|
||||||
|
✅ TEST 1 PASSED: Force stop detected and alarms rescheduled (scenario=FORCE_STOP, rescheduled=1).
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Soft Stop – Alarms Intact (Test 2.2)
|
||||||
|
|
||||||
|
Typical expected patterns:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Detected scenario: COLD_START
|
||||||
|
DNP-REACTIVATION: Cold start recovery completed: missed=0, rescheduled=0, verified>=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
The script should **not** treat this as a force-stop recovery:
|
||||||
|
|
||||||
|
```text
|
||||||
|
✅ TEST 2 PASSED: No heavy force-stop recovery when alarms are intact (scenario=COLD_START, rescheduled=0).
|
||||||
|
```
|
||||||
|
|
||||||
|
(Adjust `scenario` name to match your actual implementation.)
|
||||||
|
|
||||||
|
### 4.3 First Launch / Empty DB (Test 2.3)
|
||||||
|
|
||||||
|
Two acceptable patterns:
|
||||||
|
|
||||||
|
1. **No recovery logs at all** (`DNP-REACTIVATION` absent), or
|
||||||
|
2. Explicit "no schedules" scenario, e.g.:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: No schedules present — skipping recovery (first launch)
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Detected scenario: NONE
|
||||||
|
```
|
||||||
|
|
||||||
|
Script-level success message might be:
|
||||||
|
|
||||||
|
```text
|
||||||
|
✅ TEST 3 PASSED: NONE scenario detected with no rescheduling.
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```text
|
||||||
|
✅ TEST 3 PASSED: No recovery logs when there are no schedules (safe behavior).
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Latest Known Good Run (Emulator) – Placeholder
|
||||||
|
|
||||||
|
> Update this section after your first successful run.
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
|
||||||
|
* Device: Pixel 8 API 34 (Android 14)
|
||||||
|
* App ID: `com.timesafari.dailynotification`
|
||||||
|
* Build: Debug `app-debug.apk` from commit `<GIT_HASH>`
|
||||||
|
* Script: `./test-phase2.sh`
|
||||||
|
* Date: 2025-11-XX
|
||||||
|
|
||||||
|
**Observed Results**
|
||||||
|
|
||||||
|
* ☐ **2.1 – Force Stop / Alarms Cleared**
|
||||||
|
* `scenario=FORCE_STOP`
|
||||||
|
* `missed=1, rescheduled=1, verified=0, errors=0`
|
||||||
|
|
||||||
|
* ☐ **2.2 – Soft Stop / Alarms Intact**
|
||||||
|
* `scenario=COLD_START` (or equivalent non-force-stop scenario)
|
||||||
|
* `rescheduled=0, errors=0`
|
||||||
|
|
||||||
|
* ☐ **2.3 – First Launch / Empty DB**
|
||||||
|
* `scenario=NONE` (or no logs)
|
||||||
|
* `rescheduled=0, errors=0`
|
||||||
|
|
||||||
|
**Conclusion:**
|
||||||
|
|
||||||
|
> To be filled after first successful emulator run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Overall Status
|
||||||
|
|
||||||
|
> To be updated once the first emulator pass is complete.
|
||||||
|
|
||||||
|
* **Implementation Status:** ☐ Pending / ✅ Implemented in `ReactivationManager` (Android plugin)
|
||||||
|
* **Test Harness:** ✅ `test-phase2.sh` in `test-apps/android-test-app`
|
||||||
|
* **Emulator Verification:** ☐ Pending / ✅ Completed (update when done)
|
||||||
|
|
||||||
|
Once all boxes are checked:
|
||||||
|
|
||||||
|
> **Overall Status:** ✅ **VERIFIED** – Phase 2 behavior is implemented, emulator-tested, and aligned with `03-plugin-requirements.md` and `android-implementation-directive-phase2.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Related Documentation
|
||||||
|
|
||||||
|
- [Phase 2 Directive](../android-implementation-directive-phase2.md) - Implementation details
|
||||||
|
- [Phase 2 Emulator Testing](./PHASE2-EMULATOR-TESTING.md) - Test procedures
|
||||||
|
- [Phase 1 Verification](./PHASE1-VERIFICATION.md) - Prerequisite verification
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md) - Requirements this phase implements
|
||||||
|
- [Platform Capability Reference](./01-platform-capability-reference.md) - OS-level facts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: ☐ **PENDING** – Phase 2 implementation and testing pending
|
||||||
|
**Last Updated**: November 2025
|
||||||
325
docs/alarms/PHASE3-EMULATOR-TESTING.md
Normal file
325
docs/alarms/PHASE3-EMULATOR-TESTING.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# PHASE 3 – EMULATOR TESTING
|
||||||
|
|
||||||
|
**Boot-Time Recovery (Device Reboot / System Restart)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
Phase 3 verifies that the Daily Notification Plugin correctly:
|
||||||
|
|
||||||
|
1. Reconstructs **AlarmManager** alarms after a full device/emulator reboot.
|
||||||
|
2. Handles **past** scheduled times by marking them as missed and scheduling the next occurrence.
|
||||||
|
3. Handles **empty DB / no schedules** without misfiring recovery.
|
||||||
|
4. Performs **silent boot recovery** (recreate alarms) even when the app is never opened after reboot.
|
||||||
|
|
||||||
|
This testing is driven by the script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test-apps/android-test-app/test-phase3.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Prerequisites
|
||||||
|
|
||||||
|
* Android emulator or device, e.g.:
|
||||||
|
* Pixel 8 / API 34 (recommended baseline)
|
||||||
|
* ADB available in `PATH` (or `ADB_BIN` exported)
|
||||||
|
* Project with:
|
||||||
|
* Daily Notification Plugin integrated
|
||||||
|
* Test app at `test-apps/android-test-app`
|
||||||
|
* Debug APK path:
|
||||||
|
* `app/build/outputs/apk/debug/app-debug.apk`
|
||||||
|
* Phase 1 and Phase 2 behaviors already implemented:
|
||||||
|
* Cold start detection
|
||||||
|
* Force-stop detection
|
||||||
|
* Missed / rescheduled / verified / errors summary fields
|
||||||
|
|
||||||
|
> ⚠️ **Important:**
|
||||||
|
> This script will reboot the emulator multiple times. Each reboot may take 30–60 seconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. How to Run
|
||||||
|
|
||||||
|
From the `android-test-app` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd test-apps/android-test-app
|
||||||
|
chmod +x test-phase3.sh # first time only
|
||||||
|
./test-phase3.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
|
||||||
|
1. Run pre-flight checks (ADB / emulator readiness).
|
||||||
|
2. Build and install the debug APK.
|
||||||
|
3. Guide you through four tests:
|
||||||
|
* **TEST 1:** Boot with Future Alarms
|
||||||
|
* **TEST 2:** Boot with Past Alarms
|
||||||
|
* **TEST 3:** Boot with No Schedules
|
||||||
|
* **TEST 4:** Silent Boot Recovery (App Never Opened)
|
||||||
|
4. Parse and display `DNP-REACTIVATION` logs, including:
|
||||||
|
* `scenario`
|
||||||
|
* `missed`
|
||||||
|
* `rescheduled`
|
||||||
|
* `verified`
|
||||||
|
* `errors`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Test Cases (Script-Driven Flow)
|
||||||
|
|
||||||
|
### 4.1 TEST 1 – Boot with Future Alarms
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
|
||||||
|
Verify alarms are recreated on boot when schedules have **future run times**.
|
||||||
|
|
||||||
|
**Script flow:**
|
||||||
|
|
||||||
|
1. **Launch app & check plugin status**
|
||||||
|
* Script calls `launch_app`.
|
||||||
|
* UI prompt: Confirm plugin status shows:
|
||||||
|
* `⚙️ Plugin Settings: ✅ Configured`
|
||||||
|
* `🔌 Native Fetcher: ✅ Configured`
|
||||||
|
* If not, click **Configure Plugin**, wait until both show ✅, then continue.
|
||||||
|
|
||||||
|
2. **Schedule at least one future notification**
|
||||||
|
* UI prompt: Click e.g. **Test Notification** to schedule a notification a few minutes in the future.
|
||||||
|
|
||||||
|
3. **Verify alarms are scheduled (pre-boot)**
|
||||||
|
* Script calls `show_alarms` and `count_alarms`.
|
||||||
|
* You should see at least one `RTC_WAKEUP` entry for `com.timesafari.dailynotification`.
|
||||||
|
|
||||||
|
4. **Reboot emulator**
|
||||||
|
* Script calls `reboot_emulator`:
|
||||||
|
* `adb reboot`
|
||||||
|
* `adb wait-for-device`
|
||||||
|
* Polls `getprop sys.boot_completed` until `1`.
|
||||||
|
* You are warned that reboot will take 30–60 seconds.
|
||||||
|
|
||||||
|
5. **Collect boot recovery logs**
|
||||||
|
* Script calls `get_recovery_logs`:
|
||||||
|
```bash
|
||||||
|
adb logcat -d | grep "DNP-REACTIVATION"
|
||||||
|
```
|
||||||
|
* It parses:
|
||||||
|
* `missed`, `rescheduled`, `verified`, `errors`
|
||||||
|
* `scenario` via:
|
||||||
|
* `Starting boot recovery`/`boot recovery` → `scenario=BOOT`
|
||||||
|
* or `Detected scenario: <VALUE>`
|
||||||
|
|
||||||
|
6. **Verify alarms were recreated (post-boot)**
|
||||||
|
* Script calls `show_alarms` and `count_alarms` again.
|
||||||
|
* Checks `scenario` and `rescheduled`.
|
||||||
|
|
||||||
|
**Expected log patterns:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Starting boot recovery
|
||||||
|
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <time>
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled>=1, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pass criteria (as per script):**
|
||||||
|
|
||||||
|
* `errors = 0`
|
||||||
|
* `scenario = BOOT` (or boot detected via log text)
|
||||||
|
* `rescheduled > 0`
|
||||||
|
* Script prints:
|
||||||
|
> `✅ TEST 1 PASSED: Boot recovery detected and alarms rescheduled (scenario=BOOT, rescheduled=<n>).`
|
||||||
|
|
||||||
|
If boot recovery runs but `rescheduled=0`, script warns and suggests checking boot logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 TEST 2 – Boot with Past Alarms
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
|
||||||
|
Verify past alarms are marked as missed and **next occurrences are scheduled** after boot.
|
||||||
|
|
||||||
|
**Script flow:**
|
||||||
|
|
||||||
|
1. **Launch app & ensure plugin configured**
|
||||||
|
* Same plugin status check as TEST 1.
|
||||||
|
|
||||||
|
2. **Schedule a notification in the near future**
|
||||||
|
* UI prompt: Schedule such that **by the time you reboot and the device comes back, the planned notification time is in the past**.
|
||||||
|
|
||||||
|
3. **Wait or adjust so the alarm is effectively "in the past" at boot**
|
||||||
|
* The script may instruct you to wait, or you can coordinate timing manually.
|
||||||
|
|
||||||
|
4. **Reboot emulator**
|
||||||
|
* Same `reboot_emulator` path as TEST 1.
|
||||||
|
|
||||||
|
5. **Collect boot recovery logs**
|
||||||
|
* Script parses:
|
||||||
|
* `missed`, `rescheduled`, `errors`, `scenario`.
|
||||||
|
|
||||||
|
**Expected log patterns:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Starting boot recovery
|
||||||
|
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||||
|
DNP-REACTIVATION: Marked missed notification: daily_<id>
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <next_time>
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed>=1, rescheduled>=1, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pass criteria:**
|
||||||
|
|
||||||
|
* `errors = 0`
|
||||||
|
* `missed >= 1`
|
||||||
|
* `rescheduled >= 1`
|
||||||
|
* Script prints:
|
||||||
|
> `✅ TEST 2 PASSED: Past alarms detected and next occurrence scheduled (missed=<m>, rescheduled=<r>).`
|
||||||
|
|
||||||
|
If `missed >= 1` but `rescheduled = 0`, script warns that reschedule logic may be incomplete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 TEST 3 – Boot with No Schedules
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
|
||||||
|
Verify boot recovery handles an **empty DB / no schedules** safely and does **not** schedule anything.
|
||||||
|
|
||||||
|
**Script flow:**
|
||||||
|
|
||||||
|
1. **Uninstall app to clear DB/state**
|
||||||
|
* Script calls:
|
||||||
|
```bash
|
||||||
|
adb uninstall com.timesafari.dailynotification
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Reinstall APK**
|
||||||
|
* Script reinstalls `app-debug.apk`.
|
||||||
|
|
||||||
|
3. **Launch app WITHOUT scheduling anything**
|
||||||
|
* Script launches app; you do not configure or schedule.
|
||||||
|
|
||||||
|
4. **Collect boot/logs**
|
||||||
|
* Script reads `DNP-REACTIVATION` logs and checks:
|
||||||
|
* if there are no logs, or
|
||||||
|
* if there's a "No schedules found / present" message, or
|
||||||
|
* if `scenario=NONE` and `rescheduled=0`.
|
||||||
|
|
||||||
|
**Expected patterns:**
|
||||||
|
|
||||||
|
* *Ideal simple case:* **No** `DNP-REACTIVATION` logs at all, or:
|
||||||
|
* Explicit message in logs:
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: ... No schedules found ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pass criteria (as per script):**
|
||||||
|
|
||||||
|
* If **no logs**:
|
||||||
|
* Pass: `TEST 3 PASSED: No recovery logs when there are no schedules (safe behavior).`
|
||||||
|
* If logs exist:
|
||||||
|
* Contains `No schedules found` / `No schedules present` **and** `rescheduled=0`, or
|
||||||
|
* `scenario = NONE` and `rescheduled = 0`.
|
||||||
|
|
||||||
|
Any case where `rescheduled > 0` with an empty DB is flagged as a warning (boot recovery misfiring).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.4 TEST 4 – Silent Boot Recovery (App Never Opened)
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
|
||||||
|
Verify that boot recovery **occurs silently**, recreating alarms **without opening the app** after reboot.
|
||||||
|
|
||||||
|
**Script flow:**
|
||||||
|
|
||||||
|
1. **Launch app and configure plugin**
|
||||||
|
* Same plugin status flow:
|
||||||
|
* Ensure both plugin checks are ✅.
|
||||||
|
* Schedule a future notification via UI.
|
||||||
|
|
||||||
|
2. **Verify alarms are scheduled**
|
||||||
|
* Script shows alarms and counts (`before_count`).
|
||||||
|
|
||||||
|
3. **Reboot emulator**
|
||||||
|
* Script runs `reboot_emulator` and explicitly warns:
|
||||||
|
* Do **not** open the app after reboot.
|
||||||
|
* After emulator returns, script instructs you to **not touch the app UI**.
|
||||||
|
|
||||||
|
4. **Collect boot recovery logs**
|
||||||
|
* Script gathers and parses `DNP-REACTIVATION` lines.
|
||||||
|
|
||||||
|
5. **Verify alarms were recreated without app launch**
|
||||||
|
* Script calls `show_alarms` and `count_alarms` again.
|
||||||
|
* Uses `rescheduled` + alarm count to decide.
|
||||||
|
|
||||||
|
**Pass criteria (as per script):**
|
||||||
|
|
||||||
|
* `rescheduled > 0` after boot, and
|
||||||
|
* Alarm count after boot is > 0, and
|
||||||
|
* App was **never** launched by the user after reboot.
|
||||||
|
|
||||||
|
Script prints one of:
|
||||||
|
|
||||||
|
```text
|
||||||
|
✅ TEST 4 PASSED: Boot recovery occurred silently and alarms were recreated (rescheduled=<n>) without app launch.
|
||||||
|
|
||||||
|
✅ TEST 4 PASSED: Boot recovery occurred silently (rescheduled=<n>), but alarm count check unclear.
|
||||||
|
```
|
||||||
|
|
||||||
|
If boot recovery logs are present but no alarms appear, script warns; if no boot-recovery logs are found at all, script suggests verifying the boot receiver and BOOT_COMPLETED permission.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Overall Summary Section (from Script)
|
||||||
|
|
||||||
|
At the end, the script prints:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TEST 1: Boot with Future Alarms
|
||||||
|
- Check logs for boot recovery and rescheduled>0
|
||||||
|
|
||||||
|
TEST 2: Boot with Past Alarms
|
||||||
|
- Check logs for missed>=1 and rescheduled>=1
|
||||||
|
|
||||||
|
TEST 3: Boot with No Schedules
|
||||||
|
- Check that no recovery runs or that an explicit 'No schedules found' is logged without rescheduling
|
||||||
|
|
||||||
|
TEST 4: Silent Boot Recovery
|
||||||
|
- Check that boot recovery occurred and alarms were recreated without app launch
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this as a quick checklist after a run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Troubleshooting Notes
|
||||||
|
|
||||||
|
* If **no boot recovery logs** ever appear:
|
||||||
|
* Check that `BootReceiver` is declared and `RECEIVE_BOOT_COMPLETED` permission is set.
|
||||||
|
* Ensure the app is installed in internal storage (not moved to SD).
|
||||||
|
|
||||||
|
* If **errors > 0** in summary:
|
||||||
|
* Inspect the full `DNP-REACTIVATION` logs printed by the script.
|
||||||
|
|
||||||
|
* If **alarming duplication** is observed:
|
||||||
|
* Review `runBootRecovery` and dedupe logic around re-scheduling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Related Documentation
|
||||||
|
|
||||||
|
- [Phase 3 Directive](../android-implementation-directive-phase3.md) - Implementation details
|
||||||
|
- [Phase 3 Verification](./PHASE3-VERIFICATION.md) - Verification report
|
||||||
|
- [Phase 1 Testing Guide](./PHASE1-EMULATOR-TESTING.md) - Prerequisite testing
|
||||||
|
- [Phase 2 Testing Guide](./PHASE2-EMULATOR-TESTING.md) - Prerequisite testing
|
||||||
|
- [Activation Guide](./ACTIVATION-GUIDE.md) - How to use directives
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md) - Requirements Phase 3 implements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Ready for testing (Phase 3 implementation pending)
|
||||||
|
**Last Updated**: November 2025
|
||||||
201
docs/alarms/PHASE3-VERIFICATION.md
Normal file
201
docs/alarms/PHASE3-VERIFICATION.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# Phase 3 – Boot-Time Recovery Verification
|
||||||
|
|
||||||
|
**Plugin:** Daily Notification Plugin
|
||||||
|
**Scope:** Boot-Time Recovery (Recreate Alarms After Reboot)
|
||||||
|
**Related Docs:**
|
||||||
|
- `android-implementation-directive-phase3.md`
|
||||||
|
- `PHASE3-EMULATOR-TESTING.md`
|
||||||
|
- `test-phase3.sh`
|
||||||
|
- `000-UNIFIED-ALARM-DIRECTIVE.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objective
|
||||||
|
|
||||||
|
Phase 3 confirms that the Daily Notification Plugin:
|
||||||
|
|
||||||
|
1. Reconstructs all daily notification alarms after a **full device reboot**.
|
||||||
|
2. Correctly handles **past** vs **future** schedules:
|
||||||
|
- Past: mark as missed, schedule next occurrence
|
||||||
|
- Future: simply recreate alarms
|
||||||
|
3. Handles **empty DB / no schedules** without misfiring recovery.
|
||||||
|
4. Performs **silent boot recovery** (no app launch required) when schedules exist.
|
||||||
|
5. Logs a consistent, machine-readable summary:
|
||||||
|
- `scenario`
|
||||||
|
- `missed`
|
||||||
|
- `rescheduled`
|
||||||
|
- `verified`
|
||||||
|
- `errors`
|
||||||
|
|
||||||
|
Verification is performed via the emulator harness:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd test-apps/android-test-app
|
||||||
|
./test-phase3.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Test Matrix (From Script)
|
||||||
|
|
||||||
|
| ID | Scenario | Script Test | Expected Behavior | Result | Notes |
|
||||||
|
| --- | --------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------ | ----- |
|
||||||
|
| 3.1 | Boot with Future Alarms | TEST 1 – Boot with Future Alarms | `scenario=BOOT`, `rescheduled>0`, `errors=0`; alarms present after boot | ☐ | |
|
||||||
|
| 3.2 | Boot with Past Alarms | TEST 2 – Boot with Past Alarms | `missed>=1` and `rescheduled>=1`, `errors=0`; past schedules detected and next occurrences scheduled | ☐ | |
|
||||||
|
| 3.3 | Boot with No Schedules (Empty DB) | TEST 3 – Boot with No Schedules | Either no recovery logs **or** explicit "No schedules found/present" or `scenario=NONE` with `rescheduled=0`, `errors=0` | ☐ | |
|
||||||
|
| 3.4 | Silent Boot Recovery (App Never Opened) | TEST 4 – Silent Boot Recovery (App Never Opened) | `rescheduled>0`, alarms present after boot, and no user launch required; `errors=0` | ☐ | |
|
||||||
|
|
||||||
|
Fill **Result** and **Notes** after running `test-phase3.sh` on your baseline emulator/device.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Expected Log Patterns
|
||||||
|
|
||||||
|
The script filters logs with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb logcat -d | grep "DNP-REACTIVATION"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 Boot with Future Alarms (3.1 / TEST 1)
|
||||||
|
|
||||||
|
* Typical logs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Starting boot recovery
|
||||||
|
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <time>
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=<r>, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
* The script interprets this as:
|
||||||
|
* `scenario = BOOT` (via "Starting boot recovery" or "boot recovery" text or `Detected scenario: BOOT`)
|
||||||
|
* `rescheduled > 0`
|
||||||
|
* `errors = 0`
|
||||||
|
|
||||||
|
### 3.2 Boot with Past Alarms (3.2 / TEST 2)
|
||||||
|
|
||||||
|
* Typical logs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Starting boot recovery
|
||||||
|
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||||
|
DNP-REACTIVATION: Marked missed notification: daily_<id>
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <next_time>
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed=<m>, rescheduled=<r>, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
* The script parses `missed` and `rescheduled` and passes when:
|
||||||
|
* `missed >= 1`
|
||||||
|
* `rescheduled >= 1`
|
||||||
|
* `errors = 0`
|
||||||
|
|
||||||
|
### 3.3 Boot with No Schedules (3.3 / TEST 3)
|
||||||
|
|
||||||
|
Two acceptable patterns:
|
||||||
|
|
||||||
|
1. **No `DNP-REACTIVATION` logs at all** → safe behavior
|
||||||
|
2. Explicit "no schedules" logs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: ... No schedules found ...
|
||||||
|
```
|
||||||
|
|
||||||
|
or a neutral scenario:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: ... scenario=NONE ...
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=0, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
The script passes when:
|
||||||
|
|
||||||
|
* Either `logs` are empty, or
|
||||||
|
* Logs contain "No schedules found / present" with `rescheduled=0`, or
|
||||||
|
* `scenario=NONE` and `rescheduled=0`.
|
||||||
|
|
||||||
|
Any `rescheduled>0` in this state is flagged as a potential boot-recovery misfire.
|
||||||
|
|
||||||
|
### 3.4 Silent Boot Recovery (3.4 / TEST 4)
|
||||||
|
|
||||||
|
* Expected:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Starting boot recovery
|
||||||
|
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <time>
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=<r>, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
* After reboot:
|
||||||
|
* `count_alarms` > 0
|
||||||
|
* User **did not** relaunch the app manually
|
||||||
|
|
||||||
|
Script passes if:
|
||||||
|
|
||||||
|
* `rescheduled>0`, and
|
||||||
|
* Alarm count after boot is > 0, and
|
||||||
|
* Boot recovery is detected from logs (via "Starting boot recovery"/"boot recovery" or scenario).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Latest Known Good Run (Template)
|
||||||
|
|
||||||
|
> Fill this in after your first clean emulator run.
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
|
||||||
|
* Device: Pixel 8 API 34 (Android 14)
|
||||||
|
* App ID: `com.timesafari.dailynotification`
|
||||||
|
* Build: Debug `app-debug.apk` from commit `<GIT_HASH>`
|
||||||
|
* Script: `./test-phase3.sh`
|
||||||
|
* Date: 2025-11-XX
|
||||||
|
|
||||||
|
**Observed Results**
|
||||||
|
|
||||||
|
* ☐ **3.1 – Boot with Future Alarms**
|
||||||
|
* `scenario=BOOT`
|
||||||
|
* `missed=0, rescheduled=<r>, errors=0`
|
||||||
|
|
||||||
|
* ☐ **3.2 – Boot with Past Alarms**
|
||||||
|
* `missed=<m>=1`, `rescheduled=<r>≥1`, `errors=0`
|
||||||
|
|
||||||
|
* ☐ **3.3 – Boot with No Schedules**
|
||||||
|
* Either no logs, or explicit "No schedules found" with `rescheduled=0`
|
||||||
|
|
||||||
|
* ☐ **3.4 – Silent Boot Recovery**
|
||||||
|
* `rescheduled>0`, alarms present after boot, app not opened
|
||||||
|
|
||||||
|
**Conclusion:**
|
||||||
|
|
||||||
|
> Phase 3 **Boot-Time Recovery** is successfully verified on emulator using `test-phase3.sh`. This is the canonical baseline for future regression testing and refactors to `ReactivationManager` and `BootReceiver`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Overall Status
|
||||||
|
|
||||||
|
> Update once the first emulator run is complete.
|
||||||
|
|
||||||
|
* **Implementation Status:** ☐ Pending / ✅ Implemented (Boot receiver + `runBootRecovery`)
|
||||||
|
* **Test Harness:** ✅ `test-phase3.sh` in `test-apps/android-test-app`
|
||||||
|
* **Emulator Verification:** ☐ Pending / ✅ Completed
|
||||||
|
|
||||||
|
Once all test cases pass:
|
||||||
|
|
||||||
|
> **Overall Status:** ✅ **VERIFIED** – Phase 3 boot-time recovery is implemented and emulator-tested, aligned with `android-implementation-directive-phase3.md` and the unified alarm directive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Related Documentation
|
||||||
|
|
||||||
|
- [Phase 3 Directive](../android-implementation-directive-phase3.md) - Implementation details
|
||||||
|
- [Phase 3 Emulator Testing](./PHASE3-EMULATOR-TESTING.md) - Test procedures
|
||||||
|
- [Phase 1 Verification](./PHASE1-VERIFICATION.md) - Prerequisite verification
|
||||||
|
- [Phase 2 Verification](./PHASE2-VERIFICATION.md) - Prerequisite verification
|
||||||
|
- [Plugin Requirements](./03-plugin-requirements.md) - Requirements this phase implements
|
||||||
|
- [Platform Capability Reference](./01-platform-capability-reference.md) - OS-level facts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: ☐ **PENDING** – Phase 3 implementation and testing pending
|
||||||
|
**Last Updated**: November 2025
|
||||||
248
docs/android-alarm-persistence-directive.md
Normal file
248
docs/android-alarm-persistence-directive.md
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
# Android Alarm Persistence, Recovery, and Limitations
|
||||||
|
|
||||||
|
**⚠️ DEPRECATED**: This document has been superseded by [01-platform-capability-reference.md](./alarms/01-platform-capability-reference.md) as part of the unified alarm documentation structure.
|
||||||
|
|
||||||
|
**See**: [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) for the new documentation structure.
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: **DEPRECATED** - Superseded by unified structure
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document provides a **clean, consolidated, engineering-grade directive** summarizing Android's abilities and limitations for remembering, firing, and restoring alarms across:
|
||||||
|
|
||||||
|
- App kills
|
||||||
|
- Swipes from recents
|
||||||
|
- Device reboot
|
||||||
|
- **Force stop**
|
||||||
|
- User-triggered reactivation
|
||||||
|
|
||||||
|
This is the actionable version you can plug directly into your architecture docs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Principle
|
||||||
|
|
||||||
|
Android does **not** guarantee persistence of alarms across process death, swipes, or reboot.
|
||||||
|
|
||||||
|
It is the app's responsibility to **persist alarm definitions** and **re-schedule them** under allowed system conditions.
|
||||||
|
|
||||||
|
The following directives outline **exactly what is possible** and **what is impossible**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Allowed Behaviors (What *Can* Work)
|
||||||
|
|
||||||
|
### 2.1 Alarms survive UI kills (swipe from recents)
|
||||||
|
|
||||||
|
`AlarmManager.setExactAndAllowWhileIdle(...)` alarms **will fire** even after:
|
||||||
|
|
||||||
|
- App is swiped away
|
||||||
|
- App process is killed by the OS
|
||||||
|
|
||||||
|
The OS recreates your app's process to deliver the `PendingIntent`.
|
||||||
|
|
||||||
|
**Directive:**
|
||||||
|
|
||||||
|
Use `setExactAndAllowWhileIdle` for alarm execution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Alarms can be preserved across device reboot
|
||||||
|
|
||||||
|
Android wipes all alarms on reboot, but **you may recreate them**.
|
||||||
|
|
||||||
|
**Directive:**
|
||||||
|
|
||||||
|
1. Persist all alarms in storage (Room DB or SharedPreferences).
|
||||||
|
2. Add a `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED` broadcast receiver.
|
||||||
|
3. On boot, load all enabled alarms and reschedule them using AlarmManager.
|
||||||
|
|
||||||
|
**Permissions required:**
|
||||||
|
|
||||||
|
- `RECEIVE_BOOT_COMPLETED`
|
||||||
|
|
||||||
|
**Conditions:**
|
||||||
|
|
||||||
|
- User must have launched your app at least once before reboot to grant boot receiver execution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Alarms can fire full-screen notifications and wake the device
|
||||||
|
|
||||||
|
**Directive:**
|
||||||
|
|
||||||
|
Implement `setFullScreenIntent(...)`, use an IMPORTANCE_HIGH channel with `CATEGORY_ALARM`.
|
||||||
|
|
||||||
|
This allows Clock-app–style alarms even when the app is not foregrounded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 Alarms can be restored after app restart
|
||||||
|
|
||||||
|
If the user re-opens the app (direct user action), you may:
|
||||||
|
|
||||||
|
- Scan the persistent DB
|
||||||
|
- Detect "missed" alarms
|
||||||
|
- Reschedule future alarms
|
||||||
|
- Fire "missed alarm" notifications
|
||||||
|
- Reconstruct WorkManager/JobScheduler tasks wiped by OS
|
||||||
|
|
||||||
|
**Directive:**
|
||||||
|
|
||||||
|
Create a `ReactivationManager` that runs on every app launch and recomputes the correct alarm state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Forbidden Behaviors (What *Cannot* Work)
|
||||||
|
|
||||||
|
### 3.1 You cannot survive "Force Stop"
|
||||||
|
|
||||||
|
**Settings → Apps → YourApp → Force Stop** triggers:
|
||||||
|
|
||||||
|
- Removal of all alarms
|
||||||
|
- Removal of WorkManager tasks
|
||||||
|
- Blocking of all broadcast receivers (including BOOT_COMPLETED)
|
||||||
|
- Blocking of all JobScheduler jobs
|
||||||
|
- Blocking of AlarmManager callbacks
|
||||||
|
- Your app will NOT run until the user manually launches it again
|
||||||
|
|
||||||
|
**Directive:**
|
||||||
|
|
||||||
|
Accept that FORCE STOP is a hard kill.
|
||||||
|
|
||||||
|
No scheduling, alarms, jobs, or receivers may execute afterward.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 You cannot auto-resume after "Force Stop"
|
||||||
|
|
||||||
|
You may only resume tasks when:
|
||||||
|
|
||||||
|
- The user opens your app
|
||||||
|
- The user taps a notification belonging to your app
|
||||||
|
- The user interacts with a widget/deep link
|
||||||
|
- Another app explicitly targets your component
|
||||||
|
|
||||||
|
**Directive:**
|
||||||
|
|
||||||
|
Provide user-facing reactivation pathways (icon, widget, notification).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 Alarms cannot be preserved solely in RAM
|
||||||
|
|
||||||
|
Android can kill your app's RAM state at any time.
|
||||||
|
|
||||||
|
**Directive:**
|
||||||
|
|
||||||
|
All alarm data must be persisted in durable storage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 You cannot bypass Doze or battery optimization restrictions without permission
|
||||||
|
|
||||||
|
Doze may defer inexact alarms; exact alarms with `setExactAndAllowWhileIdle` are allowed.
|
||||||
|
|
||||||
|
**Directive:**
|
||||||
|
|
||||||
|
Request `SCHEDULE_EXACT_ALARM` on Android 12+.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Required Implementation Components
|
||||||
|
|
||||||
|
### 4.1 Persistent Storage
|
||||||
|
|
||||||
|
Create a table or serialized structure for alarms:
|
||||||
|
|
||||||
|
```
|
||||||
|
id: Int
|
||||||
|
timeMillis: Long
|
||||||
|
repeat: NONE | DAILY | WEEKLY | CUSTOM
|
||||||
|
label: String
|
||||||
|
enabled: Boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 Alarm Scheduling
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
alarmManager.setExactAndAllowWhileIdle(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
triggerAtMillis,
|
||||||
|
pendingIntent
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 Boot Receiver
|
||||||
|
|
||||||
|
Reschedules alarms from storage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.4 Reactivation Manager
|
||||||
|
|
||||||
|
Runs on **every app launch** and performs:
|
||||||
|
|
||||||
|
- Load pending alarms
|
||||||
|
- Detect overdue alarms
|
||||||
|
- Reschedule future alarms
|
||||||
|
- Trigger notifications for missed alarms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.5 Full-Screen Alarm UI
|
||||||
|
|
||||||
|
Use a `BroadcastReceiver` → Notification with full-screen intent → Activity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Summary of Android Alarm Capability Matrix
|
||||||
|
|
||||||
|
| Scenario | Will Alarm Fire? | Reason |
|
||||||
|
| --------------------------------------- | --------------------------------------- | --------------------------------------------------------------- |
|
||||||
|
| **Swipe from Recents** | ✅ Yes | AlarmManager resurrects the app process |
|
||||||
|
| **App silently killed by OS** | ✅ Yes | AlarmManager still holds scheduled alarms |
|
||||||
|
| **Device Reboot** | ❌ No (auto) / ✅ Yes (if you reschedule) | Alarms wiped on reboot |
|
||||||
|
| **Doze Mode** | ⚠️ Only "exact" alarms | Must use `setExactAndAllowWhileIdle` |
|
||||||
|
| **Force Stop** | ❌ Never | Android blocks all callbacks + receivers until next user launch |
|
||||||
|
| **User reopens app** | ✅ You may reschedule & recover | All logic must be implemented by app |
|
||||||
|
| **PendingIntent from user interaction** | ✅ If triggered by user | User action unlocks the app |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Final Directive
|
||||||
|
|
||||||
|
> **Design alarm behavior with the assumption that Android will destroy all scheduled work on reboot or force-stop.
|
||||||
|
>
|
||||||
|
> Persist all alarm definitions. On every boot or app reactivation, reconstruct and reschedule alarms.
|
||||||
|
>
|
||||||
|
> Never rely on the OS to preserve alarms except across UI process kills.
|
||||||
|
>
|
||||||
|
> Accept that "force stop" is a hard stop that cannot be bypassed.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md)
|
||||||
|
- [App Startup Recovery Solution](./app-startup-recovery-solution.md)
|
||||||
|
- [Reboot Testing Procedure](./reboot-testing-procedure.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Directives
|
||||||
|
|
||||||
|
Potential follow-up directives:
|
||||||
|
|
||||||
|
- **How to implement the minimal alarm system**
|
||||||
|
- **How to implement a Clock-style robust alarm system**
|
||||||
|
- **How to map this to your own app's architecture**
|
||||||
|
|
||||||
712
docs/android-implementation-directive-phase1.md
Normal file
712
docs/android-implementation-directive-phase1.md
Normal file
@@ -0,0 +1,712 @@
|
|||||||
|
# Android Implementation Directive: Phase 1 - Cold Start Recovery
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Phase 1 - Minimal Viable Recovery
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Last Synced With Plugin Version**: v1.1.0
|
||||||
|
|
||||||
|
**Implements**: [Plugin Requirements §3.1.2 - App Cold Start](./alarms/03-plugin-requirements.md#312-app-cold-start)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Phase 1 implements **minimal viable app launch recovery** for cold start scenarios. This focuses on detecting and handling missed notifications when the app launches after the process was killed.
|
||||||
|
|
||||||
|
**Scope**: Phase 1 implements **cold start recovery only**. Force stop detection, warm start optimization, and boot receiver enhancements are **out of scope** for this phase and deferred to later phases.
|
||||||
|
|
||||||
|
**Reference**:
|
||||||
|
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this phase implements
|
||||||
|
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope
|
||||||
|
- [Phase 2: Force Stop Recovery](./android-implementation-directive-phase2.md) - Next phase
|
||||||
|
- [Phase 3: Boot Receiver Enhancement](./android-implementation-directive-phase3.md) - Final phase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Acceptance Criteria
|
||||||
|
|
||||||
|
### 1.1 Definition of Done
|
||||||
|
|
||||||
|
**Phase 1 is complete when:**
|
||||||
|
|
||||||
|
1. ✅ **On cold start, missed notifications are detected**
|
||||||
|
- Notifications with `scheduled_time < currentTime` and `delivery_status != 'delivered'` are identified
|
||||||
|
- Detection runs automatically on app launch (via `DailyNotificationPlugin.load()`)
|
||||||
|
- Detection completes within 2 seconds (non-blocking)
|
||||||
|
|
||||||
|
2. ✅ **Missed notifications are marked in database**
|
||||||
|
- `delivery_status` updated to `'missed'`
|
||||||
|
- `last_delivery_attempt` updated to current time
|
||||||
|
- Status change logged in history table
|
||||||
|
|
||||||
|
3. ✅ **Future alarms are verified and rescheduled if missing**
|
||||||
|
- All enabled `notify` schedules checked against AlarmManager
|
||||||
|
- Missing alarms rescheduled using existing `NotifyReceiver.scheduleExactNotification()`
|
||||||
|
- No duplicate alarms created (verified before rescheduling)
|
||||||
|
|
||||||
|
4. ✅ **Recovery never crashes the app**
|
||||||
|
- All exceptions caught and logged
|
||||||
|
- Database errors don't propagate
|
||||||
|
- Invalid data handled gracefully
|
||||||
|
|
||||||
|
5. ✅ **Recovery is observable**
|
||||||
|
- All recovery actions logged with `DNP-REACTIVATION` tag
|
||||||
|
- Recovery metrics recorded in history table
|
||||||
|
- Logs include counts: missed detected, rescheduled, errors
|
||||||
|
|
||||||
|
### 1.2 Success Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Measurement |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Recovery execution time | < 2 seconds | Log timestamp difference |
|
||||||
|
| Missed detection accuracy | 100% | Manual verification via logs |
|
||||||
|
| Reschedule success rate | > 95% | History table outcome field |
|
||||||
|
| Crash rate | 0% | No exceptions propagate to app |
|
||||||
|
|
||||||
|
### 1.3 Out of Scope (Phase 1)
|
||||||
|
|
||||||
|
- ❌ Force stop detection (Phase 2)
|
||||||
|
- ❌ Warm start optimization (Phase 2)
|
||||||
|
- ❌ Boot receiver missed alarm handling (Phase 2)
|
||||||
|
- ❌ Callback event emission (Phase 2)
|
||||||
|
- ❌ Fetch work recovery (Phase 2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Implementation: ReactivationManager
|
||||||
|
|
||||||
|
### 2.1 Create New File
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||||
|
|
||||||
|
**Purpose**: Centralized cold start recovery logic
|
||||||
|
|
||||||
|
### 2.2 Class Structure
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.timesafari.dailynotification
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages recovery of alarms and notifications on app launch
|
||||||
|
* Phase 1: Cold start recovery only
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class ReactivationManager(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "DNP-REACTIVATION"
|
||||||
|
private const val RECOVERY_TIMEOUT_SECONDS = 2L
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform recovery on app launch
|
||||||
|
* Phase 1: Calls only performColdStartRecovery() when DB is non-empty
|
||||||
|
*
|
||||||
|
* Scenario detection is not implemented in Phase 1 - all app launches
|
||||||
|
* with non-empty DB are treated as cold start. Force stop, boot, and
|
||||||
|
* warm start handling are deferred to Phase 2.
|
||||||
|
*
|
||||||
|
* **Correction**: Must not run when DB is empty (first launch).
|
||||||
|
*
|
||||||
|
* Runs asynchronously with timeout to avoid blocking app startup
|
||||||
|
*
|
||||||
|
* Rollback Safety: If recovery fails, app continues normally
|
||||||
|
*/
|
||||||
|
fun performRecovery() {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
withTimeout(TimeUnit.SECONDS.toMillis(RECOVERY_TIMEOUT_SECONDS)) {
|
||||||
|
Log.i(TAG, "Starting app launch recovery (Phase 1: cold start only)")
|
||||||
|
|
||||||
|
// Correction: Short-circuit if DB is empty (first launch)
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val dbSchedules = db.scheduleDao().getEnabled()
|
||||||
|
|
||||||
|
if (dbSchedules.isEmpty()) {
|
||||||
|
Log.i(TAG, "No schedules present — skipping recovery (first launch)")
|
||||||
|
return@withTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = performColdStartRecovery()
|
||||||
|
Log.i(TAG, "App launch recovery completed: $result")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Rollback: Log error but don't crash
|
||||||
|
Log.e(TAG, "Recovery failed (non-fatal): ${e.message}", e)
|
||||||
|
// Record failure in history (best effort, don't fail if this fails)
|
||||||
|
try {
|
||||||
|
recordRecoveryFailure(e)
|
||||||
|
} catch (historyError: Exception) {
|
||||||
|
Log.w(TAG, "Failed to record recovery failure in history", historyError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... implementation methods below ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Cold Start Recovery
|
||||||
|
|
||||||
|
**Platform Reference**: [Android §2.1.4](./alarms/01-platform-capability-reference.md#214-alarms-can-be-restored-after-app-restart) - Alarms can be restored after app restart
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Perform cold start recovery
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* 1. Detect missed notifications (scheduled_time < now, not delivered)
|
||||||
|
* 2. Mark missed notifications in database
|
||||||
|
* 3. Verify future alarms are scheduled
|
||||||
|
* 4. Reschedule missing future alarms
|
||||||
|
*
|
||||||
|
* @return RecoveryResult with counts
|
||||||
|
*/
|
||||||
|
private suspend fun performColdStartRecovery(): RecoveryResult {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
Log.i(TAG, "Cold start recovery: checking for missed notifications")
|
||||||
|
|
||||||
|
// Step 1: Detect missed notifications
|
||||||
|
val missedNotifications = try {
|
||||||
|
db.notificationContentDao().getNotificationsReadyForDelivery(currentTime)
|
||||||
|
.filter { it.deliveryStatus != "delivered" }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to query missed notifications", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
var missedCount = 0
|
||||||
|
var missedErrors = 0
|
||||||
|
|
||||||
|
// Step 2: Mark missed notifications
|
||||||
|
missedNotifications.forEach { notification ->
|
||||||
|
try {
|
||||||
|
// Data integrity check: verify notification is valid
|
||||||
|
if (notification.id.isBlank()) {
|
||||||
|
Log.w(TAG, "Skipping invalid notification: empty ID")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update delivery status
|
||||||
|
notification.deliveryStatus = "missed"
|
||||||
|
notification.lastDeliveryAttempt = currentTime
|
||||||
|
notification.deliveryAttempts = (notification.deliveryAttempts ?: 0) + 1
|
||||||
|
|
||||||
|
db.notificationContentDao().updateNotification(notification)
|
||||||
|
missedCount++
|
||||||
|
|
||||||
|
Log.d(TAG, "Marked missed notification: ${notification.id}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
missedErrors++
|
||||||
|
Log.e(TAG, "Failed to mark missed notification: ${notification.id}", e)
|
||||||
|
// Continue processing other notifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Verify and reschedule future alarms
|
||||||
|
val schedules = try {
|
||||||
|
db.scheduleDao().getEnabled()
|
||||||
|
.filter { it.kind == "notify" }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to query schedules", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
var rescheduledCount = 0
|
||||||
|
var verifiedCount = 0
|
||||||
|
var rescheduleErrors = 0
|
||||||
|
|
||||||
|
schedules.forEach { schedule ->
|
||||||
|
try {
|
||||||
|
// Data integrity check: verify schedule is valid
|
||||||
|
if (schedule.id.isBlank() || schedule.nextRunAt == null) {
|
||||||
|
Log.w(TAG, "Skipping invalid schedule: ${schedule.id}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextRunTime = schedule.nextRunAt!!
|
||||||
|
|
||||||
|
// Only check future alarms
|
||||||
|
if (nextRunTime >= currentTime) {
|
||||||
|
// Verify alarm is scheduled
|
||||||
|
val isScheduled = NotifyReceiver.isAlarmScheduled(context, nextRunTime)
|
||||||
|
|
||||||
|
if (isScheduled) {
|
||||||
|
verifiedCount++
|
||||||
|
Log.d(TAG, "Verified scheduled alarm: ${schedule.id} at $nextRunTime")
|
||||||
|
} else {
|
||||||
|
// Reschedule missing alarm
|
||||||
|
rescheduleAlarm(schedule, nextRunTime, db)
|
||||||
|
rescheduledCount++
|
||||||
|
Log.i(TAG, "Rescheduled missing alarm: ${schedule.id} at $nextRunTime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
rescheduleErrors++
|
||||||
|
Log.e(TAG, "Failed to verify/reschedule: ${schedule.id}", e)
|
||||||
|
// Continue processing other schedules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Record recovery in history
|
||||||
|
val result = RecoveryResult(
|
||||||
|
missedCount = missedCount,
|
||||||
|
rescheduledCount = rescheduledCount,
|
||||||
|
verifiedCount = verifiedCount,
|
||||||
|
errors = missedErrors + rescheduleErrors
|
||||||
|
)
|
||||||
|
|
||||||
|
recordRecoveryHistory(db, "cold_start", result)
|
||||||
|
|
||||||
|
Log.i(TAG, "Cold start recovery complete: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class for recovery results
|
||||||
|
*/
|
||||||
|
private data class RecoveryResult(
|
||||||
|
val missedCount: Int,
|
||||||
|
val rescheduledCount: Int,
|
||||||
|
val verifiedCount: Int,
|
||||||
|
val errors: Int
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "missed=$missedCount, rescheduled=$rescheduledCount, verified=$verifiedCount, errors=$errors"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Helper Methods
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Reschedule an alarm
|
||||||
|
*
|
||||||
|
* Data integrity: Validates schedule before rescheduling
|
||||||
|
*/
|
||||||
|
private suspend fun rescheduleAlarm(
|
||||||
|
schedule: Schedule,
|
||||||
|
nextRunTime: Long,
|
||||||
|
db: DailyNotificationDatabase
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Use existing BootReceiver logic for calculating next run time
|
||||||
|
// For now, use schedule.nextRunAt directly
|
||||||
|
val config = UserNotificationConfig(
|
||||||
|
enabled = schedule.enabled,
|
||||||
|
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
||||||
|
title = "Daily Notification",
|
||||||
|
body = "Your daily update is ready",
|
||||||
|
sound = true,
|
||||||
|
vibration = true,
|
||||||
|
priority = "normal"
|
||||||
|
)
|
||||||
|
|
||||||
|
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
||||||
|
|
||||||
|
// Update schedule in database (best effort)
|
||||||
|
try {
|
||||||
|
db.scheduleDao().updateRunTimes(schedule.id, schedule.lastRunAt, nextRunTime)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to update schedule in database: ${schedule.id}", e)
|
||||||
|
// Don't fail rescheduling if DB update fails
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Rescheduled alarm: ${schedule.id} for $nextRunTime")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to reschedule alarm: ${schedule.id}", e)
|
||||||
|
throw e // Re-throw to be caught by caller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record recovery in history
|
||||||
|
*
|
||||||
|
* Rollback safety: If history recording fails, log warning but don't fail recovery
|
||||||
|
*/
|
||||||
|
private suspend fun recordRecoveryHistory(
|
||||||
|
db: DailyNotificationDatabase,
|
||||||
|
scenario: String,
|
||||||
|
result: RecoveryResult
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
db.historyDao().insert(
|
||||||
|
History(
|
||||||
|
refId = "recovery_${System.currentTimeMillis()}",
|
||||||
|
kind = "recovery",
|
||||||
|
occurredAt = System.currentTimeMillis(),
|
||||||
|
outcome = if (result.errors == 0) "success" else "partial",
|
||||||
|
diagJson = """
|
||||||
|
{
|
||||||
|
"scenario": "$scenario",
|
||||||
|
"missed_count": ${result.missedCount},
|
||||||
|
"rescheduled_count": ${result.rescheduledCount},
|
||||||
|
"verified_count": ${result.verifiedCount},
|
||||||
|
"errors": ${result.errors}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to record recovery history (non-fatal)", e)
|
||||||
|
// Don't throw - history recording failure shouldn't fail recovery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record recovery failure in history
|
||||||
|
*/
|
||||||
|
private suspend fun recordRecoveryFailure(e: Exception) {
|
||||||
|
try {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
db.historyDao().insert(
|
||||||
|
History(
|
||||||
|
refId = "recovery_failure_${System.currentTimeMillis()}",
|
||||||
|
kind = "recovery",
|
||||||
|
occurredAt = System.currentTimeMillis(),
|
||||||
|
outcome = "failure",
|
||||||
|
diagJson = """
|
||||||
|
{
|
||||||
|
"error": "${e.message}",
|
||||||
|
"error_type": "${e.javaClass.simpleName}"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (historyError: Exception) {
|
||||||
|
// Silently fail - we're already in error handling
|
||||||
|
Log.w(TAG, "Failed to record recovery failure", historyError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Integration: DailyNotificationPlugin
|
||||||
|
|
||||||
|
### 3.1 Update `load()` Method
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
|
||||||
|
|
||||||
|
**Location**: After database initialization (line 98)
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```kotlin
|
||||||
|
override fun load() {
|
||||||
|
super.load()
|
||||||
|
try {
|
||||||
|
if (context == null) {
|
||||||
|
Log.e(TAG, "Context is null, cannot initialize database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
Log.i(TAG, "Daily Notification Plugin loaded successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to initialize Daily Notification Plugin", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Updated Code**:
|
||||||
|
```kotlin
|
||||||
|
override fun load() {
|
||||||
|
super.load()
|
||||||
|
try {
|
||||||
|
if (context == null) {
|
||||||
|
Log.e(TAG, "Context is null, cannot initialize database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
Log.i(TAG, "Daily Notification Plugin loaded successfully")
|
||||||
|
|
||||||
|
// Phase 1: Perform app launch recovery (cold start only)
|
||||||
|
// Runs asynchronously, non-blocking, with timeout
|
||||||
|
val reactivationManager = ReactivationManager(context)
|
||||||
|
reactivationManager.performRecovery()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to initialize Daily Notification Plugin", e)
|
||||||
|
// Don't throw - allow plugin to load even if recovery fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Data Integrity Checks
|
||||||
|
|
||||||
|
### 4.1 Validation Rules
|
||||||
|
|
||||||
|
**Notification Validation**:
|
||||||
|
- ✅ `id` must not be blank
|
||||||
|
- ✅ `scheduled_time` must be valid timestamp
|
||||||
|
- ✅ `delivery_status` must be valid enum value
|
||||||
|
|
||||||
|
**Schedule Validation**:
|
||||||
|
- ✅ `id` must not be blank
|
||||||
|
- ✅ `kind` must be "notify" or "fetch"
|
||||||
|
- ✅ `nextRunAt` must be set for verification
|
||||||
|
- ✅ `enabled` must be true (filtered by DAO)
|
||||||
|
|
||||||
|
### 4.2 Orphaned Data Handling
|
||||||
|
|
||||||
|
**Orphaned Notifications** (no matching schedule):
|
||||||
|
- Log warning but don't fail recovery
|
||||||
|
- Mark as missed if past scheduled time
|
||||||
|
|
||||||
|
**Orphaned Schedules** (no matching notification content):
|
||||||
|
- Log warning but don't fail recovery
|
||||||
|
- Reschedule if future alarm is missing
|
||||||
|
|
||||||
|
**Mismatched Data**:
|
||||||
|
- If `NotificationContentEntity.scheduled_time` doesn't match `Schedule.nextRunAt`, use `scheduled_time` for missed detection
|
||||||
|
- Log warning for data inconsistency
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Rollback Safety
|
||||||
|
|
||||||
|
### 5.1 No-Crash Guarantee
|
||||||
|
|
||||||
|
**All recovery operations must:**
|
||||||
|
|
||||||
|
1. **Catch all exceptions** - Never propagate exceptions to app
|
||||||
|
2. **Log errors** - All failures logged with context
|
||||||
|
3. **Continue processing** - One failure doesn't stop recovery
|
||||||
|
4. **Timeout protection** - Recovery completes within 2 seconds or times out
|
||||||
|
5. **Best-effort updates** - Database failures don't prevent alarm rescheduling
|
||||||
|
|
||||||
|
### 5.2 Error Handling Strategy
|
||||||
|
|
||||||
|
| Error Type | Handling | Log Level |
|
||||||
|
|------------|----------|-----------|
|
||||||
|
| Database query failure | Return empty list, continue | ERROR |
|
||||||
|
| Invalid notification data | Skip notification, continue | WARN |
|
||||||
|
| Alarm reschedule failure | Log error, continue to next | ERROR |
|
||||||
|
| History recording failure | Log warning, don't fail | WARN |
|
||||||
|
| Timeout | Log timeout, abort recovery | WARN |
|
||||||
|
|
||||||
|
### 5.3 Fallback Behavior
|
||||||
|
|
||||||
|
**If recovery fails completely:**
|
||||||
|
- App continues normally
|
||||||
|
- No alarms are lost (existing alarms remain scheduled)
|
||||||
|
- User can manually trigger recovery via app restart
|
||||||
|
- Error logged in history table (if possible)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Callback Behavior (Phase 1 - Deferred)
|
||||||
|
|
||||||
|
**Phase 1 does NOT emit callbacks.** Callback behavior is deferred to Phase 2.
|
||||||
|
|
||||||
|
**Future callback contract** (for Phase 2):
|
||||||
|
|
||||||
|
| Event | Fired When | Payload | Guarantees |
|
||||||
|
|-------|------------|---------|------------|
|
||||||
|
| `missed_notification` | Missed notification detected | `{notificationId, scheduledTime, detectedAt}` | Fired once per missed notification |
|
||||||
|
| `recovery_complete` | Recovery finished | `{scenario, missedCount, rescheduledCount, errors}` | Fired once per recovery run |
|
||||||
|
|
||||||
|
**Implementation notes:**
|
||||||
|
- Callbacks will use Capacitor event system
|
||||||
|
- Events batched if multiple missed notifications detected
|
||||||
|
- Callbacks fire after database updates complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Versioning & Migration
|
||||||
|
|
||||||
|
### 7.1 Version Bump
|
||||||
|
|
||||||
|
**Plugin Version**: Increment patch version (e.g., `1.1.0` → `1.1.1`)
|
||||||
|
|
||||||
|
**Reason**: New feature (recovery), no breaking changes
|
||||||
|
|
||||||
|
### 7.2 Database Migration
|
||||||
|
|
||||||
|
**No database migration required** for Phase 1.
|
||||||
|
|
||||||
|
**Existing tables used:**
|
||||||
|
- `notification_content` - Already has `delivery_status` field
|
||||||
|
- `schedules` - Already has `nextRunAt` field
|
||||||
|
- `history` - Already supports recovery events
|
||||||
|
|
||||||
|
### 7.3 Backward Compatibility
|
||||||
|
|
||||||
|
**Phase 1 is backward compatible:**
|
||||||
|
- Existing alarms continue to work
|
||||||
|
- No schema changes
|
||||||
|
- Recovery is additive (doesn't break existing functionality)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Testing Requirements
|
||||||
|
|
||||||
|
### 8.1 Test 1: Cold Start Missed Detection
|
||||||
|
|
||||||
|
**Purpose**: Verify missed notifications are detected and marked.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule notification for 2 minutes in future
|
||||||
|
2. Kill app process: `adb shell am kill com.timesafari.dailynotification`
|
||||||
|
3. Wait 5 minutes (past scheduled time)
|
||||||
|
4. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity`
|
||||||
|
5. Check logs: `adb logcat -d | grep DNP-REACTIVATION`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Log shows "Cold start recovery: checking for missed notifications"
|
||||||
|
- ✅ Log shows "Marked missed notification: <id>"
|
||||||
|
- ✅ Database shows `delivery_status = 'missed'`
|
||||||
|
- ✅ History table has recovery entry
|
||||||
|
|
||||||
|
**Pass Criteria**: Missed notification detected and marked in database.
|
||||||
|
|
||||||
|
### 8.2 Test 2: Future Alarm Rescheduling
|
||||||
|
|
||||||
|
**Purpose**: Verify missing future alarms are rescheduled.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule notification for 10 minutes in future
|
||||||
|
2. Manually cancel alarm: `adb shell dumpsys alarm | grep timesafari` (note request code)
|
||||||
|
3. Launch app
|
||||||
|
4. Check logs: `adb logcat -d | grep DNP-REACTIVATION`
|
||||||
|
5. Verify alarm rescheduled: `adb shell dumpsys alarm | grep timesafari`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Log shows "Rescheduled missing alarm: <id>"
|
||||||
|
- ✅ AlarmManager shows rescheduled alarm
|
||||||
|
- ✅ No duplicate alarms created
|
||||||
|
|
||||||
|
**Pass Criteria**: Missing alarm rescheduled, no duplicates.
|
||||||
|
|
||||||
|
### 8.3 Test 3: Recovery Timeout
|
||||||
|
|
||||||
|
**Purpose**: Verify recovery times out gracefully.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Create large number of schedules (100+)
|
||||||
|
2. Launch app
|
||||||
|
3. Check logs for timeout
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Recovery completes within 2 seconds OR times out
|
||||||
|
- ✅ App doesn't crash
|
||||||
|
- ✅ Partial recovery logged if timeout occurs
|
||||||
|
|
||||||
|
**Pass Criteria**: Recovery doesn't block app launch.
|
||||||
|
|
||||||
|
### 8.4 Test 4: Invalid Data Handling
|
||||||
|
|
||||||
|
**Purpose**: Verify invalid data doesn't crash recovery.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Manually insert invalid notification (empty ID) into database
|
||||||
|
2. Launch app
|
||||||
|
3. Check logs
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Invalid notification skipped
|
||||||
|
- ✅ Warning logged
|
||||||
|
- ✅ Recovery continues normally
|
||||||
|
|
||||||
|
**Pass Criteria**: Invalid data handled gracefully.
|
||||||
|
|
||||||
|
### 8.4 Emulator Test Harness
|
||||||
|
|
||||||
|
The manual tests in §8.1–§8.3 are codified in the script `test-phase1.sh` in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test-apps/android-test-app/test-phase1.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
|
||||||
|
* ✅ Script implemented and polished
|
||||||
|
* ✅ Verified on Android Emulator (Pixel 8 API 34) on 27 November 2025
|
||||||
|
* ✅ Correctly recognizes both `verified>0` and `rescheduled>0` as PASS cases
|
||||||
|
* ✅ Treats `DELETE_FAILED_INTERNAL_ERROR` on uninstall as non-fatal
|
||||||
|
|
||||||
|
For regression testing, use `PHASE1-EMULATOR-TESTING.md` + `test-phase1.sh` as the canonical procedure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] Create `ReactivationManager.kt` file
|
||||||
|
- [ ] Implement `performRecovery()` with timeout
|
||||||
|
- [ ] Implement `performColdStartRecovery()`
|
||||||
|
- [ ] Implement missed notification detection
|
||||||
|
- [ ] Implement missed notification marking
|
||||||
|
- [ ] Implement future alarm verification
|
||||||
|
- [ ] Implement missing alarm rescheduling
|
||||||
|
- [ ] Add data integrity checks
|
||||||
|
- [ ] Add error handling (no-crash guarantee)
|
||||||
|
- [ ] Add recovery history recording
|
||||||
|
- [ ] Update `DailyNotificationPlugin.load()` to call recovery
|
||||||
|
- [ ] Test cold start missed detection
|
||||||
|
- [ ] Test future alarm rescheduling
|
||||||
|
- [ ] Test recovery timeout
|
||||||
|
- [ ] Test invalid data handling
|
||||||
|
- [ ] Verify no duplicate alarms
|
||||||
|
- [ ] Verify recovery doesn't block app launch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Code References
|
||||||
|
|
||||||
|
**Existing Code to Reuse**:
|
||||||
|
- `NotifyReceiver.scheduleExactNotification()` - Line 92
|
||||||
|
- `NotifyReceiver.isAlarmScheduled()` - Line 279
|
||||||
|
- `BootReceiver.calculateNextRunTime()` - Line 103 (for Phase 2)
|
||||||
|
- `NotificationContentDao.getNotificationsReadyForDelivery()` - Line 99
|
||||||
|
- `ScheduleDao.getEnabled()` - Line 298
|
||||||
|
|
||||||
|
**New Code to Create**:
|
||||||
|
- `ReactivationManager.kt` - New file (Phase 1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Success Criteria Summary
|
||||||
|
|
||||||
|
**Phase 1 is complete when:**
|
||||||
|
|
||||||
|
1. ✅ Missed notifications detected on cold start
|
||||||
|
2. ✅ Missed notifications marked in database
|
||||||
|
3. ✅ Future alarms verified and rescheduled if missing
|
||||||
|
4. ✅ Recovery never crashes app
|
||||||
|
5. ✅ Recovery completes within 2 seconds
|
||||||
|
6. ✅ All tests pass
|
||||||
|
7. ✅ No duplicate alarms created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) - Master coordination document
|
||||||
|
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this phase implements
|
||||||
|
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Plugin Behavior Exploration](./alarms/02-plugin-behavior-exploration.md) - Test scenarios
|
||||||
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope (all phases)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Incremental approach**: Phase 1 focuses on cold start only. Force stop and boot recovery in Phase 2.
|
||||||
|
- **Safety first**: All recovery operations are non-blocking and non-fatal.
|
||||||
|
- **Observability**: Extensive logging for debugging and monitoring.
|
||||||
|
- **Data integrity**: Validation prevents invalid data from causing failures.
|
||||||
|
|
||||||
787
docs/android-implementation-directive-phase2.md
Normal file
787
docs/android-implementation-directive-phase2.md
Normal file
@@ -0,0 +1,787 @@
|
|||||||
|
# Android Implementation Directive: Phase 2 - Force Stop Detection & Recovery
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Phase 2 - Force Stop Recovery
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Last Synced With Plugin Version**: v1.1.0
|
||||||
|
|
||||||
|
**Implements**: [Plugin Requirements §3.1.4 - Force Stop Recovery](./alarms/03-plugin-requirements.md#314-force-stop-recovery-android-only)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Phase 2 implements **force stop detection and comprehensive recovery**. This handles the scenario where the user force-stops the app, causing all alarms to be cancelled by the OS.
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT**: This phase **modifies and extends** the `ReactivationManager` introduced in Phase 1. Do not create a second copy; update the existing class.
|
||||||
|
|
||||||
|
**Prerequisites**: Phase 1 must be complete (cold start recovery implemented).
|
||||||
|
|
||||||
|
**Scope**: Force stop detection, scenario differentiation, and full alarm recovery.
|
||||||
|
|
||||||
|
**Reference**:
|
||||||
|
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this phase implements
|
||||||
|
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Phase 1](./android-implementation-directive-phase1.md) - Prerequisite
|
||||||
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Acceptance Criteria
|
||||||
|
|
||||||
|
### 1.1 Definition of Done
|
||||||
|
|
||||||
|
**Phase 2 is complete when:**
|
||||||
|
|
||||||
|
1. ✅ **Force stop scenario is detected correctly**
|
||||||
|
- Detection: `(DB schedules count > 0) && (AlarmManager alarms count == 0)`
|
||||||
|
- Detection runs on app launch (via `ReactivationManager`)
|
||||||
|
- False positives avoided (distinguishes from first launch)
|
||||||
|
|
||||||
|
2. ✅ **All past alarms are marked as missed**
|
||||||
|
- All schedules with `nextRunAt < currentTime` marked as missed
|
||||||
|
- Missed notifications created/updated in database
|
||||||
|
- History records created for each missed alarm
|
||||||
|
|
||||||
|
3. ✅ **All future alarms are rescheduled**
|
||||||
|
- All schedules with `nextRunAt >= currentTime` rescheduled
|
||||||
|
- Repeating schedules calculate next occurrence correctly
|
||||||
|
- No duplicate alarms created
|
||||||
|
|
||||||
|
4. ✅ **Recovery handles both notify and fetch schedules**
|
||||||
|
- `notify` schedules rescheduled via AlarmManager
|
||||||
|
- `fetch` schedules rescheduled via WorkManager
|
||||||
|
- Both types recovered completely
|
||||||
|
|
||||||
|
5. ✅ **Recovery never crashes the app**
|
||||||
|
- All exceptions caught and logged
|
||||||
|
- Partial recovery logged if some schedules fail
|
||||||
|
- App continues normally even if recovery fails
|
||||||
|
|
||||||
|
### 1.2 Success Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Measurement |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Force stop detection accuracy | 100% | Manual verification via logs |
|
||||||
|
| Past alarm recovery rate | 100% | All past alarms marked as missed |
|
||||||
|
| Future alarm recovery rate | > 95% | History table outcome field |
|
||||||
|
| Recovery execution time | < 3 seconds | Log timestamp difference |
|
||||||
|
| Crash rate | 0% | No exceptions propagate to app |
|
||||||
|
|
||||||
|
### 1.3 Out of Scope (Phase 2)
|
||||||
|
|
||||||
|
- ❌ Warm start optimization (Phase 3)
|
||||||
|
- ❌ Boot receiver missed alarm handling (Phase 3)
|
||||||
|
- ❌ Callback event emission (Phase 3)
|
||||||
|
- ❌ User notification of missed alarms (Phase 3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Implementation: Force Stop Detection
|
||||||
|
|
||||||
|
### 2.1 Update ReactivationManager
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||||
|
|
||||||
|
**Location**: Add scenario detection after Phase 1 implementation
|
||||||
|
|
||||||
|
### 2.2 Scenario Detection
|
||||||
|
|
||||||
|
**⚠️ Canonical Source**: This method supersedes any earlier scenario detection code shown in the full directive.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Detect recovery scenario based on AlarmManager state vs database
|
||||||
|
*
|
||||||
|
* Phase 2: Adds force stop detection
|
||||||
|
*
|
||||||
|
* This is the normative implementation of scenario detection.
|
||||||
|
*
|
||||||
|
* @return RecoveryScenario enum value
|
||||||
|
*/
|
||||||
|
private suspend fun detectScenario(): RecoveryScenario {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val dbSchedules = db.scheduleDao().getEnabled()
|
||||||
|
|
||||||
|
// Check for first launch (empty DB)
|
||||||
|
if (dbSchedules.isEmpty()) {
|
||||||
|
Log.d(TAG, "No schedules in database - first launch (NONE)")
|
||||||
|
return RecoveryScenario.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for boot recovery (set by BootReceiver)
|
||||||
|
if (isBootRecovery()) {
|
||||||
|
Log.i(TAG, "Boot recovery detected")
|
||||||
|
return RecoveryScenario.BOOT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for force stop: DB has schedules but no alarms exist
|
||||||
|
if (!alarmsExist()) {
|
||||||
|
Log.i(TAG, "Force stop detected: DB has ${dbSchedules.size} schedules, but no alarms exist")
|
||||||
|
return RecoveryScenario.FORCE_STOP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal cold start: DB has schedules and alarms exist
|
||||||
|
// (Alarms may have fired or may be future alarms - need to verify/resync)
|
||||||
|
Log.d(TAG, "Cold start: DB has ${dbSchedules.size} schedules, alarms exist")
|
||||||
|
return RecoveryScenario.COLD_START
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a boot recovery scenario
|
||||||
|
*
|
||||||
|
* BootReceiver sets a flag in SharedPreferences when boot completes.
|
||||||
|
* This allows ReactivationManager to detect boot scenario.
|
||||||
|
*
|
||||||
|
* @return true if boot recovery, false otherwise
|
||||||
|
*/
|
||||||
|
private fun isBootRecovery(): Boolean {
|
||||||
|
val prefs = context.getSharedPreferences("dailynotification_recovery", Context.MODE_PRIVATE)
|
||||||
|
val lastBootAt = prefs.getLong("last_boot_at", 0)
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Boot flag is valid for 60 seconds after boot
|
||||||
|
// This prevents false positives from stale flags
|
||||||
|
if (lastBootAt > 0 && (currentTime - lastBootAt) < 60000) {
|
||||||
|
// Clear the flag after reading
|
||||||
|
prefs.edit().remove("last_boot_at").apply()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if alarms exist in AlarmManager
|
||||||
|
*
|
||||||
|
* **Correction**: Replaces unreliable nextAlarmClock check with PendingIntent check.
|
||||||
|
* This eliminates false positives from nextAlarmClock.
|
||||||
|
*
|
||||||
|
* @return true if at least one alarm exists, false otherwise
|
||||||
|
*/
|
||||||
|
private fun alarmsExist(): Boolean {
|
||||||
|
return try {
|
||||||
|
// Check if any PendingIntent for our receiver exists
|
||||||
|
// This is more reliable than nextAlarmClock
|
||||||
|
val intent = Intent(context, DailyNotificationReceiver::class.java).apply {
|
||||||
|
action = "com.timesafari.daily.NOTIFICATION"
|
||||||
|
}
|
||||||
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
0, // Use 0 to check for any alarm
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val exists = pendingIntent != null
|
||||||
|
Log.d(TAG, "Alarm check: alarms exist = $exists")
|
||||||
|
exists
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error checking if alarms exist", e)
|
||||||
|
// On error, assume no alarms (conservative for force stop detection)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recovery scenario enum
|
||||||
|
*
|
||||||
|
* **Corrected Model**: Only these four scenarios are supported
|
||||||
|
*/
|
||||||
|
enum class RecoveryScenario {
|
||||||
|
COLD_START, // Process killed, alarms may or may not exist
|
||||||
|
FORCE_STOP, // Alarms cleared, DB still populated
|
||||||
|
BOOT, // Device reboot
|
||||||
|
NONE // No recovery required (warm resume or first launch)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Update performRecovery()
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Perform recovery on app launch
|
||||||
|
* Phase 2: Adds force stop handling
|
||||||
|
*/
|
||||||
|
fun performRecovery() {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
withTimeout(TimeUnit.SECONDS.toMillis(RECOVERY_TIMEOUT_SECONDS)) {
|
||||||
|
Log.i(TAG, "Starting app launch recovery (Phase 2)")
|
||||||
|
|
||||||
|
// Step 1: Detect scenario
|
||||||
|
val scenario = detectScenario()
|
||||||
|
Log.i(TAG, "Detected scenario: $scenario")
|
||||||
|
|
||||||
|
// Step 2: Handle based on scenario
|
||||||
|
when (scenario) {
|
||||||
|
RecoveryScenario.FORCE_STOP -> {
|
||||||
|
// Phase 2: Force stop recovery (new in this phase)
|
||||||
|
val result = performForceStopRecovery()
|
||||||
|
Log.i(TAG, "Force stop recovery completed: $result")
|
||||||
|
}
|
||||||
|
RecoveryScenario.COLD_START -> {
|
||||||
|
// Phase 1: Cold start recovery (reuse existing implementation)
|
||||||
|
val result = performColdStartRecovery()
|
||||||
|
Log.i(TAG, "Cold start recovery completed: $result")
|
||||||
|
}
|
||||||
|
RecoveryScenario.BOOT -> {
|
||||||
|
// Phase 3: Boot recovery (handled via ReactivationManager)
|
||||||
|
// Boot recovery uses same logic as force stop (all alarms wiped)
|
||||||
|
val result = performForceStopRecovery()
|
||||||
|
Log.i(TAG, "Boot recovery completed: $result")
|
||||||
|
}
|
||||||
|
RecoveryScenario.NONE -> {
|
||||||
|
// No recovery needed (warm resume or first launch)
|
||||||
|
Log.d(TAG, "No recovery needed (NONE scenario)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "App launch recovery completed")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Recovery failed (non-fatal): ${e.message}", e)
|
||||||
|
try {
|
||||||
|
recordRecoveryFailure(e)
|
||||||
|
} catch (historyError: Exception) {
|
||||||
|
Log.w(TAG, "Failed to record recovery failure in history", historyError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Implementation: Force Stop Recovery
|
||||||
|
|
||||||
|
### 3.1 Force Stop Recovery Method
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Perform force stop recovery
|
||||||
|
*
|
||||||
|
* Force stop scenario: ALL alarms were cancelled by OS
|
||||||
|
* Need to:
|
||||||
|
* 1. Mark all past alarms as missed
|
||||||
|
* 2. Reschedule all future alarms
|
||||||
|
* 3. Handle both notify and fetch schedules
|
||||||
|
*
|
||||||
|
* @return RecoveryResult with counts
|
||||||
|
*/
|
||||||
|
private suspend fun performForceStopRecovery(): RecoveryResult {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
Log.i(TAG, "Force stop recovery: recovering all schedules")
|
||||||
|
|
||||||
|
val dbSchedules = try {
|
||||||
|
db.scheduleDao().getEnabled()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to query schedules", e)
|
||||||
|
return RecoveryResult(0, 0, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var missedCount = 0
|
||||||
|
var rescheduledCount = 0
|
||||||
|
var errors = 0
|
||||||
|
|
||||||
|
dbSchedules.forEach { schedule ->
|
||||||
|
try {
|
||||||
|
when (schedule.kind) {
|
||||||
|
"notify" -> {
|
||||||
|
val result = recoverNotifySchedule(schedule, currentTime, db)
|
||||||
|
missedCount += result.missedCount
|
||||||
|
rescheduledCount += result.rescheduledCount
|
||||||
|
errors += result.errors
|
||||||
|
}
|
||||||
|
"fetch" -> {
|
||||||
|
val result = recoverFetchSchedule(schedule, currentTime, db)
|
||||||
|
rescheduledCount += result.rescheduledCount
|
||||||
|
errors += result.errors
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown schedule kind: ${schedule.kind}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to recover schedule: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = RecoveryResult(
|
||||||
|
missedCount = missedCount,
|
||||||
|
rescheduledCount = rescheduledCount,
|
||||||
|
verifiedCount = 0, // Not applicable for force stop
|
||||||
|
errors = errors
|
||||||
|
)
|
||||||
|
|
||||||
|
recordRecoveryHistory(db, "force_stop", result)
|
||||||
|
|
||||||
|
Log.i(TAG, "Force stop recovery complete: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class for schedule recovery results
|
||||||
|
*/
|
||||||
|
private data class ScheduleRecoveryResult(
|
||||||
|
val missedCount: Int = 0,
|
||||||
|
val rescheduledCount: Int = 0,
|
||||||
|
val errors: Int = 0
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Recover Notify Schedule
|
||||||
|
|
||||||
|
**Behavior**: Handles `kind == "notify"` schedules. Reschedules via AlarmManager.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Recover a notify schedule after force stop
|
||||||
|
*
|
||||||
|
* Handles notify schedules (kind == "notify")
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to recover
|
||||||
|
* @param currentTime Current time in milliseconds
|
||||||
|
* @param db Database instance
|
||||||
|
* @return ScheduleRecoveryResult
|
||||||
|
*/
|
||||||
|
private suspend fun recoverNotifySchedule(
|
||||||
|
schedule: Schedule,
|
||||||
|
currentTime: Long,
|
||||||
|
db: DailyNotificationDatabase
|
||||||
|
): ScheduleRecoveryResult {
|
||||||
|
|
||||||
|
// Data integrity check
|
||||||
|
if (schedule.id.isBlank()) {
|
||||||
|
Log.w(TAG, "Skipping invalid schedule: empty ID")
|
||||||
|
return ScheduleRecoveryResult(errors = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var missedCount = 0
|
||||||
|
var rescheduledCount = 0
|
||||||
|
var errors = 0
|
||||||
|
|
||||||
|
// Calculate next run time
|
||||||
|
val nextRunTime = calculateNextRunTime(schedule, currentTime)
|
||||||
|
|
||||||
|
if (nextRunTime < currentTime) {
|
||||||
|
// Past alarm - was missed during force stop
|
||||||
|
Log.i(TAG, "Past alarm detected: ${schedule.id} scheduled for $nextRunTime")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mark as missed
|
||||||
|
markMissedNotification(schedule, nextRunTime, db)
|
||||||
|
missedCount++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to mark missed notification: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reschedule next occurrence if repeating
|
||||||
|
if (isRepeating(schedule)) {
|
||||||
|
try {
|
||||||
|
val nextOccurrence = calculateNextOccurrence(schedule, currentTime)
|
||||||
|
rescheduleAlarm(schedule, nextOccurrence, db)
|
||||||
|
rescheduledCount++
|
||||||
|
Log.i(TAG, "Rescheduled next occurrence: ${schedule.id} for $nextOccurrence")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to reschedule next occurrence: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Future alarm - reschedule immediately
|
||||||
|
Log.i(TAG, "Future alarm detected: ${schedule.id} scheduled for $nextRunTime")
|
||||||
|
|
||||||
|
try {
|
||||||
|
rescheduleAlarm(schedule, nextRunTime, db)
|
||||||
|
rescheduledCount++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to reschedule future alarm: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScheduleRecoveryResult(missedCount, rescheduledCount, errors)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Recover Fetch Schedule
|
||||||
|
|
||||||
|
**Behavior**: Handles `kind == "fetch"` schedules. Reschedules via WorkManager.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Recover a fetch schedule after force stop
|
||||||
|
*
|
||||||
|
* Handles fetch schedules (kind == "fetch")
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to recover
|
||||||
|
* @param currentTime Current time in milliseconds
|
||||||
|
* @param db Database instance
|
||||||
|
* @return ScheduleRecoveryResult
|
||||||
|
*/
|
||||||
|
private suspend fun recoverFetchSchedule(
|
||||||
|
schedule: Schedule,
|
||||||
|
currentTime: Long,
|
||||||
|
db: DailyNotificationDatabase
|
||||||
|
): ScheduleRecoveryResult {
|
||||||
|
|
||||||
|
// Data integrity check
|
||||||
|
if (schedule.id.isBlank()) {
|
||||||
|
Log.w(TAG, "Skipping invalid schedule: empty ID")
|
||||||
|
return ScheduleRecoveryResult(errors = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rescheduledCount = 0
|
||||||
|
var errors = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reschedule fetch work via WorkManager
|
||||||
|
val config = ContentFetchConfig(
|
||||||
|
enabled = schedule.enabled,
|
||||||
|
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
||||||
|
url = null, // Will use registered native fetcher
|
||||||
|
timeout = 30000,
|
||||||
|
retryAttempts = 3,
|
||||||
|
retryDelay = 1000,
|
||||||
|
callbacks = CallbackConfig()
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchWorker.scheduleFetch(context, config)
|
||||||
|
rescheduledCount++
|
||||||
|
|
||||||
|
Log.i(TAG, "Rescheduled fetch: ${schedule.id}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to reschedule fetch: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScheduleRecoveryResult(rescheduledCount = rescheduledCount, errors = errors)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Helper Methods
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Mark a notification as missed
|
||||||
|
*
|
||||||
|
* @param schedule Schedule that was missed
|
||||||
|
* @param scheduledTime When the notification was scheduled
|
||||||
|
* @param db Database instance
|
||||||
|
*/
|
||||||
|
private suspend fun markMissedNotification(
|
||||||
|
schedule: Schedule,
|
||||||
|
scheduledTime: Long,
|
||||||
|
db: DailyNotificationDatabase
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Try to find existing NotificationContentEntity
|
||||||
|
val notificationId = schedule.id
|
||||||
|
val existingNotification = db.notificationContentDao().getNotificationById(notificationId)
|
||||||
|
|
||||||
|
if (existingNotification != null) {
|
||||||
|
// Update existing notification
|
||||||
|
existingNotification.deliveryStatus = "missed"
|
||||||
|
existingNotification.lastDeliveryAttempt = System.currentTimeMillis()
|
||||||
|
existingNotification.deliveryAttempts = (existingNotification.deliveryAttempts ?: 0) + 1
|
||||||
|
db.notificationContentDao().updateNotification(existingNotification)
|
||||||
|
Log.d(TAG, "Updated existing notification as missed: $notificationId")
|
||||||
|
} else {
|
||||||
|
// Create missed notification entry
|
||||||
|
// Note: This may not have full content, but marks the missed event
|
||||||
|
Log.w(TAG, "No NotificationContentEntity found for schedule: $notificationId")
|
||||||
|
// Could create a minimal entry here if needed
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to mark missed notification: ${schedule.id}", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate next run time from schedule
|
||||||
|
* Uses existing BootReceiver logic if available
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to calculate for
|
||||||
|
* @param currentTime Current time in milliseconds
|
||||||
|
* @return Next run time in milliseconds
|
||||||
|
*/
|
||||||
|
private fun calculateNextRunTime(schedule: Schedule, currentTime: Long): Long {
|
||||||
|
// Prefer nextRunAt if set
|
||||||
|
if (schedule.nextRunAt != null) {
|
||||||
|
return schedule.nextRunAt!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate from cron or clockTime
|
||||||
|
// For now, simplified: use BootReceiver logic if available
|
||||||
|
// Otherwise, default to next day at 9 AM
|
||||||
|
return when {
|
||||||
|
schedule.cron != null -> {
|
||||||
|
// TODO: Parse cron and calculate next run
|
||||||
|
// For now, return next day at 9 AM
|
||||||
|
currentTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
schedule.clockTime != null -> {
|
||||||
|
// TODO: Parse HH:mm and calculate next run
|
||||||
|
// For now, return next day at specified time
|
||||||
|
currentTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Default to next day at 9 AM
|
||||||
|
currentTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if schedule is repeating
|
||||||
|
*
|
||||||
|
* **Helper Consistency Note**: This helper must remain consistent with any
|
||||||
|
* equivalent methods used in `BootReceiver` (Phase 3). If updated, update both places.
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to check
|
||||||
|
* @return true if repeating, false if one-time
|
||||||
|
*/
|
||||||
|
private fun isRepeating(schedule: Schedule): Boolean {
|
||||||
|
// Schedules with cron or clockTime are repeating
|
||||||
|
return schedule.cron != null || schedule.clockTime != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate next occurrence for repeating schedule
|
||||||
|
*
|
||||||
|
* **Helper Consistency Note**: This helper must remain consistent with any
|
||||||
|
* equivalent methods used in `BootReceiver` (Phase 3). If updated, update both places.
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to calculate for
|
||||||
|
* @param fromTime Calculate next occurrence after this time
|
||||||
|
* @return Next occurrence time in milliseconds
|
||||||
|
*/
|
||||||
|
private fun calculateNextOccurrence(schedule: Schedule, fromTime: Long): Long {
|
||||||
|
// TODO: Implement proper calculation based on cron/clockTime
|
||||||
|
// For now, simplified: daily schedules add 24 hours
|
||||||
|
return fromTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Data Integrity Checks
|
||||||
|
|
||||||
|
### 4.1 Force Stop Detection Validation
|
||||||
|
|
||||||
|
**False Positive Prevention**:
|
||||||
|
- ✅ First launch: `DB schedules count == 0` → Not force stop
|
||||||
|
- ✅ Normal cold start: `AlarmManager has alarms` → Not force stop
|
||||||
|
- ✅ Only detect force stop when: `DB schedules > 0 && AlarmManager alarms == 0`
|
||||||
|
|
||||||
|
**Edge Cases**:
|
||||||
|
- ✅ All alarms already fired: Still detect as force stop if AlarmManager is empty
|
||||||
|
- ✅ Partial alarm cancellation: Not detected as force stop (handled by cold start recovery)
|
||||||
|
|
||||||
|
### 4.2 Schedule Validation
|
||||||
|
|
||||||
|
**Notify Schedule Validation**:
|
||||||
|
- ✅ `id` must not be blank
|
||||||
|
- ✅ `kind` must be "notify"
|
||||||
|
- ✅ `nextRunAt` or `cron`/`clockTime` must be set
|
||||||
|
|
||||||
|
**Fetch Schedule Validation**:
|
||||||
|
- ✅ `id` must not be blank
|
||||||
|
- ✅ `kind` must be "fetch"
|
||||||
|
- ✅ `cron` or `clockTime` must be set
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Rollback Safety
|
||||||
|
|
||||||
|
### 5.1 No-Crash Guarantee
|
||||||
|
|
||||||
|
**All force stop recovery operations must:**
|
||||||
|
|
||||||
|
1. **Catch all exceptions** - Never propagate exceptions to app
|
||||||
|
2. **Continue processing** - One schedule failure doesn't stop recovery
|
||||||
|
3. **Log errors** - All failures logged with context
|
||||||
|
4. **Partial recovery** - Some schedules can recover even if others fail
|
||||||
|
|
||||||
|
### 5.2 Error Handling Strategy
|
||||||
|
|
||||||
|
| Error Type | Handling | Log Level |
|
||||||
|
|------------|----------|-----------|
|
||||||
|
| Schedule query failure | Return empty result, log error | ERROR |
|
||||||
|
| Invalid schedule data | Skip schedule, continue | WARN |
|
||||||
|
| Alarm reschedule failure | Log error, continue to next | ERROR |
|
||||||
|
| Fetch reschedule failure | Log error, continue to next | ERROR |
|
||||||
|
| Missed notification marking failure | Log error, continue | ERROR |
|
||||||
|
| History recording failure | Log warning, don't fail | WARN |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Testing Requirements
|
||||||
|
|
||||||
|
### 6.1 Test 1: Force Stop Detection
|
||||||
|
|
||||||
|
**Purpose**: Verify force stop scenario is detected correctly.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule 3 notifications (2 minutes, 5 minutes, 10 minutes in future)
|
||||||
|
2. Verify alarms scheduled: `adb shell dumpsys alarm | grep timesafari`
|
||||||
|
3. Force stop app: `adb shell am force-stop com.timesafari.dailynotification`
|
||||||
|
4. Verify alarms cancelled: `adb shell dumpsys alarm | grep timesafari` (should be empty)
|
||||||
|
5. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity`
|
||||||
|
6. Check logs: `adb logcat -d | grep DNP-REACTIVATION`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Log shows "Force stop detected: DB has X schedules, AlarmManager has 0 alarms"
|
||||||
|
- ✅ Log shows "Detected scenario: FORCE_STOP"
|
||||||
|
- ✅ Log shows "Force stop recovery: recovering all schedules"
|
||||||
|
|
||||||
|
**Pass Criteria**: Force stop correctly detected.
|
||||||
|
|
||||||
|
### 6.2 Test 2: Past Alarm Recovery
|
||||||
|
|
||||||
|
**Purpose**: Verify past alarms are marked as missed.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule notification for 2 minutes in future
|
||||||
|
2. Force stop app
|
||||||
|
3. Wait 5 minutes (past scheduled time)
|
||||||
|
4. Launch app
|
||||||
|
5. Check database: `delivery_status = 'missed'` for past alarm
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Past alarm marked as missed in database
|
||||||
|
- ✅ History entry created
|
||||||
|
- ✅ Log shows "Past alarm detected" and "Marked missed notification"
|
||||||
|
|
||||||
|
**Pass Criteria**: Past alarms correctly marked as missed.
|
||||||
|
|
||||||
|
### 6.3 Test 3: Future Alarm Recovery
|
||||||
|
|
||||||
|
**Purpose**: Verify future alarms are rescheduled.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule 3 notifications (5, 10, 15 minutes in future)
|
||||||
|
2. Force stop app
|
||||||
|
3. Launch app immediately
|
||||||
|
4. Verify alarms rescheduled: `adb shell dumpsys alarm | grep timesafari`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ All 3 alarms rescheduled in AlarmManager
|
||||||
|
- ✅ Log shows "Future alarm detected" and "Rescheduled alarm"
|
||||||
|
- ✅ No duplicate alarms created
|
||||||
|
|
||||||
|
**Pass Criteria**: Future alarms correctly rescheduled.
|
||||||
|
|
||||||
|
### 6.4 Test 4: Repeating Schedule Recovery
|
||||||
|
|
||||||
|
**Purpose**: Verify repeating schedules calculate next occurrence correctly.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule daily notification (cron: "0 9 * * *")
|
||||||
|
2. Force stop app
|
||||||
|
3. Wait past scheduled time (e.g., wait until 10 AM)
|
||||||
|
4. Launch app
|
||||||
|
5. Verify next occurrence scheduled for tomorrow 9 AM
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Past occurrence marked as missed
|
||||||
|
- ✅ Next occurrence scheduled for tomorrow
|
||||||
|
- ✅ Log shows "Rescheduled next occurrence"
|
||||||
|
|
||||||
|
**Pass Criteria**: Repeating schedules correctly calculate next occurrence.
|
||||||
|
|
||||||
|
### 6.5 Test 5: Fetch Schedule Recovery
|
||||||
|
|
||||||
|
**Purpose**: Verify fetch schedules are recovered.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule fetch work (cron: "0 9 * * *")
|
||||||
|
2. Force stop app
|
||||||
|
3. Launch app
|
||||||
|
4. Check WorkManager: `adb shell dumpsys jobscheduler | grep timesafari`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Fetch work rescheduled in WorkManager
|
||||||
|
- ✅ Log shows "Rescheduled fetch"
|
||||||
|
|
||||||
|
**Pass Criteria**: Fetch schedules correctly recovered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] Add `detectScenario()` method to ReactivationManager
|
||||||
|
- [ ] Add `alarmsExist()` method (replaces getActiveAlarmCount)
|
||||||
|
- [ ] Add `isBootRecovery()` method
|
||||||
|
- [ ] Add `RecoveryScenario` enum
|
||||||
|
- [ ] Update `performRecovery()` to handle force stop
|
||||||
|
- [ ] Implement `performForceStopRecovery()`
|
||||||
|
- [ ] Implement `recoverNotifySchedule()`
|
||||||
|
- [ ] Implement `recoverFetchSchedule()`
|
||||||
|
- [ ] Implement `markMissedNotification()`
|
||||||
|
- [ ] Implement `calculateNextRunTime()` (or reuse BootReceiver logic)
|
||||||
|
- [ ] Implement `isRepeating()`
|
||||||
|
- [ ] Implement `calculateNextOccurrence()`
|
||||||
|
- [ ] Add data integrity checks
|
||||||
|
- [ ] Add error handling
|
||||||
|
- [ ] Test force stop detection
|
||||||
|
- [ ] Test past alarm recovery
|
||||||
|
- [ ] Test future alarm recovery
|
||||||
|
- [ ] Test repeating schedule recovery
|
||||||
|
- [ ] Test fetch schedule recovery
|
||||||
|
- [ ] Verify no duplicate alarms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Code References
|
||||||
|
|
||||||
|
**Existing Code to Reuse**:
|
||||||
|
- `NotifyReceiver.scheduleExactNotification()` - Line 92
|
||||||
|
- `FetchWorker.scheduleFetch()` - Line 31
|
||||||
|
- `BootReceiver.calculateNextRunTime()` - Line 103 (for next run calculation)
|
||||||
|
- `ScheduleDao.getEnabled()` - Line 298
|
||||||
|
- `NotificationContentDao.getNotificationById()` - Line 69
|
||||||
|
|
||||||
|
**New Code to Create**:
|
||||||
|
- `detectScenario()` - Add to ReactivationManager
|
||||||
|
- `alarmsExist()` - Add to ReactivationManager (replaces getActiveAlarmCount)
|
||||||
|
- `isBootRecovery()` - Add to ReactivationManager
|
||||||
|
- `performForceStopRecovery()` - Add to ReactivationManager
|
||||||
|
- `recoverNotifySchedule()` - Add to ReactivationManager
|
||||||
|
- `recoverFetchSchedule()` - Add to ReactivationManager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Success Criteria Summary
|
||||||
|
|
||||||
|
**Phase 2 is complete when:**
|
||||||
|
|
||||||
|
1. ✅ Force stop scenario detected correctly
|
||||||
|
2. ✅ All past alarms marked as missed
|
||||||
|
3. ✅ All future alarms rescheduled
|
||||||
|
4. ✅ Both notify and fetch schedules recovered
|
||||||
|
5. ✅ Repeating schedules calculate next occurrence correctly
|
||||||
|
6. ✅ Recovery never crashes app
|
||||||
|
7. ✅ All tests pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Phase 1: Cold Start Recovery](./android-implementation-directive-phase1.md) - Prerequisite
|
||||||
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope
|
||||||
|
- [Exploration Findings](./exploration-findings-initial.md) - Gap analysis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Prerequisite**: Phase 1 must be complete before starting Phase 2
|
||||||
|
- **Detection accuracy**: Force stop detection uses best available method (nextAlarmClock)
|
||||||
|
- **Comprehensive recovery**: Force stop recovery handles ALL schedules (past and future)
|
||||||
|
- **Safety first**: All recovery operations are non-blocking and non-fatal
|
||||||
|
|
||||||
221
docs/android-implementation-directive-phase3.md
Normal file
221
docs/android-implementation-directive-phase3.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# Android Implementation Directive – Phase 3
|
||||||
|
## Boot-Time Recovery (Device Reboot / System Restart)
|
||||||
|
|
||||||
|
**Plugin:** Daily Notification Plugin
|
||||||
|
**Author:** Matthew Raymer
|
||||||
|
**Applies to:** Android Plugin (Kotlin), Capacitor Bridge
|
||||||
|
**Related Docs:**
|
||||||
|
- `03-plugin-requirements.md`
|
||||||
|
- `000-UNIFIED-ALARM-DIRECTIVE.md`
|
||||||
|
- `android-implementation-directive-phase1.md`
|
||||||
|
- `android-implementation-directive-phase2.md`
|
||||||
|
- `ACTIVATION-GUIDE.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
Phase 3 introduces **Boot-Time Recovery**, which restores daily notifications after:
|
||||||
|
|
||||||
|
- Device reboot
|
||||||
|
- OS restart
|
||||||
|
- Update-related restart
|
||||||
|
- App not opened after reboot (silent recovery)
|
||||||
|
|
||||||
|
Android clears **all alarms** on reboot.
|
||||||
|
Therefore, if our plugin is not actively rescheduling on boot, the user will miss all daily notifications until they manually launch the app.
|
||||||
|
|
||||||
|
Phase 3 ensures:
|
||||||
|
|
||||||
|
1. Schedules stored in SQLite survive reboot
|
||||||
|
2. Alarms are fully reconstructed
|
||||||
|
3. No duplication / double-scheduling
|
||||||
|
4. Boot behavior avoids unnecessary heavy recovery
|
||||||
|
5. Recovery occurs even if the user does **not** manually open the app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Boot-Time Recovery Flow
|
||||||
|
|
||||||
|
### Trigger:
|
||||||
|
|
||||||
|
`BOOT_COMPLETED` broadcast received
|
||||||
|
→ Plugin's Boot Receiver invoked
|
||||||
|
→ Recovery logic executed with `scenario=BOOT`
|
||||||
|
|
||||||
|
### Recovery Steps
|
||||||
|
|
||||||
|
1. **Load all schedules** from SQLite (`NotificationRepository.getAllSchedules()`)
|
||||||
|
|
||||||
|
2. **For each schedule:**
|
||||||
|
- Calculate next runtime based on cron expression
|
||||||
|
- Compare with current time
|
||||||
|
|
||||||
|
3. **If the next scheduled time is in the future:**
|
||||||
|
- Recreate alarm with `setAlarmClock`
|
||||||
|
- Log:
|
||||||
|
`Rescheduled alarm: <id> for <ts>`
|
||||||
|
|
||||||
|
4. **If schedule was *in the past* at boot time:**
|
||||||
|
- Mark as missed
|
||||||
|
- Schedule next run according to cron rules
|
||||||
|
|
||||||
|
5. **If no schedules found:**
|
||||||
|
- Quiet exit, log only one line:
|
||||||
|
`BOOT: No schedules found`
|
||||||
|
|
||||||
|
6. **Safeties:**
|
||||||
|
- Boot recovery must **not** modify Plugin Settings
|
||||||
|
- Must not regenerate Fetcher configuration
|
||||||
|
- Must not overwrite database records
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Required Android Components
|
||||||
|
|
||||||
|
### 3.1 Boot Receiver
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<receiver
|
||||||
|
android:name=".BootReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Kotlin Class
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
if (intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||||
|
|
||||||
|
ReactivationManager.runBootRecovery(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. ReactivationManager – Boot Logic
|
||||||
|
|
||||||
|
### Method Signature
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun runBootRecovery(context: Context)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Logging (canonical)
|
||||||
|
|
||||||
|
```
|
||||||
|
DNP-REACTIVATION: Starting boot recovery
|
||||||
|
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: <id> for <ts>
|
||||||
|
DNP-REACTIVATION: Marked missed notification: <id>
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed=X, rescheduled=Y, errors=Z
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Fields
|
||||||
|
|
||||||
|
* `scenario=BOOT`
|
||||||
|
* `missed`
|
||||||
|
* `rescheduled`
|
||||||
|
* `verified` **MUST BE 0** (boot has no verification phase)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Constraints & Guardrails
|
||||||
|
|
||||||
|
1. **No plugin initialization**
|
||||||
|
Boot must *not* require running the app UI.
|
||||||
|
|
||||||
|
2. **No heavy processing**
|
||||||
|
* limit to 2 seconds
|
||||||
|
* use the same timeout guard as Phase 2
|
||||||
|
|
||||||
|
3. **No scheduling duplicates**
|
||||||
|
* Must detect existing AlarmManager entries
|
||||||
|
* Boot always clears them, so all reschedules should be fresh
|
||||||
|
|
||||||
|
4. **App does not need to be opened**
|
||||||
|
* Entire recovery must run in background context
|
||||||
|
|
||||||
|
5. **Idempotency**
|
||||||
|
* Running twice should produce identical logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Implementation Checklist
|
||||||
|
|
||||||
|
### Mandatory
|
||||||
|
|
||||||
|
* [ ] BootReceiver included
|
||||||
|
* [ ] Manifest entry added
|
||||||
|
* [ ] `runBootRecovery()` implemented
|
||||||
|
* [ ] Scenario logged as `BOOT`
|
||||||
|
* [ ] All alarms recreated
|
||||||
|
* [ ] Timeout protection
|
||||||
|
* [ ] No modifications to preferences or plugin settings
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
|
||||||
|
* [ ] Additional telemetry for analytics
|
||||||
|
* [ ] Optional debug toast for dev builds only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Expected Output Examples
|
||||||
|
|
||||||
|
### Example 1 – Normal Boot (future alarms exist)
|
||||||
|
|
||||||
|
```
|
||||||
|
DNP-REACTIVATION: Starting boot recovery
|
||||||
|
DNP-REACTIVATION: Loaded 2 schedules from DB
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_1764233911265 for 1764236120000
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_1764233465343 for 1764233700000
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=2, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2 – Schedules present but some in past
|
||||||
|
|
||||||
|
```
|
||||||
|
Marked missed notification: daily_1764233300000
|
||||||
|
Rescheduled alarm: daily_1764233300000 for next day
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3 – No schedules
|
||||||
|
|
||||||
|
```
|
||||||
|
DNP-REACTIVATION: BOOT: No schedules found
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Status
|
||||||
|
|
||||||
|
| Item | Status |
|
||||||
|
| -------------------- | -------------------------------- |
|
||||||
|
| Directive | **Complete** |
|
||||||
|
| Implementation | ☐ Pending / ✅ **Complete** (plugin v1.2+) |
|
||||||
|
| Emulator Test Script | Ready (`test-phase3.sh`) |
|
||||||
|
| Verification Doc | Ready (`PHASE3-VERIFICATION.md`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Related Documentation
|
||||||
|
|
||||||
|
- [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) - Master coordination document
|
||||||
|
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this phase implements
|
||||||
|
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Phase 1](./android-implementation-directive-phase1.md) - Prerequisite
|
||||||
|
- [Phase 2](./android-implementation-directive-phase2.md) - Prerequisite
|
||||||
|
- [Phase 3 Emulator Testing](./alarms/PHASE3-EMULATOR-TESTING.md) - Test procedures
|
||||||
|
- [Phase 3 Verification](./alarms/PHASE3-VERIFICATION.md) - Verification report
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Directive complete, ready for implementation
|
||||||
|
**Last Updated**: November 2025
|
||||||
1503
docs/android-implementation-directive.md
Normal file
1503
docs/android-implementation-directive.md
Normal file
File diff suppressed because it is too large
Load Diff
669
docs/exploration-findings-initial.md
Normal file
669
docs/exploration-findings-initial.md
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
# Plugin Behavior Exploration - Initial Findings
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Initial Code Review - In Progress
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document contains initial findings from code-level inspection of the plugin. These findings should be verified through actual testing using [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Behavior Definitions & Investigation Scope
|
||||||
|
|
||||||
|
Before examining the code, we need to clearly define what behaviors we're investigating and what each scenario means.
|
||||||
|
|
||||||
|
### 0.1 App State Scenarios
|
||||||
|
|
||||||
|
#### Swipe from Recents (Recent Apps List)
|
||||||
|
|
||||||
|
**What it is**: User swipes the app away from the Android recent apps list (app switcher) or iOS app switcher.
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
- **Android**: The app's UI is removed from the recent apps list, but:
|
||||||
|
- The app process may still be running in the background
|
||||||
|
- The app may be killed by the OS later due to memory pressure
|
||||||
|
- **AlarmManager alarms remain scheduled** and will fire even if the process is killed
|
||||||
|
- The OS will recreate the app process when the alarm fires
|
||||||
|
- **iOS**: The app is terminated, but:
|
||||||
|
- **UNUserNotificationCenter notifications remain scheduled** and will fire
|
||||||
|
- **Calendar/time-based triggers persist across reboot**
|
||||||
|
- **TimeInterval triggers also persist across reboot** (UNLESS they were scheduled with `repeats = false` AND the reboot occurs before the elapsed interval)
|
||||||
|
- The app does not run in the background (unless it has active background tasks)
|
||||||
|
- Notifications fire even though the app is not running
|
||||||
|
- **No plugin code runs when notification fires** unless the user interacts with the notification
|
||||||
|
|
||||||
|
**Key Point**: Swiping from recents does **not** cancel scheduled alarms/notifications. The OS maintains them separately from the app process.
|
||||||
|
|
||||||
|
**Android Nuance - Swipe vs Kill**:
|
||||||
|
- **"Swipe away" DOES NOT kill your process**; the OS may kill it later due to memory pressure
|
||||||
|
- **AlarmManager remains unaffected** by swipe - alarms stay scheduled
|
||||||
|
- **WorkManager tasks remain scheduled** regardless of swipe
|
||||||
|
- The app process may continue running in the background after swipe
|
||||||
|
- Only Force Stop actually cancels alarms and prevents execution
|
||||||
|
|
||||||
|
**Investigation Goal**: Verify that alarms/notifications still fire after the app is swiped away.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Force Stop (Android Only)
|
||||||
|
|
||||||
|
**What it is**: User goes to Settings → Apps → [Your App] → Force Stop. This is a **hard kill** that is different from swiping from recents.
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
- **All alarms are immediately cancelled** by the OS
|
||||||
|
- **All WorkManager tasks are cancelled**
|
||||||
|
- **All broadcast receivers are blocked** (including BOOT_COMPLETED)
|
||||||
|
- **All JobScheduler jobs are cancelled**
|
||||||
|
- **The app cannot run** until the user manually opens it again
|
||||||
|
- **No background execution** is possible
|
||||||
|
|
||||||
|
**Key Point**: Force Stop is a **hard boundary** that cannot be bypassed. It's more severe than swiping from recents.
|
||||||
|
|
||||||
|
**Investigation Goal**: Verify that alarms do NOT fire after force stop, and that the plugin can detect and recover when the app is opened again.
|
||||||
|
|
||||||
|
**Difference from Swipe**:
|
||||||
|
- **Swipe**: Alarms remain scheduled, app may still run in background
|
||||||
|
- **Force Stop**: Alarms are cancelled, app cannot run until manually opened
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### App Still Functioning When Not Visible
|
||||||
|
|
||||||
|
**Android**:
|
||||||
|
- When an app is swiped from recents but not force-stopped:
|
||||||
|
- The app process may continue running in the background
|
||||||
|
- Background services can continue
|
||||||
|
- WorkManager tasks continue
|
||||||
|
- AlarmManager alarms remain scheduled
|
||||||
|
- The app is just not visible in the recent apps list
|
||||||
|
- The OS may kill the process later due to memory pressure, but alarms remain scheduled
|
||||||
|
|
||||||
|
**iOS**:
|
||||||
|
- When an app is swiped from the app switcher:
|
||||||
|
- The app process is terminated
|
||||||
|
- Background tasks (BGTaskScheduler) may still execute (system-controlled, but **opportunistic, not exact**)
|
||||||
|
- UNUserNotificationCenter notifications remain scheduled
|
||||||
|
- The app does not run in the foreground or background (unless it has active background tasks)
|
||||||
|
- **No persistent background execution** after user swipe
|
||||||
|
- **No alarm-like wake** for plugins (unlike Android AlarmManager)
|
||||||
|
- **No background execution at notification time** unless user interacts
|
||||||
|
|
||||||
|
**iOS Limitations**:
|
||||||
|
- No background execution at notification fire time unless user interacts
|
||||||
|
- No alarm-style wakeups exist on iOS
|
||||||
|
- Background execution (BGTaskScheduler) cannot be used for precise timing
|
||||||
|
- Notifications survive reboot but plugin code does not run automatically
|
||||||
|
|
||||||
|
**Investigation Goal**: Understand that "not visible" does not mean "not functioning" for alarms/notifications, but also understand iOS limitations on background execution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 0.2 App Launch Recovery - How It Should Work
|
||||||
|
|
||||||
|
App Launch Recovery is the mechanism by which the plugin detects and handles missed alarms/notifications when the app starts.
|
||||||
|
|
||||||
|
#### Recovery Scenarios
|
||||||
|
|
||||||
|
##### Cold Start
|
||||||
|
|
||||||
|
**What it is**: App is launched from a completely terminated state (process was killed or never started).
|
||||||
|
|
||||||
|
**Recovery Process**:
|
||||||
|
1. Plugin's `load()` method is called
|
||||||
|
2. Plugin initializes database/storage
|
||||||
|
3. Plugin queries for missed alarms/notifications:
|
||||||
|
- Find alarms with `scheduled_time < now` and `delivery_status != 'delivered'`
|
||||||
|
- Find notifications that should have fired but didn't
|
||||||
|
4. For each missed alarm/notification:
|
||||||
|
- Generate a "missed alarm" event or notification
|
||||||
|
- If repeating, reschedule the next occurrence
|
||||||
|
- Update delivery status to "missed" or "delivered"
|
||||||
|
5. Reschedule future alarms/notifications that are still valid
|
||||||
|
6. Verify active alarms match stored alarms
|
||||||
|
|
||||||
|
**Investigation Goal**: Verify that the plugin detects missed alarms on cold start and handles them appropriately.
|
||||||
|
|
||||||
|
**Android Force Stop Detection**:
|
||||||
|
- On cold start, query AlarmManager for active alarms
|
||||||
|
- Query plugin DB schedules
|
||||||
|
- If `(DB.count > 0 && AlarmManager.count == 0)`: **Force Stop detected**
|
||||||
|
- Recovery: Mark all past schedules as missed, reschedule all future schedules, emit missed notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
##### Warm Start
|
||||||
|
|
||||||
|
**What it is**: App is returning from background (app was paused but process still running).
|
||||||
|
|
||||||
|
**Recovery Process**:
|
||||||
|
1. Plugin's `load()` method may be called (or app resumes)
|
||||||
|
2. Plugin checks for missed alarms/notifications (same as cold start)
|
||||||
|
3. Plugin verifies that active alarms are still scheduled correctly
|
||||||
|
4. Plugin reschedules if any alarms were cancelled (shouldn't happen, but verify)
|
||||||
|
|
||||||
|
**Investigation Goal**: Verify that the plugin checks for missed alarms on warm start and verifies active alarms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
##### Force Stop Recovery (Android)
|
||||||
|
|
||||||
|
**What it is**: App was force-stopped and user manually opens it again.
|
||||||
|
|
||||||
|
**Recovery Process**:
|
||||||
|
1. App launches (this is the only way to recover from force stop)
|
||||||
|
2. Plugin's `load()` method is called
|
||||||
|
3. Plugin detects that alarms were cancelled (all alarms have `scheduled_time < now` or are missing from AlarmManager)
|
||||||
|
4. Plugin queries database for all enabled alarms
|
||||||
|
5. For each alarm:
|
||||||
|
- If `scheduled_time < now`: Mark as missed, generate missed alarm event, reschedule if repeating
|
||||||
|
- If `scheduled_time >= now`: Reschedule the alarm
|
||||||
|
6. Plugin reschedules all future alarms
|
||||||
|
|
||||||
|
**Investigation Goal**: Verify that the plugin can detect force stop scenario and fully recover all alarms.
|
||||||
|
|
||||||
|
**Key Difference**: Force stop recovery is more comprehensive than normal app launch recovery because ALL alarms were cancelled, not just missed ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 0.3 What We're Investigating
|
||||||
|
|
||||||
|
For each scenario, we want to know:
|
||||||
|
|
||||||
|
1. **Does the alarm/notification fire?** (OS behavior)
|
||||||
|
2. **Does the plugin detect missed alarms?** (Plugin behavior)
|
||||||
|
3. **Does the plugin recover/reschedule?** (Plugin behavior)
|
||||||
|
4. **What happens on next app launch?** (Recovery behavior)
|
||||||
|
|
||||||
|
**Expected Behaviors**:
|
||||||
|
|
||||||
|
| Scenario | Alarm Fires? | Plugin Detects Missed? | Plugin Recovers? |
|
||||||
|
| -------- | ------------ | ---------------------- | ---------------- |
|
||||||
|
| Swipe from recents | ✅ Yes (OS) | N/A (fired) | N/A |
|
||||||
|
| Force stop | ❌ No (OS cancels) | ✅ Should detect | ✅ Should recover |
|
||||||
|
| Device reboot (Android) | ❌ No (OS cancels) | ✅ Should detect | ✅ Should recover |
|
||||||
|
| Device reboot (iOS) | ✅ Yes (OS persists) | ⚠️ May detect | ⚠️ May recover |
|
||||||
|
| Cold start | N/A | ✅ Should detect | ✅ Should recover |
|
||||||
|
| Warm start | N/A | ✅ Should detect | ✅ Should verify |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Android Findings
|
||||||
|
|
||||||
|
### 1.1 Boot Receiver Implementation
|
||||||
|
|
||||||
|
**Status**: ✅ **IMPLEMENTED**
|
||||||
|
|
||||||
|
**Location**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
||||||
|
|
||||||
|
**Findings**:
|
||||||
|
- Boot receiver exists and handles `ACTION_BOOT_COMPLETED` (line 24)
|
||||||
|
- Reschedules alarms from database (line 38+)
|
||||||
|
- Loads enabled schedules from Room database (line 40)
|
||||||
|
- Reschedules both "fetch" and "notify" schedules (lines 46-81)
|
||||||
|
|
||||||
|
**Gap Identified**:
|
||||||
|
- **Missed Alarm Handling**: Boot receiver only reschedules FUTURE alarms
|
||||||
|
- Line 64: `if (nextRunTime > System.currentTimeMillis())`
|
||||||
|
- This means if an alarm was scheduled for before the reboot time, it won't be rescheduled
|
||||||
|
- **No missed alarm detection or notification**
|
||||||
|
|
||||||
|
**Recommendation**: Add missed alarm detection in `rescheduleNotifications()` method
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 Missed Alarm Detection
|
||||||
|
|
||||||
|
**Status**: ⚠️ **PARTIAL**
|
||||||
|
|
||||||
|
**Location**: `android/src/main/java/com/timesafari/dailynotification/dao/NotificationContentDao.java`
|
||||||
|
|
||||||
|
**Findings**:
|
||||||
|
- DAO has query for missed alarms: `getNotificationsReadyForDelivery()` (line 98)
|
||||||
|
- Query: `SELECT * FROM notification_content WHERE scheduled_time <= :currentTime AND delivery_status != 'delivered'`
|
||||||
|
- This can identify notifications that should have fired but haven't
|
||||||
|
|
||||||
|
**Gap Identified**:
|
||||||
|
- **Not called on app launch**: The `DailyNotificationPlugin.load()` method (line 91) only initializes the database
|
||||||
|
- No recovery logic in `load()` method
|
||||||
|
- Query exists but may not be used for missed alarm detection
|
||||||
|
|
||||||
|
**Recommendation**: Add missed alarm detection in `load()` method or create separate recovery method
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 App Launch Recovery
|
||||||
|
|
||||||
|
**Status**: ❌ **NOT IMPLEMENTED**
|
||||||
|
|
||||||
|
**Location**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
|
||||||
|
|
||||||
|
**Expected Behavior** (as defined in Section 0.2):
|
||||||
|
|
||||||
|
**Cold Start Recovery**:
|
||||||
|
1. Plugin `load()` method called
|
||||||
|
2. Query database for missed alarms: `scheduled_time < now AND delivery_status != 'delivered'`
|
||||||
|
3. For each missed alarm:
|
||||||
|
- Generate missed alarm event/notification
|
||||||
|
- Reschedule if repeating
|
||||||
|
- Update delivery status
|
||||||
|
4. Reschedule all future alarms from database
|
||||||
|
5. Verify active alarms match stored alarms
|
||||||
|
|
||||||
|
**Warm Start Recovery**:
|
||||||
|
1. Plugin checks for missed alarms (same as cold start)
|
||||||
|
2. Verify active alarms are still scheduled
|
||||||
|
3. Reschedule if any were cancelled
|
||||||
|
|
||||||
|
**Force Stop Recovery**:
|
||||||
|
1. Detect that all alarms were cancelled (force stop scenario)
|
||||||
|
2. Query database for ALL enabled alarms
|
||||||
|
3. For each alarm:
|
||||||
|
- If `scheduled_time < now`: Mark as missed, generate event, reschedule if repeating
|
||||||
|
- If `scheduled_time >= now`: Reschedule immediately
|
||||||
|
4. Fully restore alarm state
|
||||||
|
|
||||||
|
**Current Implementation**:
|
||||||
|
- `load()` method (line 91) only initializes database
|
||||||
|
- No recovery logic on app launch
|
||||||
|
- No check for missed alarms
|
||||||
|
- No rescheduling of future alarms
|
||||||
|
- No distinction between cold/warm/force-stop scenarios
|
||||||
|
|
||||||
|
**Gap Identified**:
|
||||||
|
- Plugin does not recover on app cold/warm start
|
||||||
|
- Plugin does not recover from force stop
|
||||||
|
- Only boot receiver handles recovery (and only for future alarms)
|
||||||
|
- No missed alarm detection on app launch
|
||||||
|
|
||||||
|
**Recommendation**:
|
||||||
|
1. Add recovery logic to `load()` method or create `ReactivationManager`
|
||||||
|
2. Implement missed alarm detection using `getNotificationsReadyForDelivery()` query
|
||||||
|
3. Implement force stop detection (all alarms cancelled)
|
||||||
|
4. Implement rescheduling of future alarms from database
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 Persistence Completeness
|
||||||
|
|
||||||
|
**Status**: ✅ **IMPLEMENTED**
|
||||||
|
|
||||||
|
**Findings**:
|
||||||
|
- Room database used for persistence
|
||||||
|
- `Schedule` entity stores: id, kind, cron, clockTime, enabled, nextRunAt
|
||||||
|
- `NotificationContentEntity` stores: id, title, body, scheduledTime, priority, etc.
|
||||||
|
- `ContentCache` stores: fetched content with TTL
|
||||||
|
|
||||||
|
**All Required Fields Present**:
|
||||||
|
- ✅ alarm_id (Schedule.id, NotificationContentEntity.id)
|
||||||
|
- ✅ trigger_time (Schedule.nextRunAt, NotificationContentEntity.scheduledTime)
|
||||||
|
- ✅ repeat_rule (Schedule.cron, Schedule.clockTime)
|
||||||
|
- ✅ channel_id (NotificationContentEntity - implicit)
|
||||||
|
- ✅ priority (NotificationContentEntity.priority)
|
||||||
|
- ✅ title, body (NotificationContentEntity)
|
||||||
|
- ✅ sound_enabled, vibration_enabled (NotificationContentEntity)
|
||||||
|
- ✅ created_at, updated_at (NotificationContentEntity)
|
||||||
|
- ✅ enabled (Schedule.enabled)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.5 Force Stop Recovery
|
||||||
|
|
||||||
|
**Status**: ❌ **NOT IMPLEMENTED**
|
||||||
|
|
||||||
|
**Expected Behavior** (as defined in Section 0.1 and 0.2):
|
||||||
|
|
||||||
|
**Force Stop Scenario**:
|
||||||
|
- User goes to Settings → Apps → [App] → Force Stop
|
||||||
|
- All alarms are immediately cancelled by the OS
|
||||||
|
- App cannot run until user manually opens it
|
||||||
|
- When app is opened, it's a cold start scenario
|
||||||
|
|
||||||
|
**Force Stop Recovery**:
|
||||||
|
1. Detect that alarms were cancelled (check AlarmManager for scheduled alarms)
|
||||||
|
2. Compare with database: if database has alarms but AlarmManager has none → force stop occurred
|
||||||
|
3. Query database for ALL enabled alarms
|
||||||
|
4. For each alarm:
|
||||||
|
- If `scheduled_time < now`: This alarm was missed during force stop
|
||||||
|
- Generate missed alarm event/notification
|
||||||
|
- Reschedule next occurrence if repeating
|
||||||
|
- Update delivery status
|
||||||
|
- If `scheduled_time >= now`: This alarm is still in the future
|
||||||
|
- Reschedule immediately
|
||||||
|
5. Fully restore alarm state
|
||||||
|
|
||||||
|
**Current Implementation**:
|
||||||
|
- No specific force stop detection
|
||||||
|
- No recovery logic for force stop scenario
|
||||||
|
- App launch recovery (if implemented) would handle this, but app launch recovery is not implemented
|
||||||
|
- Cannot distinguish between normal app launch and force stop recovery
|
||||||
|
|
||||||
|
**Gap Identified**:
|
||||||
|
- Plugin cannot detect force stop scenario
|
||||||
|
- Plugin cannot distinguish between normal app launch and force stop recovery
|
||||||
|
- No special handling for force stop scenario
|
||||||
|
- All alarms remain cancelled until user opens app, then plugin should recover them
|
||||||
|
|
||||||
|
**Recommendation**:
|
||||||
|
1. Implement app launch recovery (which will handle force stop as a special case)
|
||||||
|
2. Add force stop detection: compare AlarmManager scheduled alarms with database
|
||||||
|
3. If force stop detected, recover ALL alarms (not just missed ones)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. iOS Findings
|
||||||
|
|
||||||
|
### 2.1 Notification Persistence
|
||||||
|
|
||||||
|
**Status**: ✅ **IMPLEMENTED**
|
||||||
|
|
||||||
|
**Location**: `ios/Plugin/DailyNotificationStorage.swift`
|
||||||
|
|
||||||
|
**Findings**:
|
||||||
|
- Plugin uses `DailyNotificationStorage` for separate persistence
|
||||||
|
- Uses UserDefaults for quick access (line 40)
|
||||||
|
- Uses CoreData for structured data (line 41)
|
||||||
|
- Stores notifications separately from UNUserNotificationCenter
|
||||||
|
|
||||||
|
**Storage Components**:
|
||||||
|
- UserDefaults: Settings, last fetch, BGTask tracking
|
||||||
|
- CoreData: NotificationContent, Schedule entities
|
||||||
|
- UNUserNotificationCenter: OS-managed notification scheduling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Missed Notification Detection
|
||||||
|
|
||||||
|
**Status**: ⚠️ **PARTIAL**
|
||||||
|
|
||||||
|
**Location**: `ios/Plugin/DailyNotificationPlugin.swift`
|
||||||
|
|
||||||
|
**Findings**:
|
||||||
|
- `checkForMissedBGTask()` method exists (line 421)
|
||||||
|
- Checks for missed background tasks (BGTaskScheduler)
|
||||||
|
- Reschedules missed BGTask if needed
|
||||||
|
|
||||||
|
**Gap Identified**:
|
||||||
|
- Only checks for missed BGTask, not missed notifications
|
||||||
|
- UNUserNotificationCenter handles notification persistence, but plugin doesn't check for missed notifications
|
||||||
|
- No comparison between plugin storage and UNUserNotificationCenter pending notifications
|
||||||
|
|
||||||
|
**Recommendation**: Add missed notification detection by comparing plugin storage with UNUserNotificationCenter pending requests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 App Launch Recovery
|
||||||
|
|
||||||
|
**Status**: ⚠️ **PARTIAL**
|
||||||
|
|
||||||
|
**Location**: `ios/Plugin/DailyNotificationPlugin.swift`
|
||||||
|
|
||||||
|
**Expected Behavior** (iOS Missed Notification Recovery Architecture):
|
||||||
|
|
||||||
|
**Required Steps for Missed Notification Detection**:
|
||||||
|
1. Query plugin storage (CoreData) for all scheduled notifications
|
||||||
|
2. Query `UNUserNotificationCenter.pendingNotificationRequests()` for future notifications
|
||||||
|
3. Query `UNUserNotificationCenter.getDeliveredNotifications()` for already-fired notifications
|
||||||
|
4. Find CoreData entries where:
|
||||||
|
- `scheduled_time < now` (should have fired)
|
||||||
|
- NOT in `deliveredNotifications` list (didn't fire)
|
||||||
|
- NOT in `pendingNotificationRequests` list (not scheduled for future)
|
||||||
|
5. Generate "missed notification" events for each detected miss
|
||||||
|
6. Reschedule repeating notifications
|
||||||
|
7. Verify that scheduled notifications in UNUserNotificationCenter align with CoreData schedules
|
||||||
|
|
||||||
|
**This must be placed in `load()` during cold start.**
|
||||||
|
|
||||||
|
**Current Implementation**:
|
||||||
|
- `load()` method exists (line 42)
|
||||||
|
- `setupBackgroundTasks()` called (line 318)
|
||||||
|
- `checkForMissedBGTask()` called on setup (line 330)
|
||||||
|
- Only checks for missed BGTask, not missed notifications
|
||||||
|
- No recovery of notification state
|
||||||
|
- No rescheduling of notifications from plugin storage
|
||||||
|
- No comparison between UNUserNotificationCenter and CoreData
|
||||||
|
|
||||||
|
**Gap Identified**:
|
||||||
|
- Only checks for missed BGTask, not missed notifications
|
||||||
|
- No recovery of notification state
|
||||||
|
- No rescheduling of notifications from plugin storage
|
||||||
|
- No cross-checking between UNUserNotificationCenter and CoreData
|
||||||
|
- **iOS cannot detect missed notifications** unless plugin compares storage vs `UNUserNotificationCenter.getDeliveredNotifications()` or infers from plugin timestamps
|
||||||
|
|
||||||
|
**Recommendation**:
|
||||||
|
1. Add notification recovery logic in `load()` or `setupBackgroundTasks()`
|
||||||
|
2. Implement three-way comparison: CoreData vs pending vs delivered notifications
|
||||||
|
3. Add missed notification detection using the architecture above
|
||||||
|
4. Note: iOS does NOT allow arbitrary code execution at notification fire time unless user interacts or Notification Service Extensions are used (not currently used)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 Background Execution Limits
|
||||||
|
|
||||||
|
**Status**: ✅ **DOCUMENTED IN CODE**
|
||||||
|
|
||||||
|
**Findings**:
|
||||||
|
- BGTaskScheduler used for background fetch
|
||||||
|
- Time budget limitations understood (30 seconds typical)
|
||||||
|
- System-controlled execution acknowledged
|
||||||
|
- Rescheduling logic handles missed tasks
|
||||||
|
|
||||||
|
**Code Evidence**:
|
||||||
|
- `checkForMissedBGTask()` handles missed BGTask (line 421)
|
||||||
|
- 15-minute miss window used (line 448)
|
||||||
|
- Reschedules if missed (line 462)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Cross-Platform Gaps Summary
|
||||||
|
|
||||||
|
| Gap | Android | iOS | Severity | Recommendation |
|
||||||
|
| --- | ------- | --- | -------- | -------------- |
|
||||||
|
| Missed alarm/notification detection | ⚠️ Partial | ⚠️ Partial | **High** | Implement on app launch |
|
||||||
|
| App launch recovery | ❌ Missing | ⚠️ Partial | **High** | **MUST implement for both platforms** |
|
||||||
|
| Force stop recovery | ❌ Missing | N/A | **Medium** | Android: Implement app launch recovery with force stop detection |
|
||||||
|
| Boot recovery missed alarms | ⚠️ Only future | N/A | **Medium** | Android: Add missed alarm handling in boot receiver |
|
||||||
|
| Cross-check mechanism (DB vs OS) | ❌ Missing | ⚠️ Partial | **High** | Android: AlarmManager vs DB; iOS: UNUserNotificationCenter vs CoreData |
|
||||||
|
|
||||||
|
**Critical Requirement**: App Launch Recovery **must be implemented on BOTH platforms**:
|
||||||
|
- Plugin must execute recovery logic during `load()` OR equivalent
|
||||||
|
- Distinguish cold vs warm start
|
||||||
|
- Use timestamps in storage to verify last known state
|
||||||
|
- Reconcile DB entries with OS scheduling APIs
|
||||||
|
- Android: Cross-check AlarmManager scheduled alarms with DB
|
||||||
|
- iOS: Cross-check UNUserNotificationCenter with CoreData schedules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Test Validation Outputs
|
||||||
|
|
||||||
|
For each scenario, the exploration should produce explicit outputs:
|
||||||
|
|
||||||
|
| Scenario | OS Expected | Plugin Expected | Observed Result | Pass/Fail | Notes |
|
||||||
|
| -------- | ----------- | --------------- | --------------- | --------- | ----- |
|
||||||
|
| Swipe from recents | Alarm fires | Alarm fires | ☐ | ☐ | |
|
||||||
|
| Force stop | Alarm does NOT fire | Plugin detects on launch | ☐ | ☐ | |
|
||||||
|
| Device reboot (Android) | Alarm does NOT fire | Plugin reschedules on boot | ☐ | ☐ | |
|
||||||
|
| Device reboot (iOS) | Notification fires | Notification fires | ☐ | ☐ | |
|
||||||
|
| Cold start | N/A | Missed alarms detected | ☐ | ☐ | |
|
||||||
|
| Warm start | N/A | Missed alarms detected | ☐ | ☐ | |
|
||||||
|
| Force stop recovery | N/A | All alarms recovered | ☐ | ☐ | |
|
||||||
|
|
||||||
|
**This creates alignment with the [Exploration Template](./plugin-behavior-exploration-template.md).**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Next Steps
|
||||||
|
|
||||||
|
1. **Verify findings through testing** using [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md)
|
||||||
|
2. **Test boot receiver** on actual device reboot
|
||||||
|
3. **Test app launch recovery** on cold/warm start
|
||||||
|
4. **Test force stop recovery** on Android (with cross-check mechanism)
|
||||||
|
5. **Test missed notification detection** on iOS (with three-way comparison)
|
||||||
|
6. **Inspect `UNUserNotificationCenter.getPendingNotificationRequests()` vs CoreData** to detect "lost" iOS notifications
|
||||||
|
7. **Update Plugin Requirements** document with verified gaps
|
||||||
|
8. **Generate Test Validation Outputs** table with actual test results
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Code References for Implementation
|
||||||
|
|
||||||
|
### Android - Add Missed Alarm Detection with Force Stop Detection
|
||||||
|
|
||||||
|
**Location**: `DailyNotificationPlugin.kt` - `load()` method (line 91)
|
||||||
|
|
||||||
|
**Suggested Implementation** (with Force Stop Detection):
|
||||||
|
```kotlin
|
||||||
|
override fun load() {
|
||||||
|
super.load()
|
||||||
|
// ... existing initialization ...
|
||||||
|
|
||||||
|
// Check for missed alarms on app launch
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
detectAndHandleMissedAlarms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun detectAndHandleMissedAlarms() {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Cross-check: Query AlarmManager for active alarms
|
||||||
|
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
|
val activeAlarmCount = getActiveAlarmCount(alarmManager) // Helper method needed
|
||||||
|
|
||||||
|
// Query database for all enabled schedules
|
||||||
|
val dbSchedules = db.scheduleDao().getEnabled()
|
||||||
|
|
||||||
|
// Force Stop Detection: If DB has schedules but AlarmManager has zero
|
||||||
|
val forceStopDetected = dbSchedules.isNotEmpty() && activeAlarmCount == 0
|
||||||
|
|
||||||
|
if (forceStopDetected) {
|
||||||
|
Log.i(TAG, "Force stop detected - all alarms were cancelled")
|
||||||
|
// Recover ALL alarms (not just missed ones)
|
||||||
|
recoverAllAlarmsAfterForceStop(db, dbSchedules, currentTime)
|
||||||
|
} else {
|
||||||
|
// Normal recovery: only check for missed alarms
|
||||||
|
val missedNotifications = db.notificationContentDao()
|
||||||
|
.getNotificationsReadyForDelivery(currentTime)
|
||||||
|
|
||||||
|
missedNotifications.forEach { notification ->
|
||||||
|
// Generate missed alarm event/notification
|
||||||
|
// Reschedule if repeating
|
||||||
|
// Update delivery status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reschedule future alarms from database
|
||||||
|
rescheduleFutureAlarms(db, dbSchedules, currentTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun recoverAllAlarmsAfterForceStop(
|
||||||
|
db: DailyNotificationDatabase,
|
||||||
|
schedules: List<Schedule>,
|
||||||
|
currentTime: Long
|
||||||
|
) {
|
||||||
|
schedules.forEach { schedule ->
|
||||||
|
val nextRunTime = calculateNextRunTime(schedule)
|
||||||
|
if (nextRunTime < currentTime) {
|
||||||
|
// Past alarm - mark as missed
|
||||||
|
// Generate missed alarm notification
|
||||||
|
// Reschedule if repeating
|
||||||
|
} else {
|
||||||
|
// Future alarm - reschedule immediately
|
||||||
|
rescheduleAlarm(schedule, nextRunTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android - Add Missed Alarm Handling in Boot Receiver
|
||||||
|
|
||||||
|
**Location**: `BootReceiver.kt` - `rescheduleNotifications()` method (line 38)
|
||||||
|
|
||||||
|
**Suggested Implementation**:
|
||||||
|
```kotlin
|
||||||
|
// After rescheduling future alarms, check for missed ones
|
||||||
|
val missedNotifications = db.notificationContentDao()
|
||||||
|
.getNotificationsReadyForDelivery(System.currentTimeMillis())
|
||||||
|
|
||||||
|
missedNotifications.forEach { notification ->
|
||||||
|
// Generate missed alarm notification
|
||||||
|
// Reschedule if repeating
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS - Add Missed Notification Detection
|
||||||
|
|
||||||
|
**Location**: `DailyNotificationPlugin.swift` - `setupBackgroundTasks()` or `load()` method
|
||||||
|
|
||||||
|
**Suggested Implementation** (Three-Way Comparison):
|
||||||
|
```swift
|
||||||
|
private func checkForMissedNotifications() async {
|
||||||
|
// Step 1: Get pending notifications (future) from UNUserNotificationCenter
|
||||||
|
let pendingRequests = await notificationCenter.pendingNotificationRequests()
|
||||||
|
let pendingIds = Set(pendingRequests.map { $0.identifier })
|
||||||
|
|
||||||
|
// Step 2: Get delivered notifications (already fired) from UNUserNotificationCenter
|
||||||
|
let deliveredNotifications = await notificationCenter.getDeliveredNotifications()
|
||||||
|
let deliveredIds = Set(deliveredNotifications.map { $0.request.identifier })
|
||||||
|
|
||||||
|
// Step 3: Get notifications from plugin storage (CoreData)
|
||||||
|
let storedNotifications = storage?.getAllNotifications() ?? []
|
||||||
|
let currentTime = Date().timeIntervalSince1970
|
||||||
|
|
||||||
|
// Step 4: Find missed notifications
|
||||||
|
// Missed = scheduled_time < now AND not in delivered AND not in pending
|
||||||
|
for notification in storedNotifications {
|
||||||
|
let scheduledTime = notification.scheduledTime
|
||||||
|
let notificationId = notification.id
|
||||||
|
|
||||||
|
if scheduledTime < currentTime {
|
||||||
|
// Should have fired by now
|
||||||
|
if !deliveredIds.contains(notificationId) && !pendingIds.contains(notificationId) {
|
||||||
|
// This notification was missed
|
||||||
|
// Generate missed notification event
|
||||||
|
// Reschedule if repeating
|
||||||
|
// Update delivery status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Verify alignment - check if CoreData schedules match UNUserNotificationCenter
|
||||||
|
// Reschedule any missing notifications from CoreData
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md) - Use this for testing
|
||||||
|
- [Plugin Requirements & Implementation](./plugin-requirements-implementation.md) - Requirements based on findings
|
||||||
|
- [Platform Capability Reference](./platform-capability-reference.md) - OS-level facts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Document Separation Directive
|
||||||
|
|
||||||
|
After improvements are complete, separate documents by purpose:
|
||||||
|
|
||||||
|
- **This file** → Exploration Findings (final) - Code inspection and test results
|
||||||
|
- **Android Behavior** → Platform Reference (see [Platform Capability Reference](./platform-capability-reference.md))
|
||||||
|
- **iOS Behavior** → Platform Reference (see [Platform Capability Reference](./platform-capability-reference.md))
|
||||||
|
- **Plugin Requirements** → Independent document (see [Plugin Requirements & Implementation](./plugin-requirements-implementation.md))
|
||||||
|
- **Future Implementation Directive** → Separate document (to be created)
|
||||||
|
|
||||||
|
This avoids future redundancy and maintains clear separation of concerns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- These findings are from **code inspection only**
|
||||||
|
- **Actual testing required** to verify behavior
|
||||||
|
- Findings should be updated after testing with [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md)
|
||||||
|
- iOS missed notification detection requires three-way comparison: CoreData vs pending vs delivered
|
||||||
|
- Android force stop detection requires cross-check: AlarmManager vs database
|
||||||
|
|
||||||
670
docs/explore-alarm-behavior-directive.md
Normal file
670
docs/explore-alarm-behavior-directive.md
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
# DIRECTIVE: Explore & Document Alarm / Schedule / Notification Behavior in Capacitor Plugin (Android & iOS)
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Active Directive - Exploration Phase
|
||||||
|
|
||||||
|
## 0. Scope & Objective
|
||||||
|
|
||||||
|
We want to **explore, test, and document** how the *current* Capacitor plugin handles:
|
||||||
|
|
||||||
|
- **Alarms / schedules / reminders**
|
||||||
|
- **Local notifications**
|
||||||
|
- **Persistence and recovery** across:
|
||||||
|
- App kill / swipe from recents
|
||||||
|
- OS process kill
|
||||||
|
- Device reboot
|
||||||
|
- **Force stop** (Android) / hard termination (iOS)
|
||||||
|
- Cross-platform **semantic differences** between Android and iOS
|
||||||
|
|
||||||
|
The focus is **observation of current behavior**, not yet changing implementation.
|
||||||
|
|
||||||
|
We want a clear map of **what the plugin actually guarantees** on each platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Key Questions to Answer
|
||||||
|
|
||||||
|
For **each platform (Android, iOS)** and for **each "scheduled thing"** the plugin supports (alarms, reminders, scheduled notifications, repeating schedules, etc.):
|
||||||
|
|
||||||
|
### 1.1 How is it implemented under the hood?
|
||||||
|
|
||||||
|
- **Android**: AlarmManager? WorkManager? JobScheduler? Foreground service?
|
||||||
|
- **iOS**: UNUserNotificationCenter? BGTaskScheduler? background fetch? timers in foreground?
|
||||||
|
|
||||||
|
### 1.2 What happens when the app is:
|
||||||
|
|
||||||
|
- Swiped away from recents?
|
||||||
|
- Killed by OS (memory pressure)?
|
||||||
|
- Device rebooted?
|
||||||
|
- On Android: explicitly **Force Stopped** in system settings?
|
||||||
|
- On iOS: explicitly swiped away, then device rebooted before next trigger?
|
||||||
|
|
||||||
|
### 1.3 What is persisted?
|
||||||
|
|
||||||
|
Are schedules/alarms stored in:
|
||||||
|
|
||||||
|
- SQLite / Room / shared preferences (Android)?
|
||||||
|
- CoreData / UserDefaults / files (iOS)?
|
||||||
|
- Or are they only in RAM / native scheduler?
|
||||||
|
|
||||||
|
### 1.4 What is re-created and when?
|
||||||
|
|
||||||
|
- On boot?
|
||||||
|
- On app cold start?
|
||||||
|
- On notification tap?
|
||||||
|
- Not at all?
|
||||||
|
|
||||||
|
### 1.5 What does the plugin *promise* to the JS/TS layer?
|
||||||
|
|
||||||
|
- "Will always fire even after reboot"?
|
||||||
|
- "Will fire as long as app hasn't been force-stopped"?
|
||||||
|
- "Best-effort only"?
|
||||||
|
|
||||||
|
We are trying to align **plugin promises** with **real platform capabilities and limitations.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Android Exploration
|
||||||
|
|
||||||
|
### 2.1 Code-Level Inspection
|
||||||
|
|
||||||
|
**Source Locations:**
|
||||||
|
|
||||||
|
- **Plugin Implementation**: `android/src/main/java/com/timesafari/dailynotification/` (Kotlin files may also be present)
|
||||||
|
- **Manifest**: `android/src/main/AndroidManifest.xml`
|
||||||
|
- **Test Applications**:
|
||||||
|
- `test-apps/android-test-app/` - Primary Android test app
|
||||||
|
- `test-apps/daily-notification-test/` - Additional test application
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
|
||||||
|
1. **Locate the Android implementation in the plugin:**
|
||||||
|
- Primary path: `android/src/main/java/com/timesafari/dailynotification/`
|
||||||
|
- Key files to examine:
|
||||||
|
- `DailyNotificationPlugin.kt` - Main plugin class (see `scheduleDailyNotification()` at line 1302)
|
||||||
|
- `DailyNotificationWorker.java` - WorkManager worker (see `doWork()` at line 59)
|
||||||
|
- `DailyNotificationReceiver.java` - BroadcastReceiver for alarms (see `onReceive()` at line 51)
|
||||||
|
- `NotifyReceiver.kt` - AlarmManager scheduling (see `scheduleExactNotification()` at line 92)
|
||||||
|
- `BootReceiver.kt` - Boot recovery (see `onReceive()` at line 24)
|
||||||
|
- `FetchWorker.kt` - WorkManager fetch scheduling (see `scheduleFetch()` at line 31)
|
||||||
|
|
||||||
|
2. **Identify the mechanisms used to schedule work:**
|
||||||
|
- **AlarmManager**: Used via `NotifyReceiver.scheduleExactNotification()` (line 92)
|
||||||
|
- `setAlarmClock()` for Android 5.0+ (line 219)
|
||||||
|
- `setExactAndAllowWhileIdle()` for Android 6.0+ (line 223)
|
||||||
|
- `setExact()` for older versions (line 231)
|
||||||
|
- **WorkManager**: Used for background fetching and notification processing
|
||||||
|
- `FetchWorker.scheduleFetch()` (line 31) - Uses `OneTimeWorkRequest`
|
||||||
|
- `DailyNotificationWorker.doWork()` (line 59) - Processes notifications
|
||||||
|
- **No JobScheduler**: Not used in current implementation
|
||||||
|
- **No repeating alarms**: Uses one-time alarms with rescheduling
|
||||||
|
|
||||||
|
3. **Inspect how notifications are issued:**
|
||||||
|
- **NotificationCompat**: Used in `DailyNotificationWorker.displayNotification()` and `NotifyReceiver.showNotification()` (line 482)
|
||||||
|
- **setFullScreenIntent**: Not currently used (check `NotifyReceiver.showNotification()` at line 443)
|
||||||
|
- **Notification channels**: Created in `NotifyReceiver.showNotification()` (lines 454-470)
|
||||||
|
- Channel ID: `"timesafari.daily"` (see `DailyNotificationWorker.java` line 46)
|
||||||
|
- Importance based on priority (HIGH/DEFAULT/LOW)
|
||||||
|
- **ChannelManager**: Check for separate channel management class
|
||||||
|
|
||||||
|
4. **Check for permissions & receivers:**
|
||||||
|
- Manifest: `android/src/main/AndroidManifest.xml`
|
||||||
|
- Look for:
|
||||||
|
- `RECEIVE_BOOT_COMPLETED` permission
|
||||||
|
- `SCHEDULE_EXACT_ALARM` permission
|
||||||
|
- Any `BroadcastReceiver` declarations for:
|
||||||
|
- `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED`
|
||||||
|
- Custom alarm intent actions
|
||||||
|
- Check test app manifests:
|
||||||
|
- `test-apps/android-test-app/app/src/main/AndroidManifest.xml`
|
||||||
|
- `test-apps/daily-notification-test/` (if applicable)
|
||||||
|
|
||||||
|
5. **Determine persistence strategy:**
|
||||||
|
- Where are scheduled alarms stored?
|
||||||
|
- SharedPreferences?
|
||||||
|
- SQLite / Room?
|
||||||
|
- Not at all (just in AlarmManager/work queue)?
|
||||||
|
|
||||||
|
6. **Check for reschedule-on-boot or reschedule-on-app-launch logic:**
|
||||||
|
- **BootReceiver**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
||||||
|
- `onReceive()` handles `ACTION_BOOT_COMPLETED` (line 24)
|
||||||
|
- `rescheduleNotifications()` reloads from database and reschedules (line 38+)
|
||||||
|
- Calls `NotifyReceiver.scheduleExactNotification()` for each schedule (line 74)
|
||||||
|
- **Alternative**: `DailyNotificationRebootRecoveryManager.java` has `BootCompletedReceiver` inner class (line 278)
|
||||||
|
- **App launch recovery**: Check `DailyNotificationPlugin.kt` for initialization logic that reschedules on app start
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Behavior Testing Matrix (Android)
|
||||||
|
|
||||||
|
**Test Applications:**
|
||||||
|
|
||||||
|
- **Primary**: `test-apps/android-test-app/` - Use this for comprehensive testing
|
||||||
|
- **Secondary**: `test-apps/daily-notification-test/` - Additional test scenarios if needed
|
||||||
|
|
||||||
|
**Run these tests on a real device or emulator.**
|
||||||
|
|
||||||
|
For each scenario, record:
|
||||||
|
|
||||||
|
- Did the alarm / notification fire?
|
||||||
|
- Did it fire on time?
|
||||||
|
- From what state did the app wake up (cold, warm, already running)?
|
||||||
|
- Any visible logs / errors?
|
||||||
|
|
||||||
|
**Scenarios:**
|
||||||
|
|
||||||
|
#### 2.2.1 Base Case
|
||||||
|
|
||||||
|
- Schedule an alarm/notification 2 minutes in the future.
|
||||||
|
- Leave app in foreground or background.
|
||||||
|
- Confirm it fires.
|
||||||
|
|
||||||
|
#### 2.2.2 Swipe from Recents
|
||||||
|
|
||||||
|
- Schedule alarm (2–5 minutes).
|
||||||
|
- Swipe app away from recents.
|
||||||
|
- Wait for trigger time.
|
||||||
|
- Observe: does alarm still fire?
|
||||||
|
|
||||||
|
#### 2.2.3 OS Kill (simulate memory pressure)
|
||||||
|
|
||||||
|
- Mainly observational; may be tricky to force, but:
|
||||||
|
- Open many other apps or use `adb shell am kill <package>` (not force-stop).
|
||||||
|
- Confirm whether scheduled alarm still fires.
|
||||||
|
|
||||||
|
#### 2.2.4 Device Reboot
|
||||||
|
|
||||||
|
- Schedule alarm (e.g. 10 minutes in the future).
|
||||||
|
- Reboot device.
|
||||||
|
- Do **not** reopen app.
|
||||||
|
- Wait past scheduled time:
|
||||||
|
- Does plugin reschedule and fire automatically?
|
||||||
|
- Or does nothing happen until user opens the app?
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
- After device reboot, manually open the app.
|
||||||
|
- Does plugin detect missed alarms and:
|
||||||
|
- Fire "missed" behavior?
|
||||||
|
- Reschedule future alarms?
|
||||||
|
- Or silently forget them?
|
||||||
|
|
||||||
|
#### 2.2.5 Android Force Stop
|
||||||
|
|
||||||
|
- Schedule alarm.
|
||||||
|
- Go to Settings → Apps → [Your App] → Force stop.
|
||||||
|
- Wait for trigger time.
|
||||||
|
- Observe: it should **not** fire (OS-level rule).
|
||||||
|
- Then open app again and see if plugin automatically:
|
||||||
|
- Detects missed alarms and recovers, or
|
||||||
|
- Treats them as lost.
|
||||||
|
|
||||||
|
**Goal:** build a clear empirical table of plugin behavior vs Android's known rules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. iOS Exploration
|
||||||
|
|
||||||
|
### 3.1 Code-Level Inspection
|
||||||
|
|
||||||
|
**Source Locations:**
|
||||||
|
|
||||||
|
- **Plugin Implementation**: `ios/Plugin/` - Swift plugin files
|
||||||
|
- **Alternative Branch**: Check `ios-2` branch for additional iOS implementations or variations
|
||||||
|
- **Test Applications**:
|
||||||
|
- `test-apps/ios-test-app/` - Primary iOS test app
|
||||||
|
- `test-apps/daily-notification-test/` - Additional test application (if iOS support exists)
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
|
||||||
|
1. **Locate the iOS implementation:**
|
||||||
|
- Primary path: `ios/Plugin/`
|
||||||
|
- Key files to examine:
|
||||||
|
- `DailyNotificationPlugin.swift` - Main plugin class (see `scheduleUserNotification()` at line 506)
|
||||||
|
- `DailyNotificationScheduler.swift` - Notification scheduling (see `scheduleNotification()` at line 133)
|
||||||
|
- `DailyNotificationBackgroundTasks.swift` - BGTaskScheduler handlers
|
||||||
|
- **Also check**: `ios-2` branch for alternative implementations or newer iOS code
|
||||||
|
```bash
|
||||||
|
git checkout ios-2
|
||||||
|
# Compare ios/Plugin/DailyNotificationPlugin.swift
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Identify the scheduling mechanism:**
|
||||||
|
- **UNUserNotificationCenter**: Primary mechanism
|
||||||
|
- `DailyNotificationScheduler.scheduleNotification()` uses `UNCalendarNotificationTrigger` (line 172)
|
||||||
|
- `DailyNotificationPlugin.scheduleUserNotification()` uses `UNTimeIntervalNotificationTrigger` (line 514)
|
||||||
|
- No `UNLocationNotificationTrigger` found
|
||||||
|
- **BGTaskScheduler**: Used for background fetch
|
||||||
|
- `DailyNotificationPlugin.scheduleBackgroundFetch()` (line 495)
|
||||||
|
- Uses `BGAppRefreshTaskRequest` (line 496)
|
||||||
|
- **No timers**: No plain Timer usage found (would die with app)
|
||||||
|
|
||||||
|
3. **Determine what's persisted:**
|
||||||
|
- Does the plugin store alarms in:
|
||||||
|
- `UserDefaults`?
|
||||||
|
- Files / CoreData?
|
||||||
|
- Or only within UNUserNotificationCenter's pending notification requests (no parallel app-side storage)?
|
||||||
|
|
||||||
|
4. **Check for re-scheduling behavior on app launch:**
|
||||||
|
- On app start (cold or warm), does plugin:
|
||||||
|
- Query `UNUserNotificationCenter` for pending notifications?
|
||||||
|
- Compare against its own store?
|
||||||
|
- Attempt to rebuild schedules?
|
||||||
|
- Or does it rely solely on UNUserNotificationCenter to manage everything?
|
||||||
|
|
||||||
|
5. **Determine capabilities / limitations:**
|
||||||
|
- Can the plugin run arbitrary code *when the notification fires*?
|
||||||
|
- Only via notification actions / `didReceive response` callbacks.
|
||||||
|
- Does it support repeating notifications (daily/weekly)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 Behavior Testing Matrix (iOS)
|
||||||
|
|
||||||
|
**Test Applications:**
|
||||||
|
|
||||||
|
- **Primary**: `test-apps/ios-test-app/` - Use this for comprehensive testing
|
||||||
|
- **Secondary**: `test-apps/daily-notification-test/` - Additional test scenarios if needed
|
||||||
|
- **Note**: Compare behavior between main branch and `ios-2` branch implementations if they differ
|
||||||
|
|
||||||
|
As with Android, test:
|
||||||
|
|
||||||
|
#### 3.2.1 Base Case
|
||||||
|
|
||||||
|
- Schedule local notification 2–5 minutes in the future; leave app backgrounded.
|
||||||
|
- Confirm it fires on time with app in background.
|
||||||
|
|
||||||
|
#### 3.2.2 Swipe App Away
|
||||||
|
|
||||||
|
- Schedule notification, then swipe app away from app switcher.
|
||||||
|
- Confirm notification still fires (iOS local notification center should handle this).
|
||||||
|
|
||||||
|
#### 3.2.3 Device Reboot
|
||||||
|
|
||||||
|
- Schedule notification for a future time.
|
||||||
|
- Reboot device.
|
||||||
|
- Do **not** open app.
|
||||||
|
- Test whether:
|
||||||
|
- Notification still fires (iOS usually persists scheduled local notifications across reboot), or
|
||||||
|
- Behavior depends on trigger type (time vs calendar, etc.).
|
||||||
|
|
||||||
|
#### 3.2.4 Hard Termination & Relaunch
|
||||||
|
|
||||||
|
- Schedule some repeating notification(s).
|
||||||
|
- Terminate app via Xcode / app switcher.
|
||||||
|
- Allow some triggers to occur.
|
||||||
|
- Reopen app and inspect whether plugin:
|
||||||
|
- Notices anything about missed events, or
|
||||||
|
- Simply trusts that UNUserNotificationCenter handled user-visible parts.
|
||||||
|
|
||||||
|
**Goal:** map what your *plugin* adds on top of native behavior vs what is entirely delegated to the OS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Cross-Platform Behavior & Promise Alignment
|
||||||
|
|
||||||
|
After exploration, produce a summary:
|
||||||
|
|
||||||
|
### 4.1 What the plugin actually guarantees to JS callers
|
||||||
|
|
||||||
|
- "Scheduled reminders will still fire after app swipe / kill"
|
||||||
|
- "On Android, reminders may not survive device reboot unless app is opened"
|
||||||
|
- "After Force Stop (Android), nothing runs until user opens app"
|
||||||
|
- "On iOS, local notifications themselves persist across reboot, but no extra app code runs at fire time unless user interacts"
|
||||||
|
|
||||||
|
### 4.2 Where semantics differ
|
||||||
|
|
||||||
|
- Android may require explicit rescheduling on boot; iOS may not.
|
||||||
|
- Android **force stop** is a hard wall; iOS has no exact equivalent in user-facing settings.
|
||||||
|
- Plugin may currently:
|
||||||
|
- Over-promise on reliability, or
|
||||||
|
- Under-document platform limitations.
|
||||||
|
|
||||||
|
### 4.3 Where we need to add warnings / notes in the public API
|
||||||
|
|
||||||
|
- E.g. "This schedule is best-effort; on Android, device reboot may cancel it unless you open the app again," etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Deliverables from This Exploration
|
||||||
|
|
||||||
|
### 5.1 Doc: `ALARMS_BEHAVIOR_MATRIX.md`
|
||||||
|
|
||||||
|
A table of scenarios (per platform) vs observed behavior.
|
||||||
|
|
||||||
|
Includes:
|
||||||
|
|
||||||
|
- App state
|
||||||
|
- OS event (reboot, force stop, etc.)
|
||||||
|
- What fired, what didn't
|
||||||
|
- Log snippets where useful
|
||||||
|
|
||||||
|
### 5.2 Doc: `PLUGIN_ALARM_LIMITATIONS.md`
|
||||||
|
|
||||||
|
Plain-language explanation of:
|
||||||
|
|
||||||
|
- Android hard limits (Force Stop, reboot behavior)
|
||||||
|
- iOS behavior (local notifications vs app code execution)
|
||||||
|
- Clear note on what the plugin promises.
|
||||||
|
|
||||||
|
### 5.3 Annotated code pointers
|
||||||
|
|
||||||
|
Commented locations in Android/iOS code where:
|
||||||
|
|
||||||
|
- Scheduling is performed
|
||||||
|
- Persistence (if any) is implemented
|
||||||
|
- Rescheduling (if any) is implemented
|
||||||
|
|
||||||
|
### 5.4 Open Questions / TODOs
|
||||||
|
|
||||||
|
Gaps uncovered:
|
||||||
|
|
||||||
|
- No reschedule-on-boot?
|
||||||
|
- No persistence of schedules?
|
||||||
|
- No handling of "missed" alarms on reactivation?
|
||||||
|
- Potential next-step directives (separate document) to improve behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. One-Liner Summary
|
||||||
|
|
||||||
|
> This directive is to **investigate, not change**: we want a precise, tested understanding of what our Capacitor plugin *currently* does with alarms/schedules/notifications on Android and iOS, especially across kills, reboots, and force stops, and where that behavior does or does not match what we think we're promising to app developers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Android Alarm Persistence Directive](./android-alarm-persistence-directive.md) - General Android alarm capabilities and limitations
|
||||||
|
- [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md) - Testing boot receiver behavior
|
||||||
|
- [App Startup Recovery Solution](./app-startup-recovery-solution.md) - Recovery mechanisms on app launch
|
||||||
|
- [Reboot Testing Procedure](./reboot-testing-procedure.md) - Step-by-step reboot testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source Code Structure Reference
|
||||||
|
|
||||||
|
### Android Source Files
|
||||||
|
|
||||||
|
**Primary Plugin Code:**
|
||||||
|
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt` (or `.java`)
|
||||||
|
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
|
||||||
|
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java`
|
||||||
|
- `android/src/main/java/com/timesafari/dailynotification/ChannelManager.java`
|
||||||
|
- `android/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
**Test Applications:**
|
||||||
|
- `test-apps/android-test-app/app/src/main/` - Test app source
|
||||||
|
- `test-apps/android-test-app/app/src/main/assets/public/index.html` - Test UI
|
||||||
|
- `test-apps/daily-notification-test/` - Additional test app (if present)
|
||||||
|
|
||||||
|
### iOS Source Files
|
||||||
|
|
||||||
|
**Primary Plugin Code:**
|
||||||
|
- `ios/Plugin/DailyNotificationPlugin.swift` (or similar)
|
||||||
|
- `ios/Plugin/` - All Swift plugin files
|
||||||
|
- **Also check**: `ios-2` branch for alternative implementations
|
||||||
|
|
||||||
|
**Test Applications:**
|
||||||
|
- `test-apps/ios-test-app/` - Test app source
|
||||||
|
- `test-apps/daily-notification-test/` - Additional test app (if iOS support exists)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Code References (File Locations, Functions, Line Numbers)
|
||||||
|
|
||||||
|
### Android Implementation Details
|
||||||
|
|
||||||
|
#### Alarm Scheduling
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt`
|
||||||
|
|
||||||
|
- **Function**: `scheduleExactNotification()` - **Lines 92-247**
|
||||||
|
- Schedules exact alarms using AlarmManager
|
||||||
|
- Uses `setAlarmClock()` for Android 5.0+ (API 21+) - **Line 219**
|
||||||
|
- Falls back to `setExactAndAllowWhileIdle()` for Android 6.0+ (API 23+) - **Line 223**
|
||||||
|
- Falls back to `setExact()` for older versions - **Line 231**
|
||||||
|
- Called from:
|
||||||
|
- `DailyNotificationPlugin.kt` - `scheduleDailyNotification()` - **Line 1385**
|
||||||
|
- `DailyNotificationPlugin.kt` - `scheduleDailyReminder()` - **Line 809**
|
||||||
|
- `DailyNotificationPlugin.kt` - `scheduleDualNotification()` - **Line 1685**
|
||||||
|
- `BootReceiver.kt` - `rescheduleNotifications()` - **Line 74**
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
|
||||||
|
|
||||||
|
- **Function**: `scheduleDailyNotification()` - **Lines 1302-1417**
|
||||||
|
- Main plugin method for scheduling notifications
|
||||||
|
- Checks exact alarm permission - **Line 1309**
|
||||||
|
- Opens settings if permission not granted - **Lines 1314-1324**
|
||||||
|
- Calls `NotifyReceiver.scheduleExactNotification()` - **Line 1385**
|
||||||
|
- Schedules prefetch 2 minutes before notification - **Line 1395**
|
||||||
|
|
||||||
|
- **Function**: `scheduleDailyReminder()` - **Lines 777-833**
|
||||||
|
- Schedules static reminders (no content dependency)
|
||||||
|
- Calls `NotifyReceiver.scheduleExactNotification()` - **Line 809**
|
||||||
|
|
||||||
|
- **Function**: `canScheduleExactAlarms()` - **Lines 835-860**
|
||||||
|
- Checks if exact alarm permission is granted (Android 12+)
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java`
|
||||||
|
|
||||||
|
- **Function**: `scheduleExactAlarm()` - **Lines 127-158**
|
||||||
|
- Uses `setExactAndAllowWhileIdle()` - **Line 135**
|
||||||
|
- Falls back to `setExact()` - **Line 141**
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationExactAlarmManager.java`
|
||||||
|
|
||||||
|
- **Function**: `scheduleExactAlarm()` - **Lines 186-201**
|
||||||
|
- Uses `setExactAndAllowWhileIdle()` - **Line 189**
|
||||||
|
- Falls back to `setExact()` - **Line 193**
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java`
|
||||||
|
|
||||||
|
- **Function**: `scheduleExactAlarm()` - **Lines 237-272**
|
||||||
|
- Uses `setExactAndAllowWhileIdle()` - **Line 242**
|
||||||
|
- Falls back to `setExact()` - **Line 251**
|
||||||
|
|
||||||
|
#### WorkManager Usage
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/FetchWorker.kt`
|
||||||
|
|
||||||
|
- **Function**: `scheduleFetch()` - **Lines 31-59**
|
||||||
|
- Schedules WorkManager one-time work request
|
||||||
|
- Uses `OneTimeWorkRequestBuilder` - **Line 36**
|
||||||
|
- Enqueues with `WorkManager.getInstance().enqueueUniqueWork()` - **Lines 53-58**
|
||||||
|
|
||||||
|
- **Function**: `scheduleDelayedPrefetch()` - **Lines 62-131**
|
||||||
|
- Schedules delayed prefetch work
|
||||||
|
- Uses `setInitialDelay()` - **Line 104**
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
|
||||||
|
|
||||||
|
- **Function**: `doWork()` - **Lines 59-915**
|
||||||
|
- Main WorkManager worker execution
|
||||||
|
- Handles notification display - **Line 91**
|
||||||
|
- Calls `displayNotification()` - **Line 200+**
|
||||||
|
|
||||||
|
- **Function**: `displayNotification()` - **Lines 200-400+**
|
||||||
|
- Displays notification using NotificationCompat
|
||||||
|
- Ensures notification channel exists
|
||||||
|
- Uses `NotificationCompat.Builder` - **Line 200+**
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java`
|
||||||
|
|
||||||
|
- **Function**: `scheduleFetch()` - **Lines 78-140**
|
||||||
|
- Schedules WorkManager fetch work
|
||||||
|
- Uses `OneTimeWorkRequest.Builder` - **Line 106**
|
||||||
|
- Enqueues with `workManager.enqueueUniqueWork()` - **Lines 115-119**
|
||||||
|
|
||||||
|
#### Boot Recovery
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
||||||
|
|
||||||
|
- **Class**: `BootReceiver` - **Lines 18-100+**
|
||||||
|
- BroadcastReceiver for BOOT_COMPLETED
|
||||||
|
- `onReceive()` - **Line 24** - Handles boot intent
|
||||||
|
- `rescheduleNotifications()` - **Line 38+** - Reschedules all notifications from database
|
||||||
|
- Calls `NotifyReceiver.scheduleExactNotification()` - **Line 74**
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationRebootRecoveryManager.java`
|
||||||
|
|
||||||
|
- **Class**: `BootCompletedReceiver` - **Lines 278-297**
|
||||||
|
- Inner BroadcastReceiver for boot events
|
||||||
|
- `onReceive()` - **Line 280** - Handles BOOT_COMPLETED action
|
||||||
|
- Calls `handleSystemReboot()` - **Line 290**
|
||||||
|
|
||||||
|
#### Notification Display
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java`
|
||||||
|
|
||||||
|
- **Function**: `onReceive()` - **Lines 51-485**
|
||||||
|
- Lightweight BroadcastReceiver triggered by AlarmManager
|
||||||
|
- Enqueues WorkManager work for heavy operations - **Line 100+**
|
||||||
|
- Extracts notification ID and action from intent
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt`
|
||||||
|
|
||||||
|
- **Function**: `showNotification()` - **Lines 443-500**
|
||||||
|
- Displays notification using NotificationCompat
|
||||||
|
- Creates notification channel if needed - **Lines 454-470**
|
||||||
|
- Uses `NotificationCompat.Builder` - **Line 482**
|
||||||
|
|
||||||
|
#### Persistence
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
|
||||||
|
|
||||||
|
- Database operations use Room database:
|
||||||
|
- `getDatabase()` - Returns DailyNotificationDatabase instance
|
||||||
|
- Schedule storage in `scheduleDailyNotification()` - **Lines 1393-1410**
|
||||||
|
- Schedule storage in `scheduleDailyReminder()` - **Lines 813-821**
|
||||||
|
|
||||||
|
#### Permissions & Manifest
|
||||||
|
|
||||||
|
**File**: `android/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
- **Note**: Plugin manifest is minimal; receivers declared in consuming app manifest
|
||||||
|
- Check test app manifest: `test-apps/android-test-app/app/src/main/AndroidManifest.xml`
|
||||||
|
- Look for `RECEIVE_BOOT_COMPLETED` permission
|
||||||
|
- Look for `SCHEDULE_EXACT_ALARM` permission
|
||||||
|
- Look for `BootReceiver` registration
|
||||||
|
- Look for `DailyNotificationReceiver` registration
|
||||||
|
|
||||||
|
### iOS Implementation Details
|
||||||
|
|
||||||
|
#### Notification Scheduling
|
||||||
|
|
||||||
|
**File**: `ios/Plugin/DailyNotificationPlugin.swift`
|
||||||
|
|
||||||
|
- **Class**: `DailyNotificationPlugin` - **Lines 24-532**
|
||||||
|
- Main Capacitor plugin class
|
||||||
|
- Uses `UNUserNotificationCenter.current()` - **Line 26**
|
||||||
|
- Uses `BGTaskScheduler.shared` - **Line 27**
|
||||||
|
|
||||||
|
- **Function**: `scheduleUserNotification()` - **Lines 506-529**
|
||||||
|
- Schedules notification using UNUserNotificationCenter
|
||||||
|
- Creates `UNMutableNotificationContent` - **Line 507**
|
||||||
|
- Creates `UNTimeIntervalNotificationTrigger` - **Line 514**
|
||||||
|
- Adds request via `notificationCenter.add()` - **Line 522**
|
||||||
|
|
||||||
|
- **Function**: `scheduleBackgroundFetch()` - **Lines 495-504**
|
||||||
|
- Schedules BGTaskScheduler background fetch
|
||||||
|
- Creates `BGAppRefreshTaskRequest` - **Line 496**
|
||||||
|
- Submits via `backgroundTaskScheduler.submit()` - **Line 502**
|
||||||
|
|
||||||
|
**File**: `ios/Plugin/DailyNotificationScheduler.swift`
|
||||||
|
|
||||||
|
- **Class**: `DailyNotificationScheduler` - **Lines 20-236+**
|
||||||
|
- Manages UNUserNotificationCenter scheduling
|
||||||
|
|
||||||
|
- **Function**: `scheduleNotification()` - **Lines 133-198**
|
||||||
|
- Schedules notification with calendar trigger
|
||||||
|
- Creates `UNCalendarNotificationTrigger` - **Lines 172-175**
|
||||||
|
- Creates `UNNotificationRequest` - **Lines 178-182**
|
||||||
|
- Adds via `notificationCenter.add()` - **Line 185**
|
||||||
|
|
||||||
|
- **Function**: `cancelNotification()` - **Lines 205-213**
|
||||||
|
- Cancels notification by ID
|
||||||
|
- Uses `notificationCenter.removePendingNotificationRequests()` - **Line 206**
|
||||||
|
|
||||||
|
#### Background Tasks
|
||||||
|
|
||||||
|
**File**: `ios/Plugin/DailyNotificationBackgroundTasks.swift`
|
||||||
|
|
||||||
|
- Background task handling for BGTaskScheduler
|
||||||
|
- Register background task identifiers
|
||||||
|
- Handle background fetch execution
|
||||||
|
|
||||||
|
#### Persistence
|
||||||
|
|
||||||
|
**File**: `ios/Plugin/DailyNotificationPlugin.swift**
|
||||||
|
|
||||||
|
- **Note**: Check for UserDefaults, CoreData, or file-based storage
|
||||||
|
- Storage component: `var storage: DailyNotificationStorage?` - **Line 35**
|
||||||
|
- Scheduler component: `var scheduler: DailyNotificationScheduler?` - **Line 36**
|
||||||
|
|
||||||
|
#### iOS-2 Branch
|
||||||
|
|
||||||
|
- **Note**: Check `ios-2` branch for alternative implementations:
|
||||||
|
```bash
|
||||||
|
git checkout ios-2
|
||||||
|
# Compare ios/Plugin/ implementations
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Tools & Commands
|
||||||
|
|
||||||
|
### Android Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check scheduled alarms
|
||||||
|
adb shell dumpsys alarm | grep -i timesafari
|
||||||
|
|
||||||
|
# Force kill (not force-stop) - adjust package name based on test app
|
||||||
|
adb shell am kill com.timesafari.dailynotification
|
||||||
|
# Or for test apps:
|
||||||
|
# adb shell am kill com.timesafari.androidtestapp
|
||||||
|
# adb shell am kill <package-name-from-test-app-manifest>
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
adb logcat | grep -i "DN\|DailyNotification"
|
||||||
|
|
||||||
|
# Check WorkManager tasks
|
||||||
|
adb shell dumpsys jobscheduler | grep -i timesafari
|
||||||
|
|
||||||
|
# Build and install test app
|
||||||
|
cd test-apps/android-test-app
|
||||||
|
./gradlew installDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View device logs (requires Xcode)
|
||||||
|
xcrun simctl spawn booted log stream --predicate 'processImagePath contains "DailyNotification"'
|
||||||
|
|
||||||
|
# List pending notifications (requires app code)
|
||||||
|
# Use UNUserNotificationCenter.getPendingNotificationRequests()
|
||||||
|
|
||||||
|
# Build test app (from test-apps/ios-test-app)
|
||||||
|
# Use Xcode or:
|
||||||
|
cd test-apps/ios-test-app
|
||||||
|
# Follow build instructions in test app README
|
||||||
|
|
||||||
|
# Check ios-2 branch for alternative implementations
|
||||||
|
git checkout ios-2
|
||||||
|
# Compare ios/Plugin/ implementations between branches
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps After Exploration
|
||||||
|
|
||||||
|
Once this exploration is complete:
|
||||||
|
|
||||||
|
1. **Document findings** in the deliverables listed above
|
||||||
|
2. **Identify gaps** between current behavior and desired behavior
|
||||||
|
3. **Create implementation directives** to address gaps (if needed)
|
||||||
|
4. **Update plugin documentation** to accurately reflect platform limitations
|
||||||
|
5. **Update API documentation** with appropriate warnings and caveats
|
||||||
|
|
||||||
296
docs/improve-alarm-directives.md
Normal file
296
docs/improve-alarm-directives.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# ✅ DIRECTIVE: Improvements to Alarm/Schedule/Notification Directives for Capacitor Plugin
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Active Improvement Directive
|
||||||
|
|
||||||
|
## 0. Goal of This Improvement Directive
|
||||||
|
|
||||||
|
Unify, refine, and strengthen the existing alarm-behavior directives so they:
|
||||||
|
|
||||||
|
1. **Eliminate duplication** between the Android Alarm Persistence Directive and the Plugin Exploration Directive.
|
||||||
|
2. **Clarify scope and purpose** (one is exploration/investigation; the other is platform mechanics).
|
||||||
|
3. **Produce a single cohesive standard** to guide future plugin improvements, testing, and documentation.
|
||||||
|
4. **Streamline testing expectations** into executable, check-box-style matrices.
|
||||||
|
5. **Map OS-level limitations directly to plugin-level behavior** so the JS/TS API contract is unambiguous.
|
||||||
|
6. **Add missing iOS-specific limitations, guarantees, and required recovery patterns**.
|
||||||
|
7. **Provide an upgrade path from exploration → design decisions → implementation directives**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Structural Improvements to Apply
|
||||||
|
|
||||||
|
### 1.1 Split responsibilities into three documents (clear roles)
|
||||||
|
|
||||||
|
Your current directives mix *exploration*, *reference*, and *design rules*.
|
||||||
|
|
||||||
|
Improve by creating three clearly separated docs:
|
||||||
|
|
||||||
|
#### **Document A — Platform Capability Reference (Android/iOS)**
|
||||||
|
|
||||||
|
* Pure OS-level facts (no plugin logic).
|
||||||
|
* Use the Android Alarm Persistence Directive as the baseline.
|
||||||
|
* Add an equivalent iOS capability matrix.
|
||||||
|
* Keep strictly normative, minimal, stable.
|
||||||
|
|
||||||
|
#### **Document B — Plugin Behavior Exploration (Android/iOS)**
|
||||||
|
|
||||||
|
* Use the uploaded exploration directive as the baseline.
|
||||||
|
* Remove platform-mechanics explanations (moved to Document A).
|
||||||
|
* Replace vague descriptions with concrete, line-number-linked tasks.
|
||||||
|
* Add "expected vs actual" checklists to each test item.
|
||||||
|
|
||||||
|
#### **Document C — Plugin Requirements & Improvements**
|
||||||
|
|
||||||
|
* Generated after exploration.
|
||||||
|
* Defines the rules the plugin *must follow* to behave predictably.
|
||||||
|
* Defines recovery strategy (boot, reboot, missed alarms, force stop behavior).
|
||||||
|
* Defines JS API caveats and warnings.
|
||||||
|
|
||||||
|
**This file you're asking for now (improvement directive) becomes the origin of Document C.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Improvements Needed to Existing Directives
|
||||||
|
|
||||||
|
### 2.1 Reduce duplication in Android section
|
||||||
|
|
||||||
|
The exploration directive currently repeats much of the alarm persistence directive.
|
||||||
|
|
||||||
|
**Improve by:**
|
||||||
|
|
||||||
|
* Referencing the Android alarm document instead of replicating content.
|
||||||
|
* Summarizing Android limitations in 5–7 lines in the exploration document.
|
||||||
|
* Keeping full explanation *only* in the Android alarm persistence reference file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Add missing iOS counterpart to Android's capability matrix
|
||||||
|
|
||||||
|
You have a complete matrix for Android, but not iOS.
|
||||||
|
|
||||||
|
Add a **parallel iOS matrix**, including:
|
||||||
|
|
||||||
|
* Notification survives swipe → yes
|
||||||
|
* Notification survives reboot → yes (for calendar/time triggers)
|
||||||
|
* App logic runs in background → no
|
||||||
|
* Arbitrary code on trigger → no
|
||||||
|
* Recovery required → only if plugin has its own DB
|
||||||
|
|
||||||
|
This fixes asymmetry in current directives.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Clarify when plugin behavior depends on OS behavior
|
||||||
|
|
||||||
|
The exploration directive needs clearer labeling:
|
||||||
|
|
||||||
|
**Label each behavior as:**
|
||||||
|
|
||||||
|
* **OS-guaranteed** (iOS will fire pending notifications even when the app is dead)
|
||||||
|
* **Plugin-guaranteed** (plugin must reschedule alarms from DB)
|
||||||
|
* **Not allowed** (Android force-stop)
|
||||||
|
|
||||||
|
This removes ambiguity for plugin developers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 Introduce "Observed Behavior Table" in the exploration doc
|
||||||
|
|
||||||
|
Currently tests describe how to test but do not include space for results.
|
||||||
|
|
||||||
|
Add a table like:
|
||||||
|
|
||||||
|
| Scenario | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ------------------ | --------------------------- | ------------------------------ | ------------- | ----- |
|
||||||
|
| Swipe from recents | Fires | Fires | | |
|
||||||
|
| Device reboot | Does NOT fire automatically | Plugin must reschedule at boot | | |
|
||||||
|
|
||||||
|
This allows the exploration document to be executable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 Add "JS/TS API Contract" section to both directives
|
||||||
|
|
||||||
|
A critical missing piece.
|
||||||
|
|
||||||
|
Define what JavaScript developers can assume:
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* "Notification will still fire if the app is swiped from recents."
|
||||||
|
* "Notifications WILL NOT fire after Android Force Stop until the app is opened again."
|
||||||
|
* "Reboot behavior depends on platform: iOS preserves, Android destroys."
|
||||||
|
|
||||||
|
This section makes plugin behavior developer-friendly and predictable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 Strengthen direction on persistence strategy
|
||||||
|
|
||||||
|
The exploration directive asks "what is persisted?" but does not specify what *should* be persisted.
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
#### **Required Persistence Items**
|
||||||
|
|
||||||
|
* alarm_id
|
||||||
|
* trigger time
|
||||||
|
* repeat rule
|
||||||
|
* channel/priority
|
||||||
|
* payload
|
||||||
|
* time created, last modified
|
||||||
|
|
||||||
|
And:
|
||||||
|
|
||||||
|
#### **Required Recovery Points**
|
||||||
|
|
||||||
|
* Boot event
|
||||||
|
* App cold start
|
||||||
|
* App warm start
|
||||||
|
* App returning from background fetch
|
||||||
|
* User tapping notification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 Add iOS Background Execution Limits section
|
||||||
|
|
||||||
|
Currently missing.
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
* No repeating background execution APIs except BGTaskScheduler
|
||||||
|
* BGTaskScheduler requires minimum intervals
|
||||||
|
* Plugin cannot rely on background execution to reconstruct alarms
|
||||||
|
* Only notification center persists notifications
|
||||||
|
|
||||||
|
This is critical for plugin parity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8 Integrate "missed alarm recovery" requirements
|
||||||
|
|
||||||
|
The exploration directive asks whether plugin detects missed alarms.
|
||||||
|
The improvement directive must assert that it **must**.
|
||||||
|
|
||||||
|
Add requirement:
|
||||||
|
|
||||||
|
* If alarm time < now, and plugin is activated by reboot or user opening the app → plugin must generate a "missed alarm" event or notification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Rewrite Testing Protocols into Standardized Formats
|
||||||
|
|
||||||
|
### Replace long paragraphs with clear test tables, e.g.:
|
||||||
|
|
||||||
|
#### Android Reboot Test
|
||||||
|
|
||||||
|
| Step | Action |
|
||||||
|
| ---- | ------------------------------------------------ |
|
||||||
|
| 1 | Schedule alarm for 10 minutes later |
|
||||||
|
| 2 | Reboot device |
|
||||||
|
| 3 | Do not open app |
|
||||||
|
| 4 | Does alarm fire? (Expected: NO) |
|
||||||
|
| 5 | Open app |
|
||||||
|
| 6 | Does plugin detect missed alarm? (Expected: YES) |
|
||||||
|
|
||||||
|
Same for iOS, force-stop, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Add Missing Directive Sections
|
||||||
|
|
||||||
|
### 4.1 Policy Section
|
||||||
|
|
||||||
|
Define:
|
||||||
|
|
||||||
|
* Unsupported features
|
||||||
|
* Required permissions
|
||||||
|
* Required manifest entries
|
||||||
|
* Required notification channels
|
||||||
|
|
||||||
|
### 4.2 Versioning Requirements
|
||||||
|
|
||||||
|
Each change to alarm behavior is **breaking** and must have:
|
||||||
|
|
||||||
|
* MAJOR version bump
|
||||||
|
* Migration guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Final Improvement Directive (What to Produce Next)
|
||||||
|
|
||||||
|
Here is your actionable deliverable list:
|
||||||
|
|
||||||
|
### **Produce Three New Documents**
|
||||||
|
|
||||||
|
1. **Platform Reference Document**
|
||||||
|
|
||||||
|
* Android alarm rules
|
||||||
|
* iOS notification rules
|
||||||
|
* Both in tabular form
|
||||||
|
* (Rewrites + merges the two uploaded directives)
|
||||||
|
|
||||||
|
2. **Exploration Results Template**
|
||||||
|
|
||||||
|
* Table format for results
|
||||||
|
* Expected vs actual
|
||||||
|
* Direct code references
|
||||||
|
* Remove platform explanation duplication
|
||||||
|
|
||||||
|
3. **Plugin Requirements + Future Implementation Directive**
|
||||||
|
|
||||||
|
* Persistence spec
|
||||||
|
* Recovery spec
|
||||||
|
* JS/TS API contract
|
||||||
|
* Parity rules
|
||||||
|
* Android/iOS caveats
|
||||||
|
* Required test harness
|
||||||
|
|
||||||
|
### **Implement Major Improvements**
|
||||||
|
|
||||||
|
* Strengthen separation of concerns
|
||||||
|
* Add iOS parity
|
||||||
|
* Integrate plugin-level persistence + recovery
|
||||||
|
* Add test matrices
|
||||||
|
* Add clear developer contracts
|
||||||
|
* Add missed-alarm handling requirements
|
||||||
|
* Add design rules for exact alarms and background restrictions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. One-Sentence Summary
|
||||||
|
|
||||||
|
> **Rewrite the existing directives into three clear documents—platform reference, plugin exploration, and plugin implementation requirements—while adding iOS parity, recovery rules, persistence requirements, and standardized testing matrices, removing duplicated Android content, and specifying a clear JS/TS API contract.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Android Alarm Persistence Directive](./android-alarm-persistence-directive.md) - Source material for Document A
|
||||||
|
- [Explore Alarm Behavior Directive](./explore-alarm-behavior-directive.md) - Source material for Document B
|
||||||
|
- [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md) - Testing procedures
|
||||||
|
- [App Startup Recovery Solution](./app-startup-recovery-solution.md) - Recovery mechanisms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Create Document A**: Platform Capability Reference (Android/iOS)
|
||||||
|
2. **Create Document B**: Plugin Behavior Exploration Template
|
||||||
|
3. **Create Document C**: Plugin Requirements & Implementation Directive
|
||||||
|
4. **Execute exploration** using Document B
|
||||||
|
5. **Update Document C** with findings from exploration
|
||||||
|
6. **Implement improvements** based on Document C
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status Tracking
|
||||||
|
|
||||||
|
- [ ] Document A created
|
||||||
|
- [ ] Document B created
|
||||||
|
- [ ] Document C created
|
||||||
|
- [ ] Exploration executed
|
||||||
|
- [ ] Findings documented
|
||||||
|
- [ ] Improvements implemented
|
||||||
|
|
||||||
305
docs/platform-capability-reference.md
Normal file
305
docs/platform-capability-reference.md
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
# Platform Capability Reference: Android & iOS Alarm/Notification Behavior
|
||||||
|
|
||||||
|
**⚠️ DEPRECATED**: This document has been superseded by [01-platform-capability-reference.md](./alarms/01-platform-capability-reference.md) as part of the unified alarm documentation structure.
|
||||||
|
|
||||||
|
**See**: [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) for the new documentation structure.
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: **DEPRECATED** - Superseded by unified structure
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document provides **pure OS-level facts** about alarm and notification capabilities on Android and iOS. It contains no plugin-specific logic—only platform mechanics that affect plugin design.
|
||||||
|
|
||||||
|
This is a **reference document** to be consulted when designing plugin behavior, not an implementation guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Principles
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
Android does **not** guarantee persistence of alarms across process death, swipes, or reboot.
|
||||||
|
|
||||||
|
It is the app's responsibility to **persist alarm definitions** and **re-schedule them** under allowed system conditions.
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
iOS **does** persist scheduled local notifications across app termination and device reboot, but:
|
||||||
|
|
||||||
|
* App code does **not** run when notifications fire (unless user interacts)
|
||||||
|
* Background execution is severely limited
|
||||||
|
* Plugin must persist its own state if it needs to track or recover missed notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Android Alarm Capability Matrix
|
||||||
|
|
||||||
|
| Scenario | Will Alarm Fire? | OS Behavior | App Responsibility |
|
||||||
|
| --------------------------------------- | --------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------- |
|
||||||
|
| **Swipe from Recents** | ✅ Yes | AlarmManager resurrects the app process | None (OS handles) |
|
||||||
|
| **App silently killed by OS** | ✅ Yes | AlarmManager still holds scheduled alarms | None (OS handles) |
|
||||||
|
| **Device Reboot** | ❌ No (auto) / ✅ Yes (if you reschedule) | All alarms wiped on reboot | Must reschedule from persistent storage on boot |
|
||||||
|
| **Doze Mode** | ⚠️ Only "exact" alarms | Inexact alarms deferred; exact alarms allowed | Must use `setExactAndAllowWhileIdle` |
|
||||||
|
| **Force Stop** | ❌ Never | Android blocks all callbacks + receivers until next user launch | Cannot bypass; must detect on app restart |
|
||||||
|
| **User reopens app** | ✅ You may reschedule & recover | App process restarted | Must detect missed alarms and reschedule future ones |
|
||||||
|
| **PendingIntent from user interaction** | ✅ If triggered by user | User action unlocks the app | None (OS handles) |
|
||||||
|
|
||||||
|
### Android Allowed Behaviors
|
||||||
|
|
||||||
|
#### 2.1 Alarms survive UI kills (swipe from recents)
|
||||||
|
|
||||||
|
`AlarmManager.setExactAndAllowWhileIdle(...)` alarms **will fire** even after:
|
||||||
|
|
||||||
|
* App is swiped away
|
||||||
|
* App process is killed by the OS
|
||||||
|
|
||||||
|
The OS recreates your app's process to deliver the `PendingIntent`.
|
||||||
|
|
||||||
|
**Required API**: `setExactAndAllowWhileIdle()` or `setAlarmClock()`
|
||||||
|
|
||||||
|
#### 2.2 Alarms can be preserved across device reboot
|
||||||
|
|
||||||
|
Android wipes all alarms on reboot, but **you may recreate them**.
|
||||||
|
|
||||||
|
**Required Components**:
|
||||||
|
|
||||||
|
1. Persist all alarms in storage (Room DB or SharedPreferences)
|
||||||
|
2. Add a `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED` broadcast receiver
|
||||||
|
3. On boot, load all enabled alarms and reschedule them using AlarmManager
|
||||||
|
|
||||||
|
**Permissions required**: `RECEIVE_BOOT_COMPLETED`
|
||||||
|
|
||||||
|
**Conditions**: User must have launched your app at least once before reboot
|
||||||
|
|
||||||
|
#### 2.3 Alarms can fire full-screen notifications and wake the device
|
||||||
|
|
||||||
|
**Required API**: `setFullScreenIntent(...)`, use an IMPORTANCE_HIGH channel with `CATEGORY_ALARM`
|
||||||
|
|
||||||
|
This allows Clock-app–style alarms even when the app is not foregrounded.
|
||||||
|
|
||||||
|
#### 2.4 Alarms can be restored after app restart
|
||||||
|
|
||||||
|
If the user re-opens the app (direct user action), you may:
|
||||||
|
|
||||||
|
* Scan the persistent DB
|
||||||
|
* Detect "missed" alarms
|
||||||
|
* Reschedule future alarms
|
||||||
|
* Fire "missed alarm" notifications
|
||||||
|
* Reconstruct WorkManager/JobScheduler tasks wiped by OS
|
||||||
|
|
||||||
|
**Required**: Create a `ReactivationManager` that runs on every app launch
|
||||||
|
|
||||||
|
### Android Forbidden Behaviors
|
||||||
|
|
||||||
|
#### 3.1 You cannot survive "Force Stop"
|
||||||
|
|
||||||
|
**Settings → Apps → YourApp → Force Stop** triggers:
|
||||||
|
|
||||||
|
* Removal of all alarms
|
||||||
|
* Removal of WorkManager tasks
|
||||||
|
* Blocking of all broadcast receivers (including BOOT_COMPLETED)
|
||||||
|
* Blocking of all JobScheduler jobs
|
||||||
|
* Blocking of AlarmManager callbacks
|
||||||
|
* Your app will NOT run until the user manually launches it again
|
||||||
|
|
||||||
|
**Directive**: Accept that FORCE STOP is a hard kill. No scheduling, alarms, jobs, or receivers may execute afterward.
|
||||||
|
|
||||||
|
#### 3.2 You cannot auto-resume after "Force Stop"
|
||||||
|
|
||||||
|
You may only resume tasks when:
|
||||||
|
|
||||||
|
* The user opens your app
|
||||||
|
* The user taps a notification belonging to your app
|
||||||
|
* The user interacts with a widget/deep link
|
||||||
|
* Another app explicitly targets your component
|
||||||
|
|
||||||
|
**Directive**: Provide user-facing reactivation pathways (icon, widget, notification).
|
||||||
|
|
||||||
|
#### 3.3 Alarms cannot be preserved solely in RAM
|
||||||
|
|
||||||
|
Android can kill your app's RAM state at any time.
|
||||||
|
|
||||||
|
**Directive**: All alarm data must be persisted in durable storage.
|
||||||
|
|
||||||
|
#### 3.4 You cannot bypass Doze or battery optimization restrictions without permission
|
||||||
|
|
||||||
|
Doze may defer inexact alarms; exact alarms with `setExactAndAllowWhileIdle` are allowed.
|
||||||
|
|
||||||
|
**Required Permission**: `SCHEDULE_EXACT_ALARM` on Android 12+ (API 31+)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. iOS Notification Capability Matrix
|
||||||
|
|
||||||
|
| Scenario | Will Notification Fire? | OS Behavior | App Responsibility |
|
||||||
|
| --------------------------------------- | ----------------------- | -------------------------------------------------------------------- | ----------------------------------------------------- |
|
||||||
|
| **Swipe from App Switcher** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) |
|
||||||
|
| **App Terminated by System** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) |
|
||||||
|
| **Device Reboot** | ✅ Yes (for calendar/time triggers) | iOS persists scheduled local notifications across reboot | None for notifications; must persist own state if needed |
|
||||||
|
| **App Force Quit (swipe away)** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) |
|
||||||
|
| **Background Execution** | ❌ No arbitrary code | Only BGTaskScheduler with strict limits | Cannot rely on background execution for recovery |
|
||||||
|
| **Notification Fires** | ✅ Yes | Notification displayed; app code does NOT run unless user interacts | Must handle missed notifications on next app launch |
|
||||||
|
| **User Taps Notification** | ✅ Yes | App launched; code can run | Can detect and handle missed notifications |
|
||||||
|
|
||||||
|
### iOS Allowed Behaviors
|
||||||
|
|
||||||
|
#### 3.1 Notifications survive app termination
|
||||||
|
|
||||||
|
`UNUserNotificationCenter` scheduled notifications **will fire** even after:
|
||||||
|
|
||||||
|
* App is swiped away from app switcher
|
||||||
|
* App is terminated by system
|
||||||
|
* Device reboots (for calendar/time-based triggers)
|
||||||
|
|
||||||
|
**Required API**: `UNUserNotificationCenter.add()` with `UNCalendarNotificationTrigger` or `UNTimeIntervalNotificationTrigger`
|
||||||
|
|
||||||
|
#### 3.2 Notifications persist across device reboot
|
||||||
|
|
||||||
|
iOS **automatically** persists scheduled local notifications across reboot.
|
||||||
|
|
||||||
|
**No app code required** for basic notification persistence.
|
||||||
|
|
||||||
|
**Limitation**: Only calendar and time-based triggers persist. Location-based triggers do not.
|
||||||
|
|
||||||
|
#### 3.3 Background tasks for prefetching
|
||||||
|
|
||||||
|
**Required API**: `BGTaskScheduler` with `BGAppRefreshTaskRequest`
|
||||||
|
|
||||||
|
**Limitations**:
|
||||||
|
|
||||||
|
* Minimum interval between tasks (system-controlled, typically hours)
|
||||||
|
* System decides when to execute (not guaranteed)
|
||||||
|
* Cannot rely on background execution for alarm recovery
|
||||||
|
* Must schedule next task immediately after current one completes
|
||||||
|
|
||||||
|
### iOS Forbidden Behaviors
|
||||||
|
|
||||||
|
#### 4.1 App code does not run when notification fires
|
||||||
|
|
||||||
|
When a scheduled notification fires:
|
||||||
|
|
||||||
|
* Notification is displayed to user
|
||||||
|
* **No app code executes** unless user taps the notification
|
||||||
|
* Cannot run arbitrary code at notification time
|
||||||
|
|
||||||
|
**Workaround**: Use notification actions or handle missed notifications on next app launch.
|
||||||
|
|
||||||
|
#### 4.2 No repeating background execution
|
||||||
|
|
||||||
|
iOS does not provide repeating background execution APIs except:
|
||||||
|
|
||||||
|
* `BGTaskScheduler` (system-controlled, not guaranteed)
|
||||||
|
* Background fetch (deprecated, unreliable)
|
||||||
|
|
||||||
|
**Directive**: Plugin cannot rely on background execution to reconstruct alarms. Must persist state and recover on app launch.
|
||||||
|
|
||||||
|
#### 4.3 No arbitrary code on notification trigger
|
||||||
|
|
||||||
|
Unlike Android's `PendingIntent` which can execute code, iOS notifications only:
|
||||||
|
|
||||||
|
* Display to user
|
||||||
|
* Launch app if user taps
|
||||||
|
* Execute notification action handlers (if configured)
|
||||||
|
|
||||||
|
**Directive**: All recovery logic must run on app launch, not at notification time.
|
||||||
|
|
||||||
|
#### 4.4 Background execution limits
|
||||||
|
|
||||||
|
**BGTaskScheduler Limitations**:
|
||||||
|
|
||||||
|
* Minimum intervals between tasks (system-controlled)
|
||||||
|
* System may defer or skip tasks
|
||||||
|
* Tasks have time budgets (typically 30 seconds)
|
||||||
|
* Cannot guarantee execution timing
|
||||||
|
|
||||||
|
**Directive**: Use BGTaskScheduler for prefetching only, not for critical scheduling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Cross-Platform Comparison
|
||||||
|
|
||||||
|
| Feature | Android | iOS |
|
||||||
|
| -------------------------------- | --------------------------------------- | --------------------------------------------- |
|
||||||
|
| **Survives swipe/termination** | ✅ Yes (with exact alarms) | ✅ Yes (automatic) |
|
||||||
|
| **Survives reboot** | ❌ No (must reschedule) | ✅ Yes (automatic for calendar/time triggers) |
|
||||||
|
| **App code runs on trigger** | ✅ Yes (via PendingIntent) | ❌ No (only if user interacts) |
|
||||||
|
| **Background execution** | ✅ WorkManager, JobScheduler | ⚠️ Limited (BGTaskScheduler only) |
|
||||||
|
| **Force stop equivalent** | ✅ Force Stop (hard kill) | ❌ No user-facing equivalent |
|
||||||
|
| **Boot recovery required** | ✅ Yes (must implement) | ❌ No (OS handles) |
|
||||||
|
| **Missed alarm detection** | ✅ Must implement on app launch | ✅ Must implement on app launch |
|
||||||
|
| **Exact timing** | ✅ Yes (with permission) | ⚠️ ±180s tolerance |
|
||||||
|
| **Repeating notifications** | ✅ Must reschedule each occurrence | ✅ Can use `repeats: true` in trigger |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Required Platform APIs
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
**Alarm Scheduling**:
|
||||||
|
* `AlarmManager.setExactAndAllowWhileIdle()` - Android 6.0+ (API 23+)
|
||||||
|
* `AlarmManager.setAlarmClock()` - Android 5.0+ (API 21+)
|
||||||
|
* `AlarmManager.setExact()` - Android 4.4+ (API 19+)
|
||||||
|
|
||||||
|
**Permissions**:
|
||||||
|
* `RECEIVE_BOOT_COMPLETED` - Boot receiver
|
||||||
|
* `SCHEDULE_EXACT_ALARM` - Android 12+ (API 31+)
|
||||||
|
|
||||||
|
**Background Work**:
|
||||||
|
* `WorkManager` - Deferrable background work
|
||||||
|
* `JobScheduler` - Alternative (API 21+)
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
**Notification Scheduling**:
|
||||||
|
* `UNUserNotificationCenter.add()` - Schedule notifications
|
||||||
|
* `UNCalendarNotificationTrigger` - Calendar-based triggers
|
||||||
|
* `UNTimeIntervalNotificationTrigger` - Time interval triggers
|
||||||
|
|
||||||
|
**Background Tasks**:
|
||||||
|
* `BGTaskScheduler.submit()` - Schedule background tasks
|
||||||
|
* `BGAppRefreshTaskRequest` - Background fetch requests
|
||||||
|
|
||||||
|
**Permissions**:
|
||||||
|
* Notification authorization (requested at runtime)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Platform-Specific Constraints Summary
|
||||||
|
|
||||||
|
### Android Constraints
|
||||||
|
|
||||||
|
1. **Reboot**: All alarms wiped; must reschedule from persistent storage
|
||||||
|
2. **Force Stop**: Hard kill; cannot bypass until user opens app
|
||||||
|
3. **Doze**: Inexact alarms deferred; must use exact alarms
|
||||||
|
4. **Exact Alarm Permission**: Required on Android 12+ for precise timing
|
||||||
|
5. **Boot Receiver**: Must be registered and handle `BOOT_COMPLETED`
|
||||||
|
|
||||||
|
### iOS Constraints
|
||||||
|
|
||||||
|
1. **Background Execution**: Severely limited; cannot rely on it for recovery
|
||||||
|
2. **Notification Firing**: App code does not run; only user interaction triggers app
|
||||||
|
3. **Timing Tolerance**: ±180 seconds for calendar triggers
|
||||||
|
4. **BGTaskScheduler**: System-controlled; not guaranteed execution
|
||||||
|
5. **State Persistence**: Must persist own state if tracking missed notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md) - Uses this reference
|
||||||
|
- [Plugin Requirements & Implementation](./plugin-requirements-implementation.md) - Implementation based on this reference
|
||||||
|
- [Android Alarm Persistence Directive](./android-alarm-persistence-directive.md) - Original Android reference
|
||||||
|
- [Improve Alarm Directives](./improve-alarm-directives.md) - Improvement directive
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v1.0** (November 2025): Initial platform capability reference
|
||||||
|
- Android alarm matrix
|
||||||
|
- iOS notification matrix
|
||||||
|
- Cross-platform comparison
|
||||||
|
|
||||||
363
docs/plugin-behavior-exploration-template.md
Normal file
363
docs/plugin-behavior-exploration-template.md
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
# Plugin Behavior Exploration Template
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Active Exploration Template
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document provides an **executable template** for exploring and documenting the current plugin's alarm/schedule/notification behavior on Android and iOS.
|
||||||
|
|
||||||
|
**Use this template to**:
|
||||||
|
1. Test plugin behavior across different scenarios
|
||||||
|
2. Document expected vs actual results
|
||||||
|
3. Identify gaps between current behavior and platform capabilities
|
||||||
|
4. Generate findings for the Plugin Requirements document
|
||||||
|
|
||||||
|
**Reference**: See [Platform Capability Reference](./platform-capability-reference.md) for OS-level facts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Quick Reference: Platform Capabilities
|
||||||
|
|
||||||
|
**Android**: See [Platform Capability Reference - Android Section](./platform-capability-reference.md#2-android-alarm-capability-matrix)
|
||||||
|
|
||||||
|
**iOS**: See [Platform Capability Reference - iOS Section](./platform-capability-reference.md#3-ios-notification-capability-matrix)
|
||||||
|
|
||||||
|
**Key Differences**:
|
||||||
|
* Android: Alarms wiped on reboot; must reschedule
|
||||||
|
* iOS: Notifications persist across reboot automatically
|
||||||
|
* Android: App code runs when alarm fires
|
||||||
|
* iOS: App code does NOT run when notification fires (unless user interacts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Android Exploration
|
||||||
|
|
||||||
|
### 1.1 Code-Level Inspection Checklist
|
||||||
|
|
||||||
|
**Source Locations**:
|
||||||
|
- Plugin: `android/src/main/java/com/timesafari/dailynotification/`
|
||||||
|
- Test App: `test-apps/android-test-app/`
|
||||||
|
- Manifest: `test-apps/android-test-app/app/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
| Task | File/Function | Line | Status | Notes |
|
||||||
|
| ---- | ------------- | ---- | ------ | ----- |
|
||||||
|
| Locate main plugin class | `DailyNotificationPlugin.kt` | 1302 | ☐ | `scheduleDailyNotification()` |
|
||||||
|
| Identify alarm scheduling | `NotifyReceiver.kt` | 92 | ☐ | `scheduleExactNotification()` |
|
||||||
|
| Check AlarmManager usage | `NotifyReceiver.kt` | 219, 223, 231 | ☐ | `setAlarmClock()`, `setExactAndAllowWhileIdle()`, `setExact()` |
|
||||||
|
| Check WorkManager usage | `FetchWorker.kt` | 31 | ☐ | `scheduleFetch()` |
|
||||||
|
| Check notification display | `DailyNotificationWorker.java` | 200+ | ☐ | `displayNotification()` |
|
||||||
|
| Check boot receiver | `BootReceiver.kt` | 24 | ☐ | `onReceive()` handles `BOOT_COMPLETED` |
|
||||||
|
| Check persistence | `DailyNotificationPlugin.kt` | 1393+ | ☐ | Room database storage |
|
||||||
|
| Check exact alarm permission | `DailyNotificationPlugin.kt` | 1309 | ☐ | `canScheduleExactAlarms()` |
|
||||||
|
| Check manifest permissions | `AndroidManifest.xml` | - | ☐ | `RECEIVE_BOOT_COMPLETED`, `SCHEDULE_EXACT_ALARM` |
|
||||||
|
|
||||||
|
### 1.2 Behavior Testing Matrix
|
||||||
|
|
||||||
|
#### Test 1: Base Case
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule alarm 2 minutes in future | - | Alarm scheduled | ☐ | |
|
||||||
|
| 2 | Leave app in foreground/background | - | - | ☐ | |
|
||||||
|
| 3 | Wait for trigger time | Alarm fires | Notification displayed | ☐ | |
|
||||||
|
| 4 | Check logs | - | No errors | ☐ | |
|
||||||
|
|
||||||
|
**Code Reference**: `NotifyReceiver.scheduleExactNotification()` line 92
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 2: Swipe from Recents
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule alarm 2-5 minutes in future | - | Alarm scheduled | ☐ | |
|
||||||
|
| 2 | Swipe app away from recents | - | - | ☐ | |
|
||||||
|
| 3 | Wait for trigger time | ✅ Alarm fires (OS resurrects process) | ✅ Notification displayed | ☐ | |
|
||||||
|
| 4 | Check app state on wake | Cold start | App process recreated | ☐ | |
|
||||||
|
| 5 | Check logs | - | No errors | ☐ | |
|
||||||
|
|
||||||
|
**Code Reference**: `NotifyReceiver.scheduleExactNotification()` uses `setAlarmClock()` line 219
|
||||||
|
|
||||||
|
**Platform Behavior**: OS-guaranteed (Android AlarmManager)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 3: OS Kill (Memory Pressure)
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule alarm 2-5 minutes in future | - | Alarm scheduled | ☐ | |
|
||||||
|
| 2 | Force kill via `adb shell am kill <package>` | - | - | ☐ | |
|
||||||
|
| 3 | Wait for trigger time | ✅ Alarm fires | ✅ Notification displayed | ☐ | |
|
||||||
|
| 4 | Check logs | - | No errors | ☐ | |
|
||||||
|
|
||||||
|
**Platform Behavior**: OS-guaranteed (Android AlarmManager)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 4: Device Reboot
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule alarm 10 minutes in future | - | Alarm scheduled | ☐ | |
|
||||||
|
| 2 | Reboot device | - | - | ☐ | |
|
||||||
|
| 3 | Do NOT open app | ❌ Alarm does NOT fire | ❌ No notification | ☐ | |
|
||||||
|
| 4 | Wait past scheduled time | ❌ No automatic firing | ❌ No notification | ☐ | |
|
||||||
|
| 5 | Open app manually | - | Plugin detects missed alarm | ☐ | |
|
||||||
|
| 6 | Check missed alarm handling | - | ✅ Missed alarm detected | ☐ | |
|
||||||
|
| 7 | Check rescheduling | - | ✅ Future alarms rescheduled | ☐ | |
|
||||||
|
|
||||||
|
**Code Reference**:
|
||||||
|
- Boot receiver: `BootReceiver.kt` line 24
|
||||||
|
- Rescheduling: `BootReceiver.kt` line 38+
|
||||||
|
|
||||||
|
**Platform Behavior**: Plugin-guaranteed (must implement boot receiver)
|
||||||
|
|
||||||
|
**Expected Plugin Behavior**: Plugin must reschedule from database on boot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 5: Android Force Stop
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule alarm | - | Alarm scheduled | ☐ | |
|
||||||
|
| 2 | Go to Settings → Apps → [App] → Force Stop | ❌ All alarms removed | ❌ All alarms removed | ☐ | |
|
||||||
|
| 3 | Wait for trigger time | ❌ Alarm does NOT fire | ❌ No notification | ☐ | |
|
||||||
|
| 4 | Open app again | - | Plugin detects missed alarm | ☐ | |
|
||||||
|
| 5 | Check recovery | - | ✅ Missed alarm detected | ☐ | |
|
||||||
|
| 6 | Check rescheduling | - | ✅ Future alarms rescheduled | ☐ | |
|
||||||
|
|
||||||
|
**Platform Behavior**: Not allowed (Android hard kill)
|
||||||
|
|
||||||
|
**Expected Plugin Behavior**: Plugin must detect and recover on app restart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 6: Exact Alarm Permission (Android 12+)
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Revoke exact alarm permission | - | - | ☐ | |
|
||||||
|
| 2 | Attempt to schedule alarm | - | Plugin requests permission | ☐ | |
|
||||||
|
| 3 | Check settings opened | - | ✅ Settings opened | ☐ | |
|
||||||
|
| 4 | Grant permission | - | - | ☐ | |
|
||||||
|
| 5 | Schedule alarm | - | ✅ Alarm scheduled | ☐ | |
|
||||||
|
| 6 | Verify alarm fires | ✅ Alarm fires | ✅ Notification displayed | ☐ | |
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationPlugin.kt` line 1309, 1314-1324
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 Persistence Investigation
|
||||||
|
|
||||||
|
| Item | Expected | Actual | Code Reference | Notes |
|
||||||
|
| ---- | -------- | ------ | -------------- | ----- |
|
||||||
|
| Alarm ID stored | ✅ Yes | ☐ | `DailyNotificationPlugin.kt` line 1393+ | |
|
||||||
|
| Trigger time stored | ✅ Yes | ☐ | Room database | |
|
||||||
|
| Repeat rule stored | ✅ Yes | ☐ | Schedule entity | |
|
||||||
|
| Channel/priority stored | ✅ Yes | ☐ | NotificationContentEntity | |
|
||||||
|
| Payload stored | ✅ Yes | ☐ | ContentCache | |
|
||||||
|
| Time created/modified | ✅ Yes | ☐ | Entity timestamps | |
|
||||||
|
|
||||||
|
**Storage Location**: Room database (`DailyNotificationDatabase`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 Recovery Points Investigation
|
||||||
|
|
||||||
|
| Recovery Point | Expected Behavior | Actual Behavior | Code Reference | Notes |
|
||||||
|
| -------------- | ----------------- | --------------- | -------------- | ----- |
|
||||||
|
| Boot event | ✅ Reschedule all alarms | ☐ | `BootReceiver.kt` line 24 | |
|
||||||
|
| App cold start | ✅ Detect missed alarms | ☐ | Check plugin initialization | |
|
||||||
|
| App warm start | ✅ Verify active alarms | ☐ | Check plugin initialization | |
|
||||||
|
| Background fetch return | ⚠️ May reschedule | ☐ | `FetchWorker.kt` | |
|
||||||
|
| User taps notification | ✅ Launch app | ☐ | Notification intent | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. iOS Exploration
|
||||||
|
|
||||||
|
### 2.1 Code-Level Inspection Checklist
|
||||||
|
|
||||||
|
**Source Locations**:
|
||||||
|
- Plugin: `ios/Plugin/`
|
||||||
|
- Test App: `test-apps/ios-test-app/`
|
||||||
|
- Alternative: Check `ios-2` branch
|
||||||
|
|
||||||
|
| Task | File/Function | Line | Status | Notes |
|
||||||
|
| ---- | ------------- | ---- | ------ | ----- |
|
||||||
|
| Locate main plugin class | `DailyNotificationPlugin.swift` | 506 | ☐ | `scheduleUserNotification()` |
|
||||||
|
| Identify notification scheduling | `DailyNotificationScheduler.swift` | 133 | ☐ | `scheduleNotification()` |
|
||||||
|
| Check UNUserNotificationCenter usage | `DailyNotificationScheduler.swift` | 185 | ☐ | `notificationCenter.add()` |
|
||||||
|
| Check trigger types | `DailyNotificationScheduler.swift` | 172 | ☐ | `UNCalendarNotificationTrigger` |
|
||||||
|
| Check BGTaskScheduler usage | `DailyNotificationPlugin.swift` | 495 | ☐ | `scheduleBackgroundFetch()` |
|
||||||
|
| Check persistence | `DailyNotificationPlugin.swift` | 35 | ☐ | `storage: DailyNotificationStorage?` |
|
||||||
|
| Check app launch recovery | `DailyNotificationPlugin.swift` | 42 | ☐ | `load()` method |
|
||||||
|
|
||||||
|
### 2.2 Behavior Testing Matrix
|
||||||
|
|
||||||
|
#### Test 1: Base Case
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule notification 2-5 minutes in future | - | Notification scheduled | ☐ | |
|
||||||
|
| 2 | Leave app backgrounded | - | - | ☐ | |
|
||||||
|
| 3 | Wait for trigger time | ✅ Notification fires | ✅ Notification displayed | ☐ | |
|
||||||
|
| 4 | Check logs | - | No errors | ☐ | |
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationScheduler.scheduleNotification()` line 133
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 2: Swipe App Away
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule notification 2-5 minutes in future | - | Notification scheduled | ☐ | |
|
||||||
|
| 2 | Swipe app away from app switcher | - | - | ☐ | |
|
||||||
|
| 3 | Wait for trigger time | ✅ Notification fires (OS handles) | ✅ Notification displayed | ☐ | |
|
||||||
|
| 4 | Check app state | App terminated | App not running | ☐ | |
|
||||||
|
|
||||||
|
**Platform Behavior**: OS-guaranteed (iOS UNUserNotificationCenter)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 3: Device Reboot
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule notification for future time | - | Notification scheduled | ☐ | |
|
||||||
|
| 2 | Reboot device | - | - | ☐ | |
|
||||||
|
| 3 | Do NOT open app | ✅ Notification fires (OS persists) | ✅ Notification displayed | ☐ | |
|
||||||
|
| 4 | Check notification timing | ✅ On time (±180s tolerance) | ✅ On time | ☐ | |
|
||||||
|
|
||||||
|
**Platform Behavior**: OS-guaranteed (iOS persists calendar/time triggers)
|
||||||
|
|
||||||
|
**Note**: Only calendar and time-based triggers persist. Location triggers do not.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 4: Hard Termination & Relaunch
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule repeating notifications | - | Notifications scheduled | ☐ | |
|
||||||
|
| 2 | Terminate app via Xcode/switcher | - | - | ☐ | |
|
||||||
|
| 3 | Allow some triggers to occur | ✅ Notifications fire | ✅ Notifications displayed | ☐ | |
|
||||||
|
| 4 | Reopen app | - | Plugin checks for missed events | ☐ | |
|
||||||
|
| 5 | Check missed event detection | ⚠️ May detect | ☐ | Plugin-specific |
|
||||||
|
| 6 | Check state recovery | ⚠️ May recover | ☐ | Plugin-specific |
|
||||||
|
|
||||||
|
**Platform Behavior**: OS-guaranteed for notifications; Plugin-guaranteed for missed event detection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Test 5: Background Execution Limits
|
||||||
|
|
||||||
|
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
|
||||||
|
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
|
||||||
|
| 1 | Schedule BGTaskScheduler task | - | Task scheduled | ☐ | |
|
||||||
|
| 2 | Wait for system to execute | ⚠️ System-controlled | ⚠️ May not execute | ☐ | |
|
||||||
|
| 3 | Check execution timing | ⚠️ Not guaranteed | ⚠️ Not guaranteed | ☐ | |
|
||||||
|
| 4 | Check time budget | ⚠️ ~30 seconds | ⚠️ Limited time | ☐ | |
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationPlugin.scheduleBackgroundFetch()` line 495
|
||||||
|
|
||||||
|
**Platform Behavior**: System-controlled (not guaranteed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Persistence Investigation
|
||||||
|
|
||||||
|
| Item | Expected | Actual | Code Reference | Notes |
|
||||||
|
| ---- | -------- | ------ | -------------- | ----- |
|
||||||
|
| Notification ID stored | ✅ Yes (in UNUserNotificationCenter) | ☐ | `UNNotificationRequest` | |
|
||||||
|
| Plugin-side storage | ⚠️ May not exist | ☐ | `DailyNotificationStorage?` | |
|
||||||
|
| Trigger time stored | ✅ Yes (in trigger) | ☐ | `UNCalendarNotificationTrigger` | |
|
||||||
|
| Repeat rule stored | ✅ Yes (in trigger) | ☐ | `repeats: true/false` | |
|
||||||
|
| Payload stored | ✅ Yes (in userInfo) | ☐ | `notificationContent.userInfo` | |
|
||||||
|
|
||||||
|
**Storage Location**:
|
||||||
|
- Primary: UNUserNotificationCenter (OS-managed)
|
||||||
|
- Secondary: Plugin storage (if implemented)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 Recovery Points Investigation
|
||||||
|
|
||||||
|
| Recovery Point | Expected Behavior | Actual Behavior | Code Reference | Notes |
|
||||||
|
| -------------- | ----------------- | --------------- | -------------- | ----- |
|
||||||
|
| Boot event | ✅ Notifications fire automatically | ☐ | OS handles | |
|
||||||
|
| App cold start | ⚠️ May detect missed notifications | ☐ | Check `load()` method | |
|
||||||
|
| App warm start | ⚠️ May verify pending notifications | ☐ | Check plugin initialization | |
|
||||||
|
| Background fetch | ⚠️ May reschedule | ☐ | `BGTaskScheduler` | |
|
||||||
|
| User taps notification | ✅ App launched | ☐ | Notification action | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Cross-Platform Comparison
|
||||||
|
|
||||||
|
### 3.1 Observed Behavior Summary
|
||||||
|
|
||||||
|
| Scenario | Android (Observed) | iOS (Observed) | Platform Difference |
|
||||||
|
| -------- | ------------------ | -------------- | ------------------- |
|
||||||
|
| Swipe/termination | ☐ | ☐ | Both should work |
|
||||||
|
| Reboot | ☐ | ☐ | iOS auto, Android manual |
|
||||||
|
| Force stop | ☐ | N/A | Android only |
|
||||||
|
| App code on trigger | ☐ | ☐ | Android yes, iOS no |
|
||||||
|
| Background execution | ☐ | ☐ | Android more flexible |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Findings & Gaps
|
||||||
|
|
||||||
|
### 4.1 Android Gaps
|
||||||
|
|
||||||
|
| Gap | Severity | Description | Recommendation |
|
||||||
|
| --- | -------- | ----------- | -------------- |
|
||||||
|
| Boot recovery | ☐ High/Medium/Low | Does plugin reschedule on boot? | Implement if missing |
|
||||||
|
| Missed alarm detection | ☐ High/Medium/Low | Does plugin detect missed alarms? | Implement if missing |
|
||||||
|
| Force stop recovery | ☐ High/Medium/Low | Does plugin recover after force stop? | Implement if missing |
|
||||||
|
| Persistence completeness | ☐ High/Medium/Low | Are all required fields persisted? | Verify and add if missing |
|
||||||
|
|
||||||
|
### 4.2 iOS Gaps
|
||||||
|
|
||||||
|
| Gap | Severity | Description | Recommendation |
|
||||||
|
| --- | -------- | ----------- | -------------- |
|
||||||
|
| Missed notification detection | ☐ High/Medium/Low | Does plugin detect missed notifications? | Implement if missing |
|
||||||
|
| Plugin-side persistence | ☐ High/Medium/Low | Does plugin persist state separately? | Consider if needed |
|
||||||
|
| Background task reliability | ☐ High/Medium/Low | Can plugin rely on BGTaskScheduler? | Document limitations |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Deliverables from This Exploration
|
||||||
|
|
||||||
|
After completing this exploration, generate:
|
||||||
|
|
||||||
|
1. **ALARMS_BEHAVIOR_MATRIX.md** - Completed test results
|
||||||
|
2. **PLUGIN_ALARM_LIMITATIONS.md** - Documented limitations and gaps
|
||||||
|
3. **Annotated code pointers** - Code locations with findings
|
||||||
|
4. **Open Questions / TODOs** - Unresolved issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Platform Capability Reference](./platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Plugin Requirements & Implementation](./plugin-requirements-implementation.md) - Requirements based on findings
|
||||||
|
- [Improve Alarm Directives](./improve-alarm-directives.md) - Improvement directive
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for Explorers
|
||||||
|
|
||||||
|
* Fill in checkboxes (☐) as you complete each test
|
||||||
|
* Document actual results in "Actual Result" columns
|
||||||
|
* Add notes for any unexpected behavior
|
||||||
|
* Reference code locations when documenting findings
|
||||||
|
* Update "Findings & Gaps" section as you discover issues
|
||||||
|
* Use platform capability reference to understand expected OS behavior
|
||||||
|
|
||||||
552
docs/plugin-requirements-implementation.md
Normal file
552
docs/plugin-requirements-implementation.md
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
# Plugin Requirements & Implementation Directive
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Active Requirements - Implementation Guide
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document defines the **rules the plugin must follow** to behave predictably across Android and iOS platforms. It specifies:
|
||||||
|
|
||||||
|
* Persistence requirements
|
||||||
|
* Recovery strategies
|
||||||
|
* JS/TS API contract and caveats
|
||||||
|
* Missed alarm handling
|
||||||
|
* Platform-specific requirements
|
||||||
|
* Testing requirements
|
||||||
|
|
||||||
|
**This document should be updated** after exploration findings are documented.
|
||||||
|
|
||||||
|
**Reference**: See [Platform Capability Reference](./platform-capability-reference.md) for OS-level facts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Requirements
|
||||||
|
|
||||||
|
### 1.1 Plugin Behavior Guarantees
|
||||||
|
|
||||||
|
The plugin **must** guarantee the following behaviors:
|
||||||
|
|
||||||
|
| Behavior | Android | iOS | Implementation Required |
|
||||||
|
| -------- | ------- | --- | ----------------------- |
|
||||||
|
| Notification fires after swipe/termination | ✅ Yes | ✅ Yes | OS-guaranteed (verify) |
|
||||||
|
| Notification fires after reboot | ⚠️ Only if rescheduled | ✅ Yes | Android: Boot receiver required |
|
||||||
|
| Missed alarm detection | ✅ Required | ✅ Required | Both: App launch recovery |
|
||||||
|
| Force stop recovery | ✅ Required | N/A | Android: App restart recovery |
|
||||||
|
| Exact timing | ✅ With permission | ⚠️ ±180s tolerance | Android: Permission check |
|
||||||
|
|
||||||
|
### 1.2 Plugin Behavior Limitations
|
||||||
|
|
||||||
|
The plugin **cannot** guarantee:
|
||||||
|
|
||||||
|
| Limitation | Platform | Reason |
|
||||||
|
| ---------- | -------- | ------ |
|
||||||
|
| Notification after Force Stop (Android) | Android | OS hard kill |
|
||||||
|
| App code execution on iOS notification fire | iOS | OS limitation |
|
||||||
|
| Background execution timing (iOS) | iOS | System-controlled |
|
||||||
|
| Exact timing (iOS) | iOS | ±180s tolerance |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Persistence Requirements
|
||||||
|
|
||||||
|
### 2.1 Required Persistence Items
|
||||||
|
|
||||||
|
The plugin **must** persist the following for each scheduled alarm/notification:
|
||||||
|
|
||||||
|
| Field | Type | Required | Purpose |
|
||||||
|
| ----- | ---- | -------- | ------- |
|
||||||
|
| `alarm_id` | String | ✅ Yes | Unique identifier |
|
||||||
|
| `trigger_time` | Long/TimeInterval | ✅ Yes | When to fire |
|
||||||
|
| `repeat_rule` | String/Enum | ✅ Yes | NONE, DAILY, WEEKLY, CUSTOM |
|
||||||
|
| `channel_id` | String | ✅ Yes | Notification channel (Android) |
|
||||||
|
| `priority` | String/Int | ✅ Yes | Notification priority |
|
||||||
|
| `title` | String | ✅ Yes | Notification title |
|
||||||
|
| `body` | String | ✅ Yes | Notification body |
|
||||||
|
| `sound_enabled` | Boolean | ✅ Yes | Sound preference |
|
||||||
|
| `vibration_enabled` | Boolean | ✅ Yes | Vibration preference |
|
||||||
|
| `payload` | String/JSON | ⚠️ Optional | Additional content |
|
||||||
|
| `created_at` | Long/TimeInterval | ✅ Yes | Creation timestamp |
|
||||||
|
| `updated_at` | Long/TimeInterval | ✅ Yes | Last update timestamp |
|
||||||
|
| `enabled` | Boolean | ✅ Yes | Whether alarm is active |
|
||||||
|
|
||||||
|
### 2.2 Storage Implementation
|
||||||
|
|
||||||
|
**Android**:
|
||||||
|
* **Primary**: Room database (`DailyNotificationDatabase`)
|
||||||
|
* **Location**: `android/src/main/java/com/timesafari/dailynotification/`
|
||||||
|
* **Entities**: `Schedule`, `NotificationContentEntity`, `ContentCache`
|
||||||
|
|
||||||
|
**iOS**:
|
||||||
|
* **Primary**: UNUserNotificationCenter (OS-managed)
|
||||||
|
* **Secondary**: Plugin storage (UserDefaults, CoreData, or files)
|
||||||
|
* **Location**: `ios/Plugin/`
|
||||||
|
* **Component**: `DailyNotificationStorage?`
|
||||||
|
|
||||||
|
### 2.3 Persistence Validation
|
||||||
|
|
||||||
|
The plugin **must**:
|
||||||
|
* Validate persistence on every alarm schedule
|
||||||
|
* Log persistence failures
|
||||||
|
* Handle persistence errors gracefully
|
||||||
|
* Provide recovery mechanism if persistence fails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Recovery Requirements
|
||||||
|
|
||||||
|
### 3.1 Required Recovery Points
|
||||||
|
|
||||||
|
The plugin **must** implement recovery at the following points:
|
||||||
|
|
||||||
|
#### 3.1.1 Boot Event (Android Only)
|
||||||
|
|
||||||
|
**Trigger**: `BOOT_COMPLETED` broadcast
|
||||||
|
|
||||||
|
**Required Actions**:
|
||||||
|
1. Load all enabled alarms from persistent storage
|
||||||
|
2. Reschedule each alarm using AlarmManager
|
||||||
|
3. Detect missed alarms (trigger_time < now)
|
||||||
|
4. Generate missed alarm events/notifications
|
||||||
|
5. Log recovery actions
|
||||||
|
|
||||||
|
**Code Reference**: `BootReceiver.kt` line 24
|
||||||
|
|
||||||
|
**Implementation Status**: ☐ Implemented / ☐ Missing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.2 App Cold Start
|
||||||
|
|
||||||
|
**Trigger**: App launched from terminated state
|
||||||
|
|
||||||
|
**Required Actions**:
|
||||||
|
1. Load all enabled alarms from persistent storage
|
||||||
|
2. Verify active alarms match stored alarms
|
||||||
|
3. Detect missed alarms (trigger_time < now)
|
||||||
|
4. Reschedule future alarms
|
||||||
|
5. Generate missed alarm events/notifications
|
||||||
|
6. Log recovery actions
|
||||||
|
|
||||||
|
**Implementation Status**: ☐ Implemented / ☐ Missing
|
||||||
|
|
||||||
|
**Code Location**: Check plugin initialization (`DailyNotificationPlugin.load()` or equivalent)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.3 App Warm Start
|
||||||
|
|
||||||
|
**Trigger**: App returning from background
|
||||||
|
|
||||||
|
**Required Actions**:
|
||||||
|
1. Verify active alarms are still scheduled
|
||||||
|
2. Detect missed alarms (trigger_time < now)
|
||||||
|
3. Reschedule if needed
|
||||||
|
4. Log recovery actions
|
||||||
|
|
||||||
|
**Implementation Status**: ☐ Implemented / ☐ Missing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.1.4 User Taps Notification
|
||||||
|
|
||||||
|
**Trigger**: User interaction with notification
|
||||||
|
|
||||||
|
**Required Actions**:
|
||||||
|
1. Launch app (OS handles)
|
||||||
|
2. Detect if notification was missed
|
||||||
|
3. Handle notification action
|
||||||
|
4. Update alarm state if needed
|
||||||
|
|
||||||
|
**Implementation Status**: ☐ Implemented / ☐ Missing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 Missed Alarm Handling
|
||||||
|
|
||||||
|
The plugin **must** detect and handle missed alarms:
|
||||||
|
|
||||||
|
**Definition**: An alarm is "missed" if:
|
||||||
|
* `trigger_time < now`
|
||||||
|
* Alarm was not fired (or firing status unknown)
|
||||||
|
* Alarm is still enabled
|
||||||
|
|
||||||
|
**Required Actions**:
|
||||||
|
1. **Detect** missed alarms during recovery
|
||||||
|
2. **Generate** missed alarm event/notification
|
||||||
|
3. **Reschedule** future occurrences (if repeating)
|
||||||
|
4. **Log** missed alarm for debugging
|
||||||
|
5. **Update** alarm state (mark as missed or reschedule)
|
||||||
|
|
||||||
|
**Implementation Requirements**:
|
||||||
|
* Must run on app launch (cold/warm start)
|
||||||
|
* Must run on boot (Android)
|
||||||
|
* Must not duplicate missed alarm notifications
|
||||||
|
* Must handle timezone changes
|
||||||
|
|
||||||
|
**Code Location**: To be implemented in recovery logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. JS/TS API Contract
|
||||||
|
|
||||||
|
### 4.1 API Guarantees
|
||||||
|
|
||||||
|
The plugin **must** document and guarantee the following behaviors to JavaScript/TypeScript developers:
|
||||||
|
|
||||||
|
#### 4.1.1 `scheduleDailyNotification(options)`
|
||||||
|
|
||||||
|
**Guarantees**:
|
||||||
|
* ✅ Notification will fire if app is swiped from recents
|
||||||
|
* ✅ Notification will fire if app is terminated by OS
|
||||||
|
* ⚠️ Notification will fire after reboot **only if**:
|
||||||
|
* Android: Boot receiver is registered and working
|
||||||
|
* iOS: Automatic (OS handles)
|
||||||
|
* ❌ Notification will **NOT** fire after Android Force Stop until app is opened
|
||||||
|
* ⚠️ iOS notifications have ±180s timing tolerance
|
||||||
|
|
||||||
|
**Caveats**:
|
||||||
|
* Android requires `SCHEDULE_EXACT_ALARM` permission on Android 12+
|
||||||
|
* Android requires `RECEIVE_BOOT_COMPLETED` permission for reboot recovery
|
||||||
|
* iOS requires notification authorization
|
||||||
|
|
||||||
|
**Error Codes**:
|
||||||
|
* `EXACT_ALARM_PERMISSION_REQUIRED` - Android 12+ exact alarm permission needed
|
||||||
|
* `NOTIFICATIONS_DENIED` - Notification permission denied
|
||||||
|
* `SCHEDULE_FAILED` - Scheduling failed (check logs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4.1.2 `scheduleDailyReminder(options)`
|
||||||
|
|
||||||
|
**Guarantees**:
|
||||||
|
* Same as `scheduleDailyNotification()` above
|
||||||
|
* Static reminder (no content dependency)
|
||||||
|
* Fires even if content fetch fails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4.1.3 `getNotificationStatus()`
|
||||||
|
|
||||||
|
**Guarantees**:
|
||||||
|
* Returns current notification status
|
||||||
|
* Includes pending notifications
|
||||||
|
* Includes last notification time
|
||||||
|
* May include missed alarm information
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 API Warnings
|
||||||
|
|
||||||
|
The plugin **must** document the following warnings:
|
||||||
|
|
||||||
|
**Android**:
|
||||||
|
* "Notifications will not fire after device reboot unless the app is opened at least once"
|
||||||
|
* "Force Stop will prevent all notifications until the app is manually opened"
|
||||||
|
* "Exact alarm permission is required on Android 12+ for precise timing"
|
||||||
|
|
||||||
|
**iOS**:
|
||||||
|
* "Notifications have ±180 seconds timing tolerance"
|
||||||
|
* "App code does not run when notifications fire (unless user interacts)"
|
||||||
|
* "Background execution is system-controlled and not guaranteed"
|
||||||
|
|
||||||
|
**Cross-Platform**:
|
||||||
|
* "Missed alarms are detected on app launch, not at trigger time"
|
||||||
|
* "Repeating alarms must be rescheduled for each occurrence"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 API Error Handling
|
||||||
|
|
||||||
|
The plugin **must**:
|
||||||
|
* Return clear error messages
|
||||||
|
* Include error codes for programmatic handling
|
||||||
|
* Open system settings when permission is needed
|
||||||
|
* Provide actionable guidance in error messages
|
||||||
|
|
||||||
|
**Example Error Response**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
code: "EXACT_ALARM_PERMISSION_REQUIRED",
|
||||||
|
message: "Exact alarm permission required. Please grant 'Alarms & reminders' permission in Settings, then try again.",
|
||||||
|
action: "opened_settings" // or "permission_denied"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Platform-Specific Requirements
|
||||||
|
|
||||||
|
### 5.1 Android Requirements
|
||||||
|
|
||||||
|
#### 5.1.1 Permissions
|
||||||
|
|
||||||
|
**Required Permissions**:
|
||||||
|
* `RECEIVE_BOOT_COMPLETED` - Boot receiver
|
||||||
|
* `SCHEDULE_EXACT_ALARM` - Android 12+ (API 31+) for exact alarms
|
||||||
|
* `POST_NOTIFICATIONS` - Android 13+ (API 33+) for notifications
|
||||||
|
|
||||||
|
**Permission Handling**:
|
||||||
|
* Check permission before scheduling
|
||||||
|
* Request permission if not granted
|
||||||
|
* Open system settings if permission denied
|
||||||
|
* Provide clear error messages
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationPlugin.kt` line 1309
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5.1.2 Manifest Entries
|
||||||
|
|
||||||
|
**Required Manifest Entries**:
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="com.timesafari.dailynotification.BootReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.timesafari.daily.NOTIFICATION" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Location**: Test app manifest: `test-apps/android-test-app/app/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5.1.3 Notification Channels
|
||||||
|
|
||||||
|
**Required Channels**:
|
||||||
|
* `timesafari.daily` - Primary notification channel
|
||||||
|
* `daily_reminders` - Reminder notifications (if used)
|
||||||
|
|
||||||
|
**Channel Configuration**:
|
||||||
|
* Importance: HIGH (for alarms), DEFAULT (for reminders)
|
||||||
|
* Sound: Enabled by default
|
||||||
|
* Vibration: Enabled by default
|
||||||
|
* Show badge: Enabled
|
||||||
|
|
||||||
|
**Code Reference**: `ChannelManager.java` or `NotifyReceiver.kt` line 454
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5.1.4 Alarm Scheduling
|
||||||
|
|
||||||
|
**Required API Usage**:
|
||||||
|
* `setAlarmClock()` for Android 5.0+ (preferred)
|
||||||
|
* `setExactAndAllowWhileIdle()` for Android 6.0+ (fallback)
|
||||||
|
* `setExact()` for older versions (fallback)
|
||||||
|
|
||||||
|
**Code Reference**: `NotifyReceiver.kt` line 219, 223, 231
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 iOS Requirements
|
||||||
|
|
||||||
|
#### 5.2.1 Permissions
|
||||||
|
|
||||||
|
**Required Permissions**:
|
||||||
|
* Notification authorization (requested at runtime)
|
||||||
|
|
||||||
|
**Permission Handling**:
|
||||||
|
* Request permission before scheduling
|
||||||
|
* Handle authorization status
|
||||||
|
* Provide clear error messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5.2.2 Background Tasks
|
||||||
|
|
||||||
|
**Required Background Task Identifiers**:
|
||||||
|
* `com.timesafari.dailynotification.fetch` - Background fetch
|
||||||
|
* `com.timesafari.dailynotification.notify` - Notification task (if used)
|
||||||
|
|
||||||
|
**Background Task Registration**:
|
||||||
|
* Register in `Info.plist`:
|
||||||
|
```xml
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>com.timesafari.dailynotification.fetch</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationPlugin.swift` line 31-32
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5.2.3 Notification Scheduling
|
||||||
|
|
||||||
|
**Required API Usage**:
|
||||||
|
* `UNUserNotificationCenter.add()` - Schedule notifications
|
||||||
|
* `UNCalendarNotificationTrigger` - Calendar-based triggers (preferred)
|
||||||
|
* `UNTimeIntervalNotificationTrigger` - Time interval triggers
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationScheduler.swift` line 185
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5.2.4 Notification Categories
|
||||||
|
|
||||||
|
**Required Categories**:
|
||||||
|
* `DAILY_NOTIFICATION` - Primary notification category
|
||||||
|
|
||||||
|
**Category Configuration**:
|
||||||
|
* Actions: Configure as needed
|
||||||
|
* Options: Custom sound, custom actions
|
||||||
|
|
||||||
|
**Code Reference**: `DailyNotificationScheduler.swift` line 62+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Testing Requirements
|
||||||
|
|
||||||
|
### 6.1 Required Test Scenarios
|
||||||
|
|
||||||
|
The plugin **must** be tested for:
|
||||||
|
|
||||||
|
**Android**:
|
||||||
|
* [ ] Base case (alarm fires on time)
|
||||||
|
* [ ] Swipe from recents
|
||||||
|
* [ ] OS kill (memory pressure)
|
||||||
|
* [ ] Device reboot (with and without app launch)
|
||||||
|
* [ ] Force stop (with app restart)
|
||||||
|
* [ ] Exact alarm permission (Android 12+)
|
||||||
|
* [ ] Boot receiver functionality
|
||||||
|
* [ ] Missed alarm detection
|
||||||
|
|
||||||
|
**iOS**:
|
||||||
|
* [ ] Base case (notification fires on time)
|
||||||
|
* [ ] Swipe app away
|
||||||
|
* [ ] Device reboot (without app launch)
|
||||||
|
* [ ] Hard termination and relaunch
|
||||||
|
* [ ] Background execution limits
|
||||||
|
* [ ] Missed notification detection
|
||||||
|
|
||||||
|
**Cross-Platform**:
|
||||||
|
* [ ] Timezone changes
|
||||||
|
* [ ] Clock adjustments
|
||||||
|
* [ ] Multiple simultaneous alarms
|
||||||
|
* [ ] Repeating alarms
|
||||||
|
* [ ] Alarm cancellation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6.2 Test Harness Requirements
|
||||||
|
|
||||||
|
**Required Test Tools**:
|
||||||
|
* Real devices (not just emulators)
|
||||||
|
* ADB commands for Android testing
|
||||||
|
* Xcode for iOS testing
|
||||||
|
* Log monitoring tools
|
||||||
|
|
||||||
|
**Required Test Documentation**:
|
||||||
|
* Test results matrix
|
||||||
|
* Log snippets for failures
|
||||||
|
* Screenshots/videos for UI issues
|
||||||
|
* Performance metrics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Versioning Requirements
|
||||||
|
|
||||||
|
### 7.1 Breaking Changes
|
||||||
|
|
||||||
|
Any change to alarm behavior is **breaking** and requires:
|
||||||
|
|
||||||
|
* **MAJOR version bump** (semantic versioning)
|
||||||
|
* **Migration guide** for existing users
|
||||||
|
* **Deprecation warnings** (if applicable)
|
||||||
|
* **Clear changelog entry**
|
||||||
|
|
||||||
|
### 7.2 Non-Breaking Changes
|
||||||
|
|
||||||
|
Non-breaking changes include:
|
||||||
|
* Bug fixes
|
||||||
|
* Performance improvements
|
||||||
|
* Additional features (backward compatible)
|
||||||
|
* Documentation updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Implementation Checklist
|
||||||
|
|
||||||
|
### 8.1 Android Implementation
|
||||||
|
|
||||||
|
- [ ] Boot receiver registered in manifest
|
||||||
|
- [ ] Boot receiver reschedules alarms from database
|
||||||
|
- [ ] Exact alarm permission checked and requested
|
||||||
|
- [ ] Notification channels created
|
||||||
|
- [ ] Alarm scheduling uses correct API (`setAlarmClock` preferred)
|
||||||
|
- [ ] Persistence implemented (Room database)
|
||||||
|
- [ ] Missed alarm detection on app launch
|
||||||
|
- [ ] Force stop recovery on app restart
|
||||||
|
- [ ] Error handling and user guidance
|
||||||
|
|
||||||
|
### 8.2 iOS Implementation
|
||||||
|
|
||||||
|
- [ ] Notification authorization requested
|
||||||
|
- [ ] Background tasks registered in Info.plist
|
||||||
|
- [ ] Notification scheduling uses UNUserNotificationCenter
|
||||||
|
- [ ] Calendar triggers used (not just time interval)
|
||||||
|
- [ ] Plugin-side persistence (if needed for missed detection)
|
||||||
|
- [ ] Missed notification detection on app launch
|
||||||
|
- [ ] Background task limitations documented
|
||||||
|
- [ ] Error handling and user guidance
|
||||||
|
|
||||||
|
### 8.3 Cross-Platform Implementation
|
||||||
|
|
||||||
|
- [ ] JS/TS API contract documented
|
||||||
|
- [ ] Platform-specific caveats documented
|
||||||
|
- [ ] Error codes standardized
|
||||||
|
- [ ] Test scenarios covered
|
||||||
|
- [ ] Migration guide (if breaking changes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Open Questions / TODOs
|
||||||
|
|
||||||
|
**To be filled after exploration**:
|
||||||
|
|
||||||
|
| Question | Platform | Priority | Status |
|
||||||
|
| -------- | -------- | -------- | ------ |
|
||||||
|
| Does boot receiver work correctly? | Android | High | ☐ |
|
||||||
|
| Is missed alarm detection implemented? | Both | High | ☐ |
|
||||||
|
| Are all required fields persisted? | Both | Medium | ☐ |
|
||||||
|
| Is force stop recovery implemented? | Android | High | ☐ |
|
||||||
|
| Does iOS plugin persist state separately? | iOS | Medium | ☐ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Platform Capability Reference](./platform-capability-reference.md) - OS-level facts
|
||||||
|
- [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md) - Exploration template
|
||||||
|
- [Improve Alarm Directives](./improve-alarm-directives.md) - Improvement directive
|
||||||
|
- [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md) - Testing procedures
|
||||||
|
- [App Startup Recovery Solution](./app-startup-recovery-solution.md) - Recovery mechanisms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v1.0** (November 2025): Initial requirements document
|
||||||
|
- Persistence requirements
|
||||||
|
- Recovery requirements
|
||||||
|
- JS/TS API contract
|
||||||
|
- Platform-specific requirements
|
||||||
|
- Testing requirements
|
||||||
|
|
||||||
BIN
lib/bin/main/org/example/Library.class
Normal file
BIN
lib/bin/main/org/example/Library.class
Normal file
Binary file not shown.
BIN
lib/bin/test/org/example/LibraryTest.class
Normal file
BIN
lib/bin/test/org/example/LibraryTest.class
Normal file
Binary file not shown.
@@ -328,6 +328,15 @@ export interface Schedule {
|
|||||||
stateJson?: string;
|
stateJson?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule with AlarmManager status
|
||||||
|
* Extends Schedule with isActuallyScheduled flag indicating if alarm is registered in AlarmManager
|
||||||
|
*/
|
||||||
|
export interface ScheduleWithStatus extends Schedule {
|
||||||
|
/** Whether the alarm is actually scheduled in AlarmManager (Android only, for 'notify' schedules) */
|
||||||
|
isActuallyScheduled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input type for creating a new schedule
|
* Input type for creating a new schedule
|
||||||
*/
|
*/
|
||||||
@@ -697,6 +706,29 @@ export interface DailyNotificationPlugin {
|
|||||||
*/
|
*/
|
||||||
getSchedules(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: Schedule[] }>;
|
getSchedules(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: Schedule[] }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all schedules with their AlarmManager status
|
||||||
|
* Returns schedules from database with isActuallyScheduled flag for each
|
||||||
|
*
|
||||||
|
* @param options Optional filters:
|
||||||
|
* - kind: Filter by schedule type ('fetch' | 'notify')
|
||||||
|
* - enabled: Filter by enabled status (true = only enabled, false = only disabled, undefined = all)
|
||||||
|
* @returns Promise resolving to object with schedules array: { schedules: ScheduleWithStatus[] }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // Get all notification schedules with AlarmManager status
|
||||||
|
* const result = await DailyNotification.getSchedulesWithStatus({
|
||||||
|
* kind: 'notify',
|
||||||
|
* enabled: true
|
||||||
|
* });
|
||||||
|
* result.schedules.forEach(schedule => {
|
||||||
|
* console.log(`${schedule.id}: ${schedule.isActuallyScheduled ? 'Scheduled' : 'Not scheduled'}`);
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
getSchedulesWithStatus(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: ScheduleWithStatus[] }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single schedule by ID
|
* Get a single schedule by ID
|
||||||
*
|
*
|
||||||
|
|||||||
613
test-apps/android-test-app/alarm-test-lib.sh
Normal file
613
test-apps/android-test-app/alarm-test-lib.sh
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Shared Alarm Test Library
|
||||||
|
# ========================================
|
||||||
|
#
|
||||||
|
# Common helpers for Phase 1, 2, and 3 test scripts
|
||||||
|
#
|
||||||
|
# Usage: source this file in your test script:
|
||||||
|
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# source "${SCRIPT_DIR}/alarm-test-lib.sh"
|
||||||
|
#
|
||||||
|
# Configuration can be overridden before sourcing:
|
||||||
|
# APP_ID="custom.package" source "${SCRIPT_DIR}/alarm-test-lib.sh"
|
||||||
|
|
||||||
|
# --- Config Defaults (can be overridden before sourcing) ---
|
||||||
|
|
||||||
|
: "${APP_ID:=com.timesafari.dailynotification}"
|
||||||
|
: "${APK_PATH:=./app/build/outputs/apk/debug/app-debug.apk}"
|
||||||
|
: "${ADB_BIN:=adb}"
|
||||||
|
|
||||||
|
# Reactivation log tag (common across phases)
|
||||||
|
: "${REACTIVATION_TAG:=DNP-REACTIVATION}"
|
||||||
|
: "${SCENARIO_KEY:=Detected scenario: }"
|
||||||
|
|
||||||
|
# Screenshot configuration
|
||||||
|
: "${SCREENSHOT_ROOT:=screenshots}"
|
||||||
|
: "${ENABLE_SCREENSHOTS:=1}"
|
||||||
|
|
||||||
|
# Derived config (for backward compatibility with Phase 1)
|
||||||
|
PACKAGE="${APP_ID}"
|
||||||
|
ACTIVITY="${APP_ID}/.MainActivity"
|
||||||
|
|
||||||
|
# Colors for output (used by Phase 1 print_* functions)
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# --- UI/Log Helpers ---
|
||||||
|
|
||||||
|
section() {
|
||||||
|
echo
|
||||||
|
echo "========================================"
|
||||||
|
echo "$1"
|
||||||
|
echo "========================================"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
substep() {
|
||||||
|
echo "→ $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "ℹ️ $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
ok() {
|
||||||
|
echo -e "✅ $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "⚠️ $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "❌ $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
echo
|
||||||
|
read -rp "Press Enter when ready to continue..."
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_prompt() {
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "👆 UI ACTION REQUIRED"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo -e "$1"
|
||||||
|
echo
|
||||||
|
read -rp "Press Enter after completing the action above..."
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Phase 1 compatibility aliases (print_* functions)
|
||||||
|
print_header() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo -e "${BLUE}$1${NC}"
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
print_step() {
|
||||||
|
echo -e "${GREEN}→ Step $1:${NC} $2"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_wait() {
|
||||||
|
echo -e "${YELLOW}⏳ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warn() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_user() {
|
||||||
|
echo ""
|
||||||
|
read -p "Press Enter when ready to continue..."
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_ui_action() {
|
||||||
|
ui_prompt "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- ADB/Build Helpers ---
|
||||||
|
|
||||||
|
require_adb_device() {
|
||||||
|
section "Pre-Flight Checks"
|
||||||
|
|
||||||
|
if ! $ADB_BIN devices | awk 'NR>1 && $2=="device"{found=1} END{exit !found}'; then
|
||||||
|
error "No emulator/device in 'device' state. Start your emulator first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "ADB device connected"
|
||||||
|
|
||||||
|
info "Checking emulator status..."
|
||||||
|
if ! $ADB_BIN shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
|
||||||
|
info "Waiting for emulator to boot..."
|
||||||
|
$ADB_BIN wait-for-device
|
||||||
|
while [ "$($ADB_BIN shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
ok "Emulator is ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Phase 1 compatibility: check_adb_connection + check_emulator_ready
|
||||||
|
check_adb_connection() {
|
||||||
|
if ! $ADB_BIN devices | grep -q "device$"; then
|
||||||
|
print_error "No Android device/emulator connected"
|
||||||
|
echo "Please connect a device or start an emulator, then run:"
|
||||||
|
echo " adb devices"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_success "ADB device connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_emulator_ready() {
|
||||||
|
print_info "Checking emulator status..."
|
||||||
|
if ! $ADB_BIN shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
|
||||||
|
print_wait "Waiting for emulator to boot..."
|
||||||
|
$ADB_BIN wait-for-device
|
||||||
|
while [ "$($ADB_BIN shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
print_success "Emulator is ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_app() {
|
||||||
|
section "Building Test App"
|
||||||
|
|
||||||
|
substep "Step 1: Building debug APK..."
|
||||||
|
if ./gradlew :app:assembleDebug; then
|
||||||
|
ok "Build successful"
|
||||||
|
else
|
||||||
|
error "Build failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$APK_PATH" ]]; then
|
||||||
|
ok "APK ready: $APK_PATH"
|
||||||
|
else
|
||||||
|
error "APK not found at $APK_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_app() {
|
||||||
|
section "Installing App"
|
||||||
|
|
||||||
|
substep "Step 1: Uninstalling existing app (if present)..."
|
||||||
|
set +e
|
||||||
|
uninstall_output="$($ADB_BIN uninstall "$APP_ID" 2>&1)"
|
||||||
|
uninstall_status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ $uninstall_status -ne 0 ]]; then
|
||||||
|
if grep -q "DELETE_FAILED_INTERNAL_ERROR" <<<"$uninstall_output"; then
|
||||||
|
info "No existing app to uninstall (continuing)"
|
||||||
|
else
|
||||||
|
warn "Uninstall returned non-zero status: $uninstall_output (continuing anyway)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "Previous app uninstall succeeded"
|
||||||
|
fi
|
||||||
|
|
||||||
|
substep "Step 2: Installing new APK..."
|
||||||
|
if $ADB_BIN install -r "$APK_PATH"; then
|
||||||
|
ok "App installed successfully"
|
||||||
|
else
|
||||||
|
error "App installation failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
substep "Step 3: Verifying installation..."
|
||||||
|
if $ADB_BIN shell pm list packages | grep -q "$APP_ID"; then
|
||||||
|
ok "App verified in package list"
|
||||||
|
else
|
||||||
|
error "App not found in package list"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Clearing logcat buffer..."
|
||||||
|
$ADB_BIN logcat -c
|
||||||
|
ok "Logcat cleared"
|
||||||
|
}
|
||||||
|
|
||||||
|
launch_app() {
|
||||||
|
# Check if we're in Phase 1 context (has print_info function)
|
||||||
|
if type print_info >/dev/null 2>&1; then
|
||||||
|
print_info "Launching app..."
|
||||||
|
$ADB_BIN shell am start -n "${ACTIVITY}"
|
||||||
|
sleep 3 # Give app time to load and check status
|
||||||
|
print_success "App launched"
|
||||||
|
else
|
||||||
|
substep "Launching app main activity..."
|
||||||
|
$ADB_BIN shell am start -n "${ACTIVITY}" >/dev/null 2>&1
|
||||||
|
sleep 3 # Give app time to load
|
||||||
|
ok "App launched"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_logs() {
|
||||||
|
info "Clearing logcat buffer..."
|
||||||
|
$ADB_BIN logcat -c
|
||||||
|
ok "Logs cleared"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_alarms() {
|
||||||
|
info "Checking AlarmManager status..."
|
||||||
|
echo
|
||||||
|
$ADB_BIN shell dumpsys alarm | grep -A3 "$APP_ID" || true
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plugin-specific alarm action (must match AndroidManifest.xml)
|
||||||
|
PLUGIN_ALARM_ACTION="com.timesafari.daily.NOTIFICATION"
|
||||||
|
|
||||||
|
get_plugin_alarm_count() {
|
||||||
|
# Returns count of ONLY the plugin's NOTIFICATION alarms (not prefetch - that uses WorkManager)
|
||||||
|
# Expected: 1 notification alarm per daily schedule
|
||||||
|
#
|
||||||
|
# This function counts ALARM_CLOCK wake alarms (RTC_WAKEUP) tagged as:
|
||||||
|
# tag=*walarm*:com.timesafari.daily.NOTIFICATION
|
||||||
|
#
|
||||||
|
# Uses deduplicating parser to avoid double-counting the same alarm that appears in both:
|
||||||
|
# - Main alarm list
|
||||||
|
# - "Next wake from idle" section (ignored - only counts RTC_WAKEUP blocks)
|
||||||
|
# - Alarm Stats section (ignored - only counts actual alarm blocks)
|
||||||
|
#
|
||||||
|
# Tracks unique Alarm handles to ensure each alarm is counted only once.
|
||||||
|
# Checks for app package AND action string anywhere in the block (they appear on different lines).
|
||||||
|
local count app_id action
|
||||||
|
app_id="$APP_ID"
|
||||||
|
action="com.timesafari.daily.NOTIFICATION"
|
||||||
|
count="$($ADB_BIN shell dumpsys alarm 2>/dev/null | awk -v app="$app_id" -v action="$action" '
|
||||||
|
BEGIN {
|
||||||
|
in_block = 0
|
||||||
|
alarmId = ""
|
||||||
|
hasAppLine = 0
|
||||||
|
hasActionLine = 0
|
||||||
|
}
|
||||||
|
# Start of a new RTC_WAKEUP alarm block
|
||||||
|
/^[[:space:]]*RTC_WAKEUP/ {
|
||||||
|
# Flush previous block if it was a plugin notification
|
||||||
|
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
|
||||||
|
seen[alarmId] = 1
|
||||||
|
}
|
||||||
|
in_block = 1
|
||||||
|
alarmId = ""
|
||||||
|
hasAppLine = 0
|
||||||
|
hasActionLine = 0
|
||||||
|
# Extract alarmId from "Alarm{11245c ..."
|
||||||
|
if (match($0, /Alarm\{[0-9a-f]+/)) {
|
||||||
|
# match is like "Alarm{11245c", extract just the hex part
|
||||||
|
alarmId = substr($0, RSTART + 6, RLENGTH - 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Blank line = end of block
|
||||||
|
/^[[:space:]]*$/ {
|
||||||
|
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
|
||||||
|
seen[alarmId] = 1
|
||||||
|
}
|
||||||
|
in_block = 0
|
||||||
|
alarmId = ""
|
||||||
|
hasAppLine = 0
|
||||||
|
hasActionLine = 0
|
||||||
|
}
|
||||||
|
# Lines inside an alarm block
|
||||||
|
in_block == 1 {
|
||||||
|
if ($0 ~ app) {
|
||||||
|
hasAppLine = 1
|
||||||
|
}
|
||||||
|
if ($0 ~ action) {
|
||||||
|
hasActionLine = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
# Flush final block if it was a plugin notification
|
||||||
|
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
|
||||||
|
seen[alarmId] = 1
|
||||||
|
}
|
||||||
|
count = 0
|
||||||
|
for (id in seen) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
print count + 0
|
||||||
|
}
|
||||||
|
' 2>/dev/null || echo "0")"
|
||||||
|
echo "$count" | tr -d '\n\r' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alias for backward compatibility
|
||||||
|
get_notification_alarm_count() {
|
||||||
|
get_plugin_alarm_count
|
||||||
|
}
|
||||||
|
|
||||||
|
get_prefetch_work_count() {
|
||||||
|
# Returns count of prefetch WorkManager jobs (not AlarmManager alarms)
|
||||||
|
# Note: Prefetch uses WorkManager, not AlarmManager, so it won't appear in dumpsys alarm
|
||||||
|
# This is a placeholder - actual WorkManager job counting would require different approach
|
||||||
|
local count
|
||||||
|
count="$($ADB_BIN shell dumpsys jobscheduler | grep -c "prefetch" 2>/dev/null || echo "0")"
|
||||||
|
echo "$count" | tr -d '\n\r' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
get_system_alarm_count() {
|
||||||
|
# Returns total RTC_WAKEUP alarms on system (for debugging/context)
|
||||||
|
local count
|
||||||
|
count="$($ADB_BIN shell dumpsys alarm 2>/dev/null | grep -c "RTC_WAKEUP" || echo "0")"
|
||||||
|
echo "$count" | tr -d '\n\r' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
count_alarms() {
|
||||||
|
# Legacy function: now returns plugin-specific alarm count
|
||||||
|
# Use get_plugin_alarm_count() for clarity, or get_system_alarm_count() for total
|
||||||
|
get_plugin_alarm_count
|
||||||
|
}
|
||||||
|
|
||||||
|
show_plugin_alarms_compact() {
|
||||||
|
# Prints only the plugin's alarm block(s) for debugging
|
||||||
|
# Shows complete RTC_WAKEUP alarm blocks that contain the app ID
|
||||||
|
# This makes it visually obvious why the AWK matcher should pick up alarms
|
||||||
|
# (app ID on header line, action on tag line within the same block)
|
||||||
|
$ADB_BIN shell dumpsys alarm 2>/dev/null \
|
||||||
|
| awk -v app="$APP_ID" '
|
||||||
|
BEGIN {
|
||||||
|
in_block = 0
|
||||||
|
block = ""
|
||||||
|
found_app = 0
|
||||||
|
}
|
||||||
|
/^[[:space:]]*RTC_WAKEUP/ {
|
||||||
|
# Print previous block if it contained app ID
|
||||||
|
if (in_block && found_app) {
|
||||||
|
print block ORS
|
||||||
|
}
|
||||||
|
# Start new block
|
||||||
|
in_block = 1
|
||||||
|
block = $0 ORS
|
||||||
|
found_app = ($0 ~ app) ? 1 : 0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^[[:space:]]*$/ {
|
||||||
|
# End of block
|
||||||
|
if (in_block && found_app) {
|
||||||
|
print block ORS
|
||||||
|
}
|
||||||
|
in_block = 0
|
||||||
|
block = ""
|
||||||
|
found_app = 0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{
|
||||||
|
if (in_block) {
|
||||||
|
block = block $0 ORS
|
||||||
|
if ($0 ~ app) {
|
||||||
|
found_app = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
if (in_block && found_app) {
|
||||||
|
print block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' \
|
||||||
|
| sed -n '1,80p' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_stable_plugin_alarm_count() {
|
||||||
|
# Polls for plugin alarm count to stabilize (reduces race condition false negatives)
|
||||||
|
# Usage: wait_for_stable_plugin_alarm_count [attempts] [delay_seconds]
|
||||||
|
# Default: 5 attempts, 2 second delay (total ~10 seconds)
|
||||||
|
# Returns: alarm count (0 if none found after all attempts)
|
||||||
|
local attempts=${1:-5}
|
||||||
|
local delay=${2:-2}
|
||||||
|
local count=0
|
||||||
|
local i
|
||||||
|
|
||||||
|
for i in $(seq 1 "$attempts"); do
|
||||||
|
count="$(get_plugin_alarm_count)"
|
||||||
|
if [ "$count" -ge 1 ] 2>/dev/null; then
|
||||||
|
echo "$count"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$i" -lt "$attempts" ]; then
|
||||||
|
sleep "$delay"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "$count"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Screenshot Helpers ---
|
||||||
|
|
||||||
|
take_screenshot() {
|
||||||
|
# Captures a device screenshot and saves it with test name, step, and timestamp
|
||||||
|
# Usage: take_screenshot "test_name" "step_name"
|
||||||
|
# Example: take_screenshot "phase1_test0_daily_rollover" "before_scheduling"
|
||||||
|
local test_name="$1"
|
||||||
|
local step_name="$2"
|
||||||
|
|
||||||
|
# Do nothing if screenshots are disabled
|
||||||
|
if [ "$ENABLE_SCREENSHOTS" != "1" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ADB_BIN" ]; then
|
||||||
|
echo "⚠️ ADB_BIN is not set; cannot take screenshot." >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Timestamp for uniqueness
|
||||||
|
local ts
|
||||||
|
ts="$(date '+%Y%m%d-%H%M%S' 2>/dev/null)" || ts="unknown"
|
||||||
|
|
||||||
|
# Directory: screenshots/<test_name>/
|
||||||
|
# Use absolute path relative to script directory if SCREENSHOT_ROOT is relative
|
||||||
|
local dir
|
||||||
|
if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR" ]; then
|
||||||
|
dir="${SCRIPT_DIR}/${SCREENSHOT_ROOT}/${test_name}"
|
||||||
|
elif [ -n "${BASH_SOURCE[0]}" ]; then
|
||||||
|
# Fallback: derive from this script's location
|
||||||
|
local lib_dir
|
||||||
|
lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || lib_dir=""
|
||||||
|
if [ -n "$lib_dir" ]; then
|
||||||
|
dir="${lib_dir}/${SCREENSHOT_ROOT}/${test_name}"
|
||||||
|
else
|
||||||
|
dir="${SCREENSHOT_ROOT}/${test_name}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
dir="${SCREENSHOT_ROOT}/${test_name}"
|
||||||
|
fi
|
||||||
|
mkdir -p "$dir" 2>/dev/null || {
|
||||||
|
echo "⚠️ Failed to create screenshot directory: $dir" >&2
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# File name: <test>_<step>_<timestamp>.png, with spaces converted to dashes
|
||||||
|
local safe_step="${step_name// /-}"
|
||||||
|
local file="${dir}/${test_name}_${safe_step}_${ts}.png"
|
||||||
|
|
||||||
|
echo "📸 Capturing screenshot: ${file}" >&2
|
||||||
|
# Use exec-out to avoid newline mangling
|
||||||
|
if ! "$ADB_BIN" exec-out screencap -p > "$file" 2>/dev/null; then
|
||||||
|
echo "⚠️ Failed to capture screenshot via adb." >&2
|
||||||
|
# Clean up empty file if created
|
||||||
|
[ -s "$file" ] || rm -f "$file" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify file was created and has content
|
||||||
|
if [ ! -s "$file" ]; then
|
||||||
|
echo "⚠️ Screenshot file is empty or missing: $file" >&2
|
||||||
|
rm -f "$file" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
take_failure_screenshot() {
|
||||||
|
# Convenience wrapper for failure cases
|
||||||
|
# Usage: take_failure_screenshot "test_name" "reason"
|
||||||
|
# Example: take_failure_screenshot "phase1_test0_daily_rollover" "no_alarm_after_rollover"
|
||||||
|
local test_name="$1"
|
||||||
|
local reason="$2"
|
||||||
|
take_screenshot "$test_name" "FAIL_${reason}"
|
||||||
|
}
|
||||||
|
|
||||||
|
force_stop_app() {
|
||||||
|
info "Forcing stop of app process..."
|
||||||
|
$ADB_BIN shell am force-stop "$APP_ID" || true
|
||||||
|
sleep 2
|
||||||
|
ok "Force stop issued"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Phase 1 compatibility alias
|
||||||
|
kill_app() {
|
||||||
|
print_info "Killing app process..."
|
||||||
|
$ADB_BIN shell am kill "$APP_ID"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Verify process is killed
|
||||||
|
if $ADB_BIN shell ps | grep -q "$APP_ID"; then
|
||||||
|
print_wait "Process still running, using force-stop..."
|
||||||
|
$ADB_BIN shell am force-stop "$APP_ID"
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $ADB_BIN shell ps | grep -q "$APP_ID"; then
|
||||||
|
print_success "App process terminated"
|
||||||
|
else
|
||||||
|
print_error "App process still running"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
reboot_emulator() {
|
||||||
|
info "Rebooting emulator..."
|
||||||
|
$ADB_BIN reboot
|
||||||
|
ok "Reboot initiated"
|
||||||
|
|
||||||
|
info "Waiting for emulator to come back online..."
|
||||||
|
$ADB_BIN wait-for-device
|
||||||
|
|
||||||
|
info "Waiting for system to fully boot..."
|
||||||
|
while [ "$($ADB_BIN shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
sleep 5 # Additional buffer for system services
|
||||||
|
ok "Emulator back online"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Log Parsing Helpers ---
|
||||||
|
|
||||||
|
get_recovery_logs() {
|
||||||
|
# Collect only the relevant reactivation logs
|
||||||
|
$ADB_BIN logcat -d | grep "$REACTIVATION_TAG" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_field_from_logs() {
|
||||||
|
# Usage: extract_field_from_logs "$logs" "rescheduled"
|
||||||
|
local logs="$1"
|
||||||
|
local key="$2"
|
||||||
|
echo "$logs" | grep -E "$key=" | sed -E "s/.*$key=([0-9]+).*/\1/" | tail -n1 || echo "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_scenario_from_logs() {
|
||||||
|
local logs="$1"
|
||||||
|
echo "$logs" | grep -E "$SCENARIO_KEY" | sed -E "s/.*$SCENARIO_KEY([A-Z_]+).*/\1/" | tail -n1 || echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Phase 1 compatibility: check_recovery_logs
|
||||||
|
check_recovery_logs() {
|
||||||
|
print_info "Checking recovery logs..."
|
||||||
|
echo ""
|
||||||
|
$ADB_BIN logcat -d | grep -E "$REACTIVATION_TAG" | tail -10
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Phase 1 compatibility: check_alarm_status
|
||||||
|
check_alarm_status() {
|
||||||
|
print_info "Checking AlarmManager status..."
|
||||||
|
echo ""
|
||||||
|
$ADB_BIN shell dumpsys alarm | grep -i "$APP_ID" | head -5
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Test Selection Helper ---
|
||||||
|
|
||||||
|
should_run_test() {
|
||||||
|
# Usage: should_run_test "1" SELECTED_TESTS
|
||||||
|
# Returns 0 (success) if test should run, 1 (failure) if not
|
||||||
|
local id="$1"
|
||||||
|
local -n selected_tests_ref="$2"
|
||||||
|
|
||||||
|
# If no tests are specified, run all tests
|
||||||
|
if [[ "${#selected_tests_ref[@]}" -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for t in "${selected_tests_ref[@]}"; do
|
||||||
|
if [[ "$t" == "$id" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
@@ -17,6 +17,9 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
debuggable true // Enable debugging for test app
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
|||||||
@@ -62,6 +62,11 @@
|
|||||||
<div id="pluginStatusContent" style="margin-top: 8px;">
|
<div id="pluginStatusContent" style="margin-top: 8px;">
|
||||||
Loading plugin status...
|
Loading plugin status...
|
||||||
</div>
|
</div>
|
||||||
|
<div id="notificationReceivedIndicator" style="margin-top: 8px; padding: 8px; background: rgba(0, 255, 0, 0.2); border-radius: 5px; display: none;">
|
||||||
|
<strong>🔔 Notification Received!</strong><br>
|
||||||
|
<span id="notificationReceivedTime"></span><br>
|
||||||
|
<small>Check the top of your screen for the notification banner</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -232,7 +237,8 @@
|
|||||||
const notificationTimeReadable = notificationTime.toLocaleTimeString();
|
const notificationTimeReadable = notificationTime.toLocaleTimeString();
|
||||||
status.innerHTML = '✅ Notification scheduled!<br>' +
|
status.innerHTML = '✅ Notification scheduled!<br>' +
|
||||||
'📥 Prefetch: ' + prefetchTimeReadable + ' (' + prefetchTimeString + ')<br>' +
|
'📥 Prefetch: ' + prefetchTimeReadable + ' (' + prefetchTimeString + ')<br>' +
|
||||||
'🔔 Notification: ' + notificationTimeReadable + ' (' + notificationTimeString + ')';
|
'🔔 Notification: ' + notificationTimeReadable + ' (' + notificationTimeString + ')<br><br>' +
|
||||||
|
'<small>💡 When the notification fires, look for a banner at the <strong>top of your screen</strong>.</small>';
|
||||||
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
|
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
|
||||||
// Refresh plugin status display
|
// Refresh plugin status display
|
||||||
setTimeout(() => loadPluginStatus(), 500);
|
setTimeout(() => loadPluginStatus(), 500);
|
||||||
@@ -411,6 +417,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for notification delivery periodically
|
||||||
|
function checkNotificationDelivery() {
|
||||||
|
if (!window.DailyNotification) return;
|
||||||
|
|
||||||
|
window.DailyNotification.getNotificationStatus()
|
||||||
|
.then(result => {
|
||||||
|
if (result.lastNotificationTime) {
|
||||||
|
const lastTime = new Date(result.lastNotificationTime);
|
||||||
|
const now = new Date();
|
||||||
|
const timeDiff = now - lastTime;
|
||||||
|
|
||||||
|
// If notification was received in the last 2 minutes, show indicator
|
||||||
|
if (timeDiff > 0 && timeDiff < 120000) {
|
||||||
|
const indicator = document.getElementById('notificationReceivedIndicator');
|
||||||
|
const timeSpan = document.getElementById('notificationReceivedTime');
|
||||||
|
|
||||||
|
if (indicator && timeSpan) {
|
||||||
|
indicator.style.display = 'block';
|
||||||
|
timeSpan.textContent = `Received at ${lastTime.toLocaleTimeString()}`;
|
||||||
|
|
||||||
|
// Hide after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
indicator.style.display = 'none';
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Silently fail - this is just for visual feedback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Load plugin status automatically on page load
|
// Load plugin status automatically on page load
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
console.log('Page loaded, loading plugin status...');
|
console.log('Page loaded, loading plugin status...');
|
||||||
@@ -419,6 +458,9 @@
|
|||||||
loadPluginStatus();
|
loadPluginStatus();
|
||||||
loadPermissionStatus();
|
loadPermissionStatus();
|
||||||
loadChannelStatus();
|
loadChannelStatus();
|
||||||
|
|
||||||
|
// Check for notification delivery every 5 seconds
|
||||||
|
setInterval(checkNotificationDelivery, 5000);
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
234
test-apps/android-test-app/docs/PHASE1_TEST0_GOLDEN.md
Normal file
234
test-apps/android-test-app/docs/PHASE1_TEST0_GOLDEN.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# Phase 1 — TEST 0 Golden Run (Daily Rollover Verification)
|
||||||
|
|
||||||
|
**Last Updated:** 2025-12-04
|
||||||
|
**Status:** ✅ PASS (Golden Baseline)
|
||||||
|
|
||||||
|
**Related Docs:**
|
||||||
|
- [PHASE1_TEST0_GOLDEN.md](./PHASE1_TEST0_GOLDEN.md) - Daily Rollover Verification (this document)
|
||||||
|
- [PHASE1_TEST1_GOLDEN.md](./PHASE1_TEST1_GOLDEN.md) - Force-Stop Recovery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Test Overview
|
||||||
|
|
||||||
|
This document captures a **golden baseline** for **Phase 1 – TEST 0: Daily Rollover Verification**.
|
||||||
|
|
||||||
|
**Purpose:** Verify that after a notification fires, the plugin:
|
||||||
|
- Computes **next day's time** (T + 24h)
|
||||||
|
- Schedules **exactly one** `AlarmManager` notification alarm for tomorrow
|
||||||
|
- Does **not** create duplicates
|
||||||
|
- Leaves prefetch in **WorkManager** (not visible in `dumpsys alarm`)
|
||||||
|
|
||||||
|
> **Golden Rule:** If a future run looks like this doc, TEST 0 should be considered a PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Environment & Build Info
|
||||||
|
|
||||||
|
### Emulator / Device
|
||||||
|
- **API Level:** 35
|
||||||
|
- **Android Version:** 15
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- **Gradle Version:** 8.13
|
||||||
|
- **Build Warnings:**
|
||||||
|
- `WARNING: Using flatDir should be avoided because it doesn't support any meta-data formats.`
|
||||||
|
- `Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.`
|
||||||
|
|
||||||
|
### Command Used
|
||||||
|
```bash
|
||||||
|
./test-phase1.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
- `ENABLE_SCREENSHOTS=1` (screenshots enabled)
|
||||||
|
- `ADB_BIN=adb` (default)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Step-by-Step Execution (Golden Run)
|
||||||
|
|
||||||
|
1. Ran `./test-phase1.sh`.
|
||||||
|
2. Confirmed pre-flight checks (ADB device + emulator ready).
|
||||||
|
3. Allowed script to rebuild and reinstall the app.
|
||||||
|
4. Confirmed plugin status in the UI:
|
||||||
|
- ⚙️ Plugin Settings: ✅ Configured
|
||||||
|
- 🔌 Native Fetcher: ✅ Configured
|
||||||
|
- 🔔 Notifications: ✅ Granted
|
||||||
|
- ⏰ Exact Alarms: ✅ Granted
|
||||||
|
- 📢 Channel: ✅ Enabled (High)
|
||||||
|
5. From the UI, scheduled a **daily notification** for ~1–2 minutes in the future (scheduled for `09:23:00` on 2025-12-04).
|
||||||
|
6. Waited for the notification banner to fire.
|
||||||
|
7. Pressed Enter to continue when prompted.
|
||||||
|
8. Let the script perform the post-rollover alarm check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Expected Script Output (Key Excerpts)
|
||||||
|
|
||||||
|
### 4.1. Pre-Schedule Check
|
||||||
|
```text
|
||||||
|
✅ Found 1 notification alarm (expected: 1) – preliminary check passed.
|
||||||
|
ℹ️ This is preliminary check only; final verdict after rollover.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2. Notification Alarm Details (After Scheduling)
|
||||||
|
```text
|
||||||
|
ℹ️ Notification alarm details:
|
||||||
|
tag=*walarm*:com.timesafari.daily.NOTIFICATION
|
||||||
|
type=RTC_WAKEUP origWhen=2025-12-04 09:23:00.000 window=0 exactAllowReason=policy_permission repeatInterval=0 count=0 flags=0x3
|
||||||
|
policyWhenElapsed: requester=+3m34s315ms app_standby=-10s456ms device_idle=-- battery_saver=--
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3. Post-Rollover Check
|
||||||
|
```text
|
||||||
|
ℹ️ Polling for stable alarm count (allowing up to ~10 seconds for Android to settle)...
|
||||||
|
ℹ️ Notification alarms after rollover: 1 (expected: 1)
|
||||||
|
ℹ️ System/other alarms: <N> (for context)
|
||||||
|
ℹ️ Note: Prefetch is scheduled via WorkManager (not AlarmManager), so it won't appear in alarm count
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4. Final Verdict
|
||||||
|
```text
|
||||||
|
✅ TEST 0 PASSED: Daily rollover created exactly one NOTIFICATION alarm for tomorrow.
|
||||||
|
Expected state after rollover:
|
||||||
|
✅ 1 notification alarm (AlarmManager) for tomorrow
|
||||||
|
✅ 1 prefetch job (WorkManager) for 2 minutes before tomorrow's notification
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The `origWhen` for tomorrow will be the next day at the same time (e.g., `2025-12-05 09:23:00.000` if scheduled for `2025-12-04 09:23:00.000`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Expected UI State (Screenshots)
|
||||||
|
|
||||||
|
### 5.1 Screenshot Files (Golden Run)
|
||||||
|
|
||||||
|
- `screenshots/phase1_test0_daily_rollover/phase1_test0_daily_rollover_before_scheduling_20251204-091910.png`
|
||||||
|
- **Status:** Active Schedules: **No**; Next Notification: **None scheduled**.
|
||||||
|
|
||||||
|
- `screenshots/phase1_test0_daily_rollover/phase1_test0_daily_rollover_after_scheduling_20251204-091925.png`
|
||||||
|
- **Status:** Active Schedules: **Yes**; Next Notification: **today at 09:23:00 AM**; Pending: **1**.
|
||||||
|
|
||||||
|
- `screenshots/phase1_test0_daily_rollover/phase1_test0_daily_rollover_after_rollover_check_20251204-092307.png`
|
||||||
|
- **Status:** Active Schedules: **Yes**; Next Notification: **tomorrow at 09:23:00 AM** (24 hours later); Pending: **1**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Expected `dumpsys alarm` Shape
|
||||||
|
|
||||||
|
### 6.1. Representative Snippet
|
||||||
|
|
||||||
|
```text
|
||||||
|
RTC_WAKEUP #<N>: Alarm{<handle> type 0 origWhen <timestamp> whenElapsed ... com.timesafari.dailynotification}
|
||||||
|
tag=*walarm*:com.timesafari.daily.NOTIFICATION
|
||||||
|
type=RTC_WAKEUP origWhen=2025-12-05 09:23:00.000 ...
|
||||||
|
...
|
||||||
|
|
||||||
|
Next wake from idle: Alarm{<handle> type 0 origWhen <timestamp> ... com.timesafari.dailynotification}
|
||||||
|
tag=*walarm*:com.timesafari.daily.NOTIFICATION
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2. Key Observations
|
||||||
|
|
||||||
|
- There should be **exactly one unique alarm handle** for the plugin (the handle will differ between runs).
|
||||||
|
- It can appear both in the main list and in **"Next wake from idle"**, but counted as **one** alarm (deduplication by alarm handle).
|
||||||
|
- `tag` must be `*walarm*:com.timesafari.daily.NOTIFICATION`.
|
||||||
|
- `type` must be `RTC_WAKEUP`.
|
||||||
|
- `origWhen` should be **tomorrow** at the same time-of-day as the scheduled notification (e.g., `2025-12-05 09:23:00.000` if scheduled for `2025-12-04 09:23:00.000`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Expected `logcat` Patterns
|
||||||
|
|
||||||
|
### 7.1. Scheduling Test Notification
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-SCHEDULE: Scheduling next daily alarm: id=daily_..., nextRun=2025-12-04 09:23:00, source=TEST_NOTIFICATION
|
||||||
|
DNP-NOTIFY: Stored notification content in database: id=daily_...
|
||||||
|
DNP-NOTIFY: Scheduling alarm: triggerTime=2025-12-04 09:23:00, ...
|
||||||
|
DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2. Rollover on Fire
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-SCHEDULE: Scheduling next daily alarm: id=daily_rollover_..., nextRun=2025-12-05 09:23:00, source=ROLLOVER_ON_FIRE
|
||||||
|
DNP-NOTIFY: Stored notification content in database: id=notify_...
|
||||||
|
DNP-NOTIFY: Scheduling alarm: triggerTime=2025-12-05 09:23:00, ...
|
||||||
|
DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3. Critical Requirements
|
||||||
|
|
||||||
|
**Both sequences must be present** for a true PASS:
|
||||||
|
- `source=TEST_NOTIFICATION` sequence when scheduling the initial test notification
|
||||||
|
- `source=ROLLOVER_ON_FIRE` sequence when the notification fires and schedules tomorrow's alarm
|
||||||
|
- Times must match: initial schedule time → tomorrow's time (T + 24h)
|
||||||
|
- Example: `2025-12-04 09:23:00` → `2025-12-05 09:23:00`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Quick Pass/Fail Checklist
|
||||||
|
|
||||||
|
A run of TEST 0 is a **PASS** if all of the following are true:
|
||||||
|
|
||||||
|
### Script Output
|
||||||
|
- [ ] Shows "Found 1 notification alarm (expected: 1) – preliminary check passed."
|
||||||
|
- [ ] Shows "Notification alarms after rollover: 1 (expected: 1)".
|
||||||
|
- [ ] Ends with "✅ TEST 0 PASSED: Daily rollover created exactly one NOTIFICATION alarm for tomorrow."
|
||||||
|
|
||||||
|
### UI State
|
||||||
|
- [ ] **Before scheduling:** Active Schedules: No; Next Notification: None scheduled.
|
||||||
|
- [ ] **After scheduling:** Active Schedules: Yes; Next Notification: *today* at the chosen time.
|
||||||
|
- [ ] **After rollover:** Active Schedules: Yes; Next Notification: *tomorrow* at the same time.
|
||||||
|
|
||||||
|
### `dumpsys alarm`
|
||||||
|
- [ ] Exactly one `RTC_WAKEUP` alarm with `tag=*walarm*:com.timesafari.daily.NOTIFICATION` for **tomorrow**.
|
||||||
|
- [ ] Same alarm handle may appear under "Next wake from idle", but no second distinct handle.
|
||||||
|
- [ ] `origWhen` timestamp is exactly 24 hours after the initial scheduled time.
|
||||||
|
|
||||||
|
### `logcat`
|
||||||
|
- [ ] Shows both `source=TEST_NOTIFICATION` and `source=ROLLOVER_ON_FIRE` sequences with matching times.
|
||||||
|
- [ ] No duplicate `DNP-SCHEDULE` entries for the same `nextRun` time.
|
||||||
|
- [ ] No errors or warnings related to alarm scheduling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Notes / Deviations
|
||||||
|
|
||||||
|
### Failure Conditions
|
||||||
|
- If there is exactly **0** alarms after rollover, treat as **INCONCLUSIVE** and investigate:
|
||||||
|
- Check `logcat` for `ROLLOVER_ON_FIRE` sequence
|
||||||
|
- Verify `dumpsys alarm` manually
|
||||||
|
- Check for scheduling errors in logs
|
||||||
|
- If there are **>1** alarms after rollover, treat as **FAIL** (duplicate alarm bug):
|
||||||
|
- Check for multiple `DNP-SCHEDULE` entries with same `nextRun`
|
||||||
|
- Verify idempotence checks are working
|
||||||
|
- Check for race conditions between rollover and recovery paths
|
||||||
|
|
||||||
|
### Time-of-Day Variations
|
||||||
|
- Time-of-day may differ in future golden runs; **structure and relationships must remain the same**.
|
||||||
|
- The key is: initial time → tomorrow's time (T + 24h), not the specific hour/minute.
|
||||||
|
|
||||||
|
### Screenshot Timestamps
|
||||||
|
- Screenshot filenames include timestamps (`YYYYMMDD-HHMMSS`), so exact filenames will differ between runs.
|
||||||
|
- Focus on the **content** of screenshots (UI state) rather than exact filenames.
|
||||||
|
|
||||||
|
### Alarm Handle Variations
|
||||||
|
- The alarm handle (e.g., `Alarm{1f00a1b}`) will differ between runs; this is expected.
|
||||||
|
- The important thing is that there is **exactly one unique handle** per scheduled alarm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Updating This Document
|
||||||
|
|
||||||
|
When updating this golden run document:
|
||||||
|
1. Update timestamps and IDs with actual values from your successful run
|
||||||
|
2. Replace placeholder values (marked with "Update...") with real data
|
||||||
|
3. Update screenshot filenames with actual timestamps
|
||||||
|
4. Add any environment-specific notes that might affect future runs
|
||||||
|
5. Document any deviations or edge cases encountered
|
||||||
|
|
||||||
|
**Last Golden Run Date:** 2025-12-04 (09:23:00 scheduled time)
|
||||||
|
|
||||||
327
test-apps/android-test-app/docs/PHASE1_TEST1_GOLDEN.md
Normal file
327
test-apps/android-test-app/docs/PHASE1_TEST1_GOLDEN.md
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
# Phase 1 — TEST 1 Golden Run (Force-Stop Recovery - Database Restoration)
|
||||||
|
|
||||||
|
**Related Docs:**
|
||||||
|
- [PHASE1_TEST0_GOLDEN.md](./PHASE1_TEST0_GOLDEN.md)
|
||||||
|
- [PHASE1_TEST1_GOLDEN.md](./PHASE1_TEST1_GOLDEN.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Test Overview
|
||||||
|
|
||||||
|
**Test Name:** TEST 1 — Force-Stop Recovery: Database Restoration
|
||||||
|
**Purpose:** Verify that after an Android **force-stop** (which clears all scheduled alarms), the plugin:
|
||||||
|
|
||||||
|
1. **Persists** the alarm schedule in its database.
|
||||||
|
2. **Detects** that alarms are missing on app launch.
|
||||||
|
3. **Rebuilds** the missing alarm(s) from the database.
|
||||||
|
4. Restores the **one-notification-per-day** contract:
|
||||||
|
- Before force-stop: 1 alarm
|
||||||
|
- After force-stop: 0 alarms
|
||||||
|
- After recovery: 1 alarm (same trigger time as before force-stop)
|
||||||
|
|
||||||
|
This golden run documents a **known-good execution** on 2025-12-04.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Environment & Build Info
|
||||||
|
|
||||||
|
- **Date/Time of Run:** 2025-12-04 (around 09:51–09:55 UTC)
|
||||||
|
- **Command Used:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test-phase1.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Project:** `daily-notification-plugin` — `test-apps/android-test-app`
|
||||||
|
|
||||||
|
* **Gradle:**
|
||||||
|
|
||||||
|
* Build invoked via `./gradlew assembleDebug` (through the script)
|
||||||
|
* Gradle output included:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
|
||||||
|
```
|
||||||
|
|
||||||
|
* **APK Built:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
./app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
* **App ID:** `com.timesafari.dailynotification`
|
||||||
|
|
||||||
|
> **Note:** Device/emulator model & API level can be added here later if desired.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Step-by-Step Execution (Golden Run)
|
||||||
|
|
||||||
|
This section captures the **actual** sequence for the golden run.
|
||||||
|
|
||||||
|
1. Ran `./test-phase1.sh`.
|
||||||
|
2. Pre-flight checks:
|
||||||
|
* ✅ ADB device connected
|
||||||
|
* ✅ Emulator ready
|
||||||
|
3. Build phase:
|
||||||
|
* ✅ Debug APK built successfully.
|
||||||
|
* ✅ APK path: `./app/build/outputs/apk/debug/app-debug.apk`
|
||||||
|
4. Install phase:
|
||||||
|
* ✅ Previous app uninstall succeeded
|
||||||
|
* ✅ APK installed successfully
|
||||||
|
* ✅ App verified in package list
|
||||||
|
* ✅ Logcat cleared (twice)
|
||||||
|
5. **TEST 0** executed:
|
||||||
|
* Configured plugin in UI (if needed).
|
||||||
|
* Scheduled daily notification.
|
||||||
|
* Verified:
|
||||||
|
* Before scheduling: 0 plugin alarms.
|
||||||
|
* After scheduling: 1 plugin alarm for **today**.
|
||||||
|
* After fire/rollover: 1 plugin alarm for **tomorrow**.
|
||||||
|
* **TEST 0 PASSED.**
|
||||||
|
6. **TEST 1** started:
|
||||||
|
* Step 1: Detected 1 lingering plugin alarm (tomorrow's alarm from TEST 0).
|
||||||
|
* Auto-reset:
|
||||||
|
* Uninstalled app
|
||||||
|
* Reinstalled APK
|
||||||
|
* Cleared logcat
|
||||||
|
* Verified plugin alarms = 0
|
||||||
|
* Confirmed **clean starting state** for TEST 1.
|
||||||
|
7. Step 2: Launched app and confirmed plugin configured.
|
||||||
|
8. In UI: tapped **"Test Notification"** button (schedules a notification ~4 minutes in the future).
|
||||||
|
9. Step 3 (pre-FS verify):
|
||||||
|
* Verified **1** plugin alarm exists in AlarmManager.
|
||||||
|
* Confirmed alarm details (time, tag, type).
|
||||||
|
* Confirmed scheduling logs with `source=TEST_NOTIFICATION`.
|
||||||
|
10. Step 4: Performed **force-stop** via `adb shell am force-stop com.timesafari.dailynotification`.
|
||||||
|
11. Step 5 (post-FS verify):
|
||||||
|
* Verified plugin alarms = **0** (force-stop cleared alarms).
|
||||||
|
12. Step 6: Relaunched app (cold start).
|
||||||
|
13. Step 7 (recovery verify):
|
||||||
|
* Verified plugin alarms = **1** after recovery.
|
||||||
|
* Confirmed recovery logs under `DNP-REACTIVATION`:
|
||||||
|
* App launch recovery
|
||||||
|
* Boot recovery
|
||||||
|
* `rescheduled=1`
|
||||||
|
* Confirmed rescheduled alarm uses the **same scheduleId and triggerTime** as pre-force-stop.
|
||||||
|
14. Fire verification was **skipped** (`VERIFY_FIRE=false`).
|
||||||
|
15. TEST 1 summary:
|
||||||
|
* ✅ Before FS: 1 alarm
|
||||||
|
* ✅ After FS: 0 alarms
|
||||||
|
* ✅ After recovery: 1 alarm
|
||||||
|
* ✅ `rescheduled=1`, `errors=0`
|
||||||
|
* ✅ TEST 1 PASSED.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Expected Script Output (Key Excerpts)
|
||||||
|
|
||||||
|
These are the **critical excerpts** from the test harness output for a passing TEST 1.
|
||||||
|
|
||||||
|
### 4.1 Clean Start & Auto-Reset
|
||||||
|
|
||||||
|
```text
|
||||||
|
→ Step 1: Clean start - checking for lingering alarms...
|
||||||
|
ℹ️ Current plugin notification alarms: 1
|
||||||
|
ℹ️ System/other alarms: 17 (for context)
|
||||||
|
⚠️ Found 1 lingering plugin alarm(s) - these will interfere with TEST 1.
|
||||||
|
ℹ️ TEST 1 needs a clean state (no existing plugin alarms).
|
||||||
|
ℹ️ Resetting app state via uninstall + reinstall...
|
||||||
|
ℹ️ Uninstalling existing app...
|
||||||
|
✅ App uninstall succeeded (clean slate).
|
||||||
|
ℹ️ Reinstalling APK...
|
||||||
|
✅ App reinstall succeeded.
|
||||||
|
ℹ️ Clearing logcat buffer after reinstall...
|
||||||
|
ℹ️ Rechecking plugin alarms after reset...
|
||||||
|
ℹ️ Plugin alarms after reset: 0 (expected: 0)
|
||||||
|
✅ App state reset complete. TEST 1 starting from clean state.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Alarm Scheduled Before Force-Stop
|
||||||
|
|
||||||
|
```text
|
||||||
|
→ Step 3: Verifying alarm exists in AlarmManager (BEFORE force-stop)...
|
||||||
|
ℹ️ Plugin alarms: 1 (expected: 1)
|
||||||
|
ℹ️ System/other alarms: 19 (for context)
|
||||||
|
✅ ✅ Single plugin alarm confirmed in AlarmManager (one per day)
|
||||||
|
ℹ️ Alarm details:
|
||||||
|
RTC_WAKEUP #4: Alarm{161cd2b type 0 origWhen 1764842100000 whenElapsed 13009441 com.timesafari.dailynotification}
|
||||||
|
tag=*walarm*:com.timesafari.daily.NOTIFICATION
|
||||||
|
type=RTC_WAKEUP origWhen=2025-12-04 09:55:00.000 window=0 exactAllowReason=policy_permission repeatInterval=0 count=0 flags=0x3
|
||||||
|
policyWhenElapsed: requester=+3m4s281ms app_standby=-6s566ms device_idle=-- battery_saver=--
|
||||||
|
--
|
||||||
|
ℹ️ Alarm scheduled for: Thu Dec 4 09:55:00 AM UTC 2025 (1764842100000 ms)
|
||||||
|
ℹ️ Checking logs for scheduling confirmation...
|
||||||
|
12-04 09:51:49.150 6803 6867 W DNP-SCHEDULE: Cancelling existing alarm before rescheduling: requestCode=3454, scheduleId=daily_1764841909137, source=TEST_NOTIFICATION
|
||||||
|
12-04 09:51:49.151 6803 6867 I DNP-NOTIFY: Scheduling alarm: triggerTime=2025-12-04 09:55:00, delayMs=190849, requestCode=3454, scheduleId=daily_1764841909137
|
||||||
|
12-04 09:51:49.152 6803 6867 I DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, triggerTime=1764842100000, requestCode=3454, scheduleId=daily_1764841909137, source=TEST_NOTIFICATION, pendingIntentHash=267839060, showIntentHash=256236029
|
||||||
|
12-04 09:51:49.153 6803 6867 I DNP-NOTIFY: Alarm clock scheduled (setAlarmClock): triggerAt=1764842100000, requestCode=3454
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Force-Stop & Alarm Clearance
|
||||||
|
|
||||||
|
```text
|
||||||
|
→ Step 4: Force-stopping app (clears all alarms)...
|
||||||
|
⚠️ Force-stop will clear ALL alarms from AlarmManager
|
||||||
|
ℹ️ Executing: adb shell am force-stop com.timesafari.dailynotification
|
||||||
|
ℹ️ Forcing stop of app process...
|
||||||
|
✅ Force stop issued
|
||||||
|
→ Step 5: Verifying alarms are MISSING from AlarmManager (AFTER force-stop)...
|
||||||
|
ℹ️ Plugin alarms after force-stop: 0 (expected: 0)
|
||||||
|
ℹ️ System/other alarms: 17 (for context)
|
||||||
|
✅ ✅ Plugin alarms cleared by force-stop (count: 0)
|
||||||
|
ℹ️ This confirms: Force-stop cleared alarms from AlarmManager
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Recovery & Rescheduling
|
||||||
|
|
||||||
|
```text
|
||||||
|
→ Step 6: Relaunching app (triggers recovery from database)...
|
||||||
|
ℹ️ Clearing logcat buffer...
|
||||||
|
✅ Logs cleared
|
||||||
|
ℹ️ Launching app...
|
||||||
|
Starting: Intent { cmp=com.timesafari.dailynotification/.MainActivity }
|
||||||
|
✅ App launched
|
||||||
|
→ Step 7: Verifying recovery rebuilt alarms from database...
|
||||||
|
ℹ️ Plugin alarms after recovery: 1 (expected: 1)
|
||||||
|
ℹ️ System/other alarms: 21 (for context)
|
||||||
|
ℹ️ Checking recovery logs...
|
||||||
|
|
||||||
|
12-04 09:52:21.919 6954 7015 I DNP-REACTIVATION: Starting app launch recovery (Phase 1: cold start only)
|
||||||
|
12-04 09:52:21.926 6954 7015 I DNP-REACTIVATION: Cold start recovery: checking for missed notifications
|
||||||
|
12-04 09:52:21.937 6954 7015 I DNP-REACTIVATION: Rescheduled alarm: daily_1764841909137 for 1764842100000
|
||||||
|
12-04 09:52:21.937 6954 7015 I DNP-REACTIVATION: Rescheduled missing alarm: daily_1764841909137 at 1764842100000
|
||||||
|
12-04 09:52:21.952 6954 7015 I DNP-REACTIVATION: Cold start recovery complete: missed=0, rescheduled=1, verified=0, errors=0
|
||||||
|
12-04 09:52:21.952 6954 7015 I DNP-REACTIVATION: App launch recovery completed: missed=0, rescheduled=1, verified=0, errors=0
|
||||||
|
12-04 09:52:22.077 6954 7015 I DNP-REACTIVATION: Starting boot recovery
|
||||||
|
12-04 09:52:22.135 6954 7017 I DNP-REACTIVATION: Loaded 1 schedules from DB
|
||||||
|
12-04 09:52:22.138 6954 7017 I DNP-REACTIVATION: Rescheduled alarm: daily_1764841909137 for 1764842100000
|
||||||
|
12-04 09:52:22.143 6954 7017 I DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=1, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Final summary:
|
||||||
|
|
||||||
|
```text
|
||||||
|
✅ ✅ Alarms restored in AlarmManager (count: 1)
|
||||||
|
✅ ✅ Recovery logs confirm rescheduling (rescheduled=1)
|
||||||
|
✅ TEST 1 PASSED: Recovery successfully rebuilt alarms from database!
|
||||||
|
ℹ️ Summary:
|
||||||
|
- Before force-stop: 1 alarm(s)
|
||||||
|
- After force-stop: 0 alarm(s) (cleared)
|
||||||
|
- After recovery: 1 alarm(s) (rebuilt)
|
||||||
|
- Rescheduled: 1 alarm(s)
|
||||||
|
- Verified: 0 alarm(s)
|
||||||
|
ℹ️ Skipping fire verification (VERIFY_FIRE=false, set VERIFY_FIRE=true to enable)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Expected `dumpsys alarm` Shapes
|
||||||
|
|
||||||
|
### 5.1 Before Force-Stop (Scheduled)
|
||||||
|
|
||||||
|
* **Plugin alarm count:** 1
|
||||||
|
* Example block (shape, not necessarily exact handle):
|
||||||
|
|
||||||
|
```text
|
||||||
|
RTC_WAKEUP #4: Alarm{161cd2b type 0 origWhen 1764842100000 whenElapsed 13009441 com.timesafari.dailynotification}
|
||||||
|
tag=*walarm*:com.timesafari.daily.NOTIFICATION
|
||||||
|
type=RTC_WAKEUP origWhen=2025-12-04 09:55:00.000 window=0 exactAllowReason=policy_permission repeatInterval=0 count=0 flags=0x3
|
||||||
|
policyWhenElapsed: requester=+3m4s281ms app_standby=-6s566ms device_idle=-- battery_saver=--
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 After Force-Stop
|
||||||
|
|
||||||
|
* **Plugin alarm count:** 0
|
||||||
|
* No `*walarm*:com.timesafari.daily.NOTIFICATION` entries should appear.
|
||||||
|
|
||||||
|
### 5.3 After Recovery
|
||||||
|
|
||||||
|
* **Plugin alarm count:** 1
|
||||||
|
* Alarm should be restored with:
|
||||||
|
* Same `scheduleId`: `daily_1764841909137`
|
||||||
|
* Same `triggerTime`: `1764842100000` → `2025-12-04 09:55:00`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Expected `logcat` Patterns
|
||||||
|
|
||||||
|
For a passing run, look for:
|
||||||
|
|
||||||
|
* **Scheduling (before FS):**
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-NOTIFY: Scheduling alarm: triggerTime=2025-12-04 09:55:00, delayMs=..., requestCode=3454, scheduleId=daily_1764841909137
|
||||||
|
DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, triggerTime=1764842100000, requestCode=3454, scheduleId=daily_1764841909137, source=TEST_NOTIFICATION, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Recovery (after FS + relaunch):**
|
||||||
|
|
||||||
|
```text
|
||||||
|
DNP-REACTIVATION: Starting app launch recovery (Phase 1: cold start only)
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_1764841909137 for 1764842100000
|
||||||
|
DNP-REACTIVATION: Cold start recovery complete: missed=0, rescheduled=1, verified=0, errors=0
|
||||||
|
DNP-REACTIVATION: App launch recovery completed: missed=0, rescheduled=1, verified=0, errors=0
|
||||||
|
|
||||||
|
DNP-REACTIVATION: Starting boot recovery
|
||||||
|
DNP-REACTIVATION: Loaded 1 schedules from DB
|
||||||
|
DNP-REACTIVATION: Rescheduled alarm: daily_1764841909137 for 1764842100000
|
||||||
|
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=1, verified=0, errors=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Key invariants:
|
||||||
|
|
||||||
|
* `rescheduled=1`
|
||||||
|
* `errors=0`
|
||||||
|
* `scheduleId` and `triggerTime` match pre-FS values.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Quick Pass/Fail Checklist
|
||||||
|
|
||||||
|
A TEST 1 run is a **PASS** if all of the following are true:
|
||||||
|
|
||||||
|
* **Starting state:**
|
||||||
|
* Script auto-resets if lingering alarms exist from TEST 0.
|
||||||
|
* After reset: plugin alarm count = **0**.
|
||||||
|
|
||||||
|
* **Pre-force-stop:**
|
||||||
|
* Plugin alarm count = **1**.
|
||||||
|
* Alarm is tagged `*walarm*:com.timesafari.daily.NOTIFICATION`.
|
||||||
|
* `triggerTime` and `origWhen` are consistent (e.g. `2025-12-04 09:55:00`, `1764842100000`).
|
||||||
|
* `scheduleId` looks like `daily_<timestamp>` (here: `daily_1764841909137`).
|
||||||
|
* Logs show `source=TEST_NOTIFICATION`.
|
||||||
|
|
||||||
|
* **After force-stop:**
|
||||||
|
* Plugin alarm count = **0**.
|
||||||
|
* No NOTIFICATION alarms remain in `dumpsys`.
|
||||||
|
|
||||||
|
* **After recovery (relaunch):**
|
||||||
|
* Plugin alarm count = **1**.
|
||||||
|
* Logs show `DNP-REACTIVATION` with:
|
||||||
|
* `rescheduled=1`
|
||||||
|
* `errors=0`
|
||||||
|
* Same `scheduleId` and `triggerTime` as pre-FS.
|
||||||
|
|
||||||
|
* **Script summary:**
|
||||||
|
* States:
|
||||||
|
* Before force-stop: 1 alarm
|
||||||
|
* After force-stop: 0 alarms
|
||||||
|
* After recovery: 1 alarm
|
||||||
|
* Ends with: `✅ TEST 1 PASSED: Recovery successfully rebuilt alarms from database!`
|
||||||
|
|
||||||
|
If any of these conditions fail, the run is **NOT GOLDEN** and should not overwrite this reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Notes / Deviations
|
||||||
|
|
||||||
|
* This golden run **skips fire verification** (`VERIFY_FIRE=false`).
|
||||||
|
Future runs may enable fire verification; if that becomes standard, update this doc.
|
||||||
|
* It is acceptable for both:
|
||||||
|
* **Cold start recovery** and
|
||||||
|
* **Boot recovery**
|
||||||
|
to run on app launch, as long as:
|
||||||
|
* `rescheduled=1`
|
||||||
|
* No duplicate alarms are scheduled.
|
||||||
|
* If OS behavior changes (e.g., force-stop no longer clears alarms on a future Android version), this test's expectations may need to be revised; document any such deviations explicitly here before changing the checklist.
|
||||||
1171
test-apps/android-test-app/test-phase1.sh
Executable file
1171
test-apps/android-test-app/test-phase1.sh
Executable file
File diff suppressed because it is too large
Load Diff
352
test-apps/android-test-app/test-phase2.sh
Executable file
352
test-apps/android-test-app/test-phase2.sh
Executable file
@@ -0,0 +1,352 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Phase 2 Testing Script – Force Stop Recovery
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Source shared library
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "${SCRIPT_DIR}/alarm-test-lib.sh"
|
||||||
|
|
||||||
|
# Phase 2 specific configuration
|
||||||
|
# Log tags / patterns (matched to actual ReactivationManager logs)
|
||||||
|
FORCE_STOP_SCENARIO_VALUE="FORCE_STOP"
|
||||||
|
COLD_START_SCENARIO_VALUE="COLD_START"
|
||||||
|
NONE_SCENARIO_VALUE="NONE"
|
||||||
|
BOOT_SCENARIO_VALUE="BOOT"
|
||||||
|
|
||||||
|
# Allow selecting specific tests on the command line (e.g. ./test-phase2.sh 2 3)
|
||||||
|
SELECTED_TESTS=()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# TEST 1 – Force Stop with Cleared Alarms
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test1_force_stop_cleared_alarms() {
|
||||||
|
section "TEST 1: Force Stop – Alarms Cleared"
|
||||||
|
|
||||||
|
echo "Purpose: Verify force stop detection and alarm rescheduling when alarms are cleared."
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 1: Launch app & check plugin status"
|
||||||
|
launch_app
|
||||||
|
|
||||||
|
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf either shows ❌ or 'Not configured', click 'Configure Plugin', wait until both are ✅, then press Enter."
|
||||||
|
|
||||||
|
ui_prompt "Now schedule at least one future notification (e.g., click 'Test Notification' to schedule for a few minutes in the future)."
|
||||||
|
|
||||||
|
substep "Step 2: Verify alarms are scheduled"
|
||||||
|
show_alarms
|
||||||
|
local before_count system_count
|
||||||
|
before_count="$(get_plugin_alarm_count)"
|
||||||
|
system_count="$(get_system_alarm_count)"
|
||||||
|
info "Plugin alarms before force stop: $before_count (expected: 1)"
|
||||||
|
info "System/other alarms: $system_count (for context)"
|
||||||
|
|
||||||
|
if [[ "$before_count" -eq 0 ]]; then
|
||||||
|
warn "No plugin alarms found before force stop; TEST 1 may not be meaningful."
|
||||||
|
elif [[ "$before_count" -eq 1 ]]; then
|
||||||
|
ok "Single plugin alarm confirmed (one per day)"
|
||||||
|
else
|
||||||
|
warn "Found $before_count plugin alarms (expected: 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 3: Force stop app (should clear alarms on many devices)"
|
||||||
|
force_stop_app
|
||||||
|
|
||||||
|
substep "Step 4: Check alarms after force stop"
|
||||||
|
local after_count system_after
|
||||||
|
after_count="$(get_plugin_alarm_count)"
|
||||||
|
system_after="$(get_system_alarm_count)"
|
||||||
|
info "Plugin alarms after force stop: $after_count (expected: 0)"
|
||||||
|
info "System/other alarms: $system_after (for context)"
|
||||||
|
show_alarms
|
||||||
|
|
||||||
|
if [[ "$after_count" -gt 0 ]]; then
|
||||||
|
warn "Plugin alarms still present after force stop. This device/OS may not clear alarms on force stop."
|
||||||
|
warn "TEST 1 will continue but may not fully validate FORCE_STOP scenario."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 4.5: Clear boot flag (prevent false BOOT detection)"
|
||||||
|
# Clear boot flag to ensure force stop detection works correctly
|
||||||
|
# Boot flag might be set from previous runs or emulator quirks
|
||||||
|
adb shell "run-as ${APP_ID} rm -f shared_prefs/dailynotification_recovery.xml 2>/dev/null || true"
|
||||||
|
info "Boot flag cleared (if it existed)"
|
||||||
|
|
||||||
|
substep "Step 5: Launch app (triggers recovery) and capture logs"
|
||||||
|
clear_logs
|
||||||
|
launch_app
|
||||||
|
sleep 5 # give recovery a moment to run
|
||||||
|
|
||||||
|
info "Collecting recovery logs..."
|
||||||
|
local logs
|
||||||
|
logs="$(get_recovery_logs)"
|
||||||
|
echo "$logs"
|
||||||
|
|
||||||
|
local scenario rescheduled verified errors
|
||||||
|
scenario="$(extract_scenario_from_logs "$logs")"
|
||||||
|
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||||
|
verified="$(extract_field_from_logs "$logs" "verified")"
|
||||||
|
errors="$(extract_field_from_logs "$logs" "errors")"
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Parsed recovery summary:"
|
||||||
|
echo " scenario = ${scenario:-<none>}"
|
||||||
|
echo " rescheduled= ${rescheduled}"
|
||||||
|
echo " verified = ${verified}"
|
||||||
|
echo " errors = ${errors}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ "$errors" -gt 0 ]]; then
|
||||||
|
error "Recovery reported errors>0 (errors=$errors)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$scenario" == "$FORCE_STOP_SCENARIO_VALUE" && "$rescheduled" -gt 0 ]]; then
|
||||||
|
ok "TEST 1 PASSED: Force stop detected and alarms rescheduled (scenario=$scenario, rescheduled=$rescheduled)."
|
||||||
|
elif [[ "$scenario" == "$FORCE_STOP_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
|
||||||
|
warn "TEST 1: scenario=FORCE_STOP but rescheduled=0. Check implementation or logs."
|
||||||
|
elif [[ "$after_count" -gt 0 ]]; then
|
||||||
|
info "TEST 1: Device/emulator kept alarms after force stop; FORCE_STOP scenario may not trigger here."
|
||||||
|
if [[ "$rescheduled" -gt 0 ]]; then
|
||||||
|
info "Recovery still worked (rescheduled=$rescheduled), but scenario was ${scenario:-COLD_START} instead of FORCE_STOP"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "TEST 1: Expected FORCE_STOP scenario not clearly detected. Review logs and scenario detection logic."
|
||||||
|
info "Scenario detected: ${scenario:-<none>}, rescheduled=$rescheduled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
substep "Step 6: Verify alarms are rescheduled in AlarmManager"
|
||||||
|
show_alarms
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# TEST 2 – Force Stop / Process Stop with Intact Alarms
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test2_force_stop_intact_alarms() {
|
||||||
|
section "TEST 2: Force Stop / Process Stop – Alarms Intact"
|
||||||
|
|
||||||
|
echo "Purpose: Verify that heavy FORCE_STOP recovery does not run when alarms are still present."
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 1: Launch app & schedule notifications"
|
||||||
|
launch_app
|
||||||
|
ui_prompt "In the app UI, ensure plugin is configured and schedule at least one future notification.\n\nPress Enter when done."
|
||||||
|
|
||||||
|
substep "Step 2: Verify alarms are scheduled"
|
||||||
|
show_alarms
|
||||||
|
local before system_before
|
||||||
|
before="$(get_plugin_alarm_count)"
|
||||||
|
system_before="$(get_system_alarm_count)"
|
||||||
|
info "Plugin alarms before stop: $before (expected: 1)"
|
||||||
|
info "System/other alarms: $system_before (for context)"
|
||||||
|
|
||||||
|
if [[ "$before" -eq 0 ]]; then
|
||||||
|
warn "No plugin alarms found; TEST 2 may not be meaningful."
|
||||||
|
elif [[ "$before" -eq 1 ]]; then
|
||||||
|
ok "Single plugin alarm confirmed (one per day)"
|
||||||
|
else
|
||||||
|
warn "Found $before plugin alarms (expected: 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 3: Simulate a 'soft' stop or process kill that does NOT clear alarms"
|
||||||
|
info "Killing app process (non-destructive - may not clear alarms)..."
|
||||||
|
$ADB_BIN shell am kill "$APP_ID" || true
|
||||||
|
sleep 2
|
||||||
|
ok "Kill signal sent (soft stop)"
|
||||||
|
|
||||||
|
substep "Step 4: Verify alarms are still scheduled"
|
||||||
|
local after system_after
|
||||||
|
after="$(get_plugin_alarm_count)"
|
||||||
|
system_after="$(get_system_alarm_count)"
|
||||||
|
info "Plugin alarms after soft stop: $after (expected: 1)"
|
||||||
|
info "System/other alarms: $system_after (for context)"
|
||||||
|
show_alarms
|
||||||
|
|
||||||
|
if [[ "$after" -eq 0 ]]; then
|
||||||
|
warn "Alarms appear cleared after soft stop; this environment may not distinguish TEST 2 well."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 5: Relaunch app and check recovery logs"
|
||||||
|
clear_logs
|
||||||
|
launch_app
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
info "Collecting recovery logs..."
|
||||||
|
local logs
|
||||||
|
logs="$(get_recovery_logs)"
|
||||||
|
echo "$logs"
|
||||||
|
|
||||||
|
local scenario rescheduled missed verified errors
|
||||||
|
scenario="$(extract_scenario_from_logs "$logs")"
|
||||||
|
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||||
|
missed="$(extract_field_from_logs "$logs" "missed")"
|
||||||
|
verified="$(extract_field_from_logs "$logs" "verified")"
|
||||||
|
errors="$(extract_field_from_logs "$logs" "errors")"
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Parsed recovery summary:"
|
||||||
|
echo " scenario = ${scenario:-<none>}"
|
||||||
|
echo " rescheduled= ${rescheduled}"
|
||||||
|
echo " missed = ${missed}"
|
||||||
|
echo " verified = ${verified}"
|
||||||
|
echo " errors = ${errors}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ "$errors" -gt 0 ]]; then
|
||||||
|
error "Recovery reported errors>0 (errors=$errors)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$after" -gt 0 && "$rescheduled" -eq 0 && "$scenario" != "$FORCE_STOP_SCENARIO_VALUE" ]]; then
|
||||||
|
ok "TEST 2 PASSED: Alarms remained intact, and FORCE_STOP scenario did not run (scenario=$scenario, rescheduled=0)."
|
||||||
|
else
|
||||||
|
warn "TEST 2: Verify that FORCE_STOP recovery didn't misfire when alarms were intact."
|
||||||
|
info "Scenario=${scenario:-<none>}, rescheduled=$rescheduled, after_count=$after"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# TEST 3 – First Launch / Empty DB Safeguard
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test3_first_launch_no_schedules() {
|
||||||
|
section "TEST 3: First Launch / No Schedules Safeguard"
|
||||||
|
|
||||||
|
echo "Purpose: Ensure force-stop recovery is NOT triggered when DB is empty or plugin isn't configured."
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 1: Uninstall app to clear DB/state"
|
||||||
|
set +e
|
||||||
|
$ADB_BIN uninstall "$APP_ID" >/dev/null 2>&1
|
||||||
|
set -e
|
||||||
|
ok "App uninstalled (state cleared)"
|
||||||
|
|
||||||
|
substep "Step 2: Reinstall app"
|
||||||
|
if $ADB_BIN install -r "$APK_PATH"; then
|
||||||
|
ok "App installed"
|
||||||
|
else
|
||||||
|
error "Reinstall failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Clearing logcat..."
|
||||||
|
$ADB_BIN logcat -c
|
||||||
|
ok "Logs cleared"
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 3: Launch app for the first time"
|
||||||
|
launch_app
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
substep "Step 4: Collect logs and ensure no force-stop recovery ran"
|
||||||
|
local logs
|
||||||
|
logs="$(get_recovery_logs)"
|
||||||
|
echo "$logs"
|
||||||
|
|
||||||
|
local scenario rescheduled
|
||||||
|
scenario="$(extract_scenario_from_logs "$logs")"
|
||||||
|
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Parsed summary:"
|
||||||
|
echo " scenario = ${scenario:-<none>}"
|
||||||
|
echo " rescheduled= ${rescheduled}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ -z "$logs" ]]; then
|
||||||
|
ok "TEST 3 PASSED: No force-stop recovery logs on first launch."
|
||||||
|
elif [[ "$scenario" == "$NONE_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
|
||||||
|
ok "TEST 3 PASSED: NONE scenario logged with rescheduled=0 on first launch."
|
||||||
|
elif [[ "$rescheduled" -gt 0 ]]; then
|
||||||
|
warn "TEST 3: rescheduled>0 on first launch / empty DB. Check that force-stop recovery isn't misfiring."
|
||||||
|
else
|
||||||
|
info "TEST 3: Logs present but no rescheduling; review scenario handling to ensure it's explicit about NONE / FIRST_LAUNCH."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Allow selecting specific tests: e.g. `./test-phase2.sh 1 3`
|
||||||
|
if [[ "$#" -gt 0 && ( "$1" == "-h" || "$1" == "--help" ) ]]; then
|
||||||
|
echo "Usage: $0 [TEST_IDS...]"
|
||||||
|
echo
|
||||||
|
echo "If no TEST_IDS are given, all tests (1, 2, 3) will run."
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 # run all tests"
|
||||||
|
echo " $0 1 # run only TEST 1"
|
||||||
|
echo " $0 2 3 # run only TEST 2 and TEST 3"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SELECTED_TESTS=("$@")
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "========================================"
|
||||||
|
echo "Phase 2 Testing Script – Force Stop Recovery"
|
||||||
|
echo "========================================"
|
||||||
|
echo
|
||||||
|
echo "This script will guide you through Phase 2 tests."
|
||||||
|
echo "You'll be prompted when UI interaction is needed."
|
||||||
|
echo
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
require_adb_device
|
||||||
|
build_app
|
||||||
|
install_app
|
||||||
|
|
||||||
|
if should_run_test "1" SELECTED_TESTS; then
|
||||||
|
test1_force_stop_cleared_alarms
|
||||||
|
pause
|
||||||
|
fi
|
||||||
|
|
||||||
|
if should_run_test "2" SELECTED_TESTS; then
|
||||||
|
test2_force_stop_intact_alarms
|
||||||
|
pause
|
||||||
|
fi
|
||||||
|
|
||||||
|
if should_run_test "3" SELECTED_TESTS; then
|
||||||
|
test3_first_launch_no_schedules
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Testing Complete"
|
||||||
|
|
||||||
|
echo "Test Results Summary (see logs above for details):"
|
||||||
|
echo
|
||||||
|
echo "TEST 1: Force Stop – Alarms Cleared"
|
||||||
|
echo " - Check logs for scenario=$FORCE_STOP_SCENARIO_VALUE and rescheduled>0"
|
||||||
|
echo
|
||||||
|
echo "TEST 2: Force Stop / Process Stop – Alarms Intact"
|
||||||
|
echo " - Verify FORCE_STOP scenario is not incorrectly triggered when alarms are still present"
|
||||||
|
echo
|
||||||
|
echo "TEST 3: First Launch / No Schedules"
|
||||||
|
echo " - Confirm that no force-stop recovery runs, or that NONE/FIRST_LAUNCH scenario is logged with rescheduled=0"
|
||||||
|
echo
|
||||||
|
|
||||||
|
ok "Phase 2 testing script complete!"
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " - Review logs above"
|
||||||
|
echo " - Capture snippets into PHASE2-EMULATOR-TESTING.md"
|
||||||
|
echo " - Update PHASE2-VERIFICATION.md and unified directive status matrix"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
437
test-apps/android-test-app/test-phase3.sh
Executable file
437
test-apps/android-test-app/test-phase3.sh
Executable file
@@ -0,0 +1,437 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Phase 3 Testing Script – Boot Recovery
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Source shared library
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "${SCRIPT_DIR}/alarm-test-lib.sh"
|
||||||
|
|
||||||
|
# Phase 3 specific configuration
|
||||||
|
# Log tags / patterns (matched to actual ReactivationManager logs)
|
||||||
|
BOOT_SCENARIO_VALUE="BOOT"
|
||||||
|
NONE_SCENARIO_VALUE="NONE"
|
||||||
|
|
||||||
|
# Allow selecting specific tests on the command line (e.g. ./test-phase3.sh 1 3)
|
||||||
|
SELECTED_TESTS=()
|
||||||
|
|
||||||
|
# Phase 3 specific: override extract_scenario_from_logs to handle boot recovery
|
||||||
|
extract_scenario_from_logs() {
|
||||||
|
local logs="$1"
|
||||||
|
local scen
|
||||||
|
# Looks for "Detected scenario: BOOT" or "Starting boot recovery" format
|
||||||
|
if echo "$logs" | grep -qi "Starting boot recovery\|boot recovery"; then
|
||||||
|
echo "$BOOT_SCENARIO_VALUE"
|
||||||
|
else
|
||||||
|
# Use shared library function as fallback
|
||||||
|
scen="$(grep -oE "${SCENARIO_KEY}[A-Z_]+" <<<"$logs" | tail -n1 | sed "s/${SCENARIO_KEY}//" || true)"
|
||||||
|
echo "$scen"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# TEST 1 – Boot with Future Alarms
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test1_boot_future_alarms() {
|
||||||
|
section "TEST 1: Boot with Future Alarms"
|
||||||
|
|
||||||
|
echo "Purpose: Verify alarms are recreated on boot when schedules have future run times."
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 1: Launch app & check plugin status"
|
||||||
|
launch_app
|
||||||
|
|
||||||
|
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf either shows ❌ or 'Not configured', click 'Configure Plugin', wait until both are ✅, then press Enter."
|
||||||
|
|
||||||
|
ui_prompt "Now schedule at least one future notification (e.g., click 'Test Notification' to schedule for a few minutes in the future)."
|
||||||
|
|
||||||
|
substep "Step 2: Verify alarms are scheduled"
|
||||||
|
show_alarms
|
||||||
|
local before_count system_before
|
||||||
|
before_count="$(get_plugin_alarm_count)"
|
||||||
|
system_before="$(get_system_alarm_count)"
|
||||||
|
info "Plugin alarms before reboot: $before_count (expected: 1)"
|
||||||
|
info "System/other alarms: $system_before (for context)"
|
||||||
|
|
||||||
|
if [[ "$before_count" -eq 0 ]]; then
|
||||||
|
warn "No plugin alarms found before reboot; TEST 1 may not be meaningful."
|
||||||
|
elif [[ "$before_count" -eq 1 ]]; then
|
||||||
|
ok "Single plugin alarm confirmed (one per day)"
|
||||||
|
else
|
||||||
|
warn "Found $before_count plugin alarms (expected: 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 3: Reboot emulator"
|
||||||
|
warn "The emulator will reboot now. This will take 30-60 seconds."
|
||||||
|
pause
|
||||||
|
reboot_emulator
|
||||||
|
|
||||||
|
substep "Step 4: Collect boot recovery logs"
|
||||||
|
info "Collecting recovery logs from boot..."
|
||||||
|
sleep 2 # Give recovery a moment to complete
|
||||||
|
local logs
|
||||||
|
logs="$(get_recovery_logs)"
|
||||||
|
echo "$logs"
|
||||||
|
|
||||||
|
local missed rescheduled verified errors scenario
|
||||||
|
missed="$(extract_field_from_logs "$logs" "missed")"
|
||||||
|
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||||
|
verified="$(extract_field_from_logs "$logs" "verified")"
|
||||||
|
errors="$(extract_field_from_logs "$logs" "errors")"
|
||||||
|
scenario="$(extract_scenario_from_logs "$logs")"
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Parsed recovery summary:"
|
||||||
|
echo " scenario = ${scenario:-<none>}"
|
||||||
|
echo " missed = ${missed}"
|
||||||
|
echo " rescheduled= ${rescheduled}"
|
||||||
|
echo " verified = ${verified}"
|
||||||
|
echo " errors = ${errors}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ "$errors" -gt 0 ]]; then
|
||||||
|
error "Recovery reported errors>0 (errors=$errors)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
substep "Step 5: Verify alarms were recreated"
|
||||||
|
show_alarms
|
||||||
|
local after_count system_after
|
||||||
|
after_count="$(get_plugin_alarm_count)"
|
||||||
|
system_after="$(get_system_alarm_count)"
|
||||||
|
info "Plugin alarms after boot: $after_count (expected: 1)"
|
||||||
|
info "System/other alarms: $system_after (for context)"
|
||||||
|
|
||||||
|
if [[ "$scenario" == "$BOOT_SCENARIO_VALUE" && "$rescheduled" -gt 0 ]]; then
|
||||||
|
ok "TEST 1 PASSED: Boot recovery detected and alarms rescheduled (scenario=$scenario, rescheduled=$rescheduled)."
|
||||||
|
elif echo "$logs" | grep -qi "Starting boot recovery\|boot recovery"; then
|
||||||
|
if [[ "$rescheduled" -gt 0 ]]; then
|
||||||
|
ok "TEST 1 PASSED: Boot recovery ran and alarms rescheduled (rescheduled=$rescheduled)."
|
||||||
|
else
|
||||||
|
warn "TEST 1: Boot recovery ran but rescheduled=0. Check implementation or logs."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "TEST 1: Boot recovery not clearly detected. Review logs and boot receiver implementation."
|
||||||
|
info "Scenario detected: ${scenario:-<none>}, rescheduled=$rescheduled"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# TEST 2 – Boot with Past Alarms
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test2_boot_past_alarms() {
|
||||||
|
section "TEST 2: Boot with Past Alarms"
|
||||||
|
|
||||||
|
echo "Purpose: Verify missed alarms are detected and next occurrence is scheduled on boot."
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 1: Launch app & ensure plugin configured"
|
||||||
|
launch_app
|
||||||
|
|
||||||
|
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf needed, click 'Configure Plugin', then press Enter."
|
||||||
|
|
||||||
|
ui_prompt "Click 'Test Notification' to schedule a notification for 2 minutes in the future.\n\nAfter scheduling, we'll wait for the alarm time to pass, then reboot."
|
||||||
|
|
||||||
|
substep "Step 2: Wait for alarm time to pass"
|
||||||
|
info "Waiting 3 minutes for scheduled alarm time to pass..."
|
||||||
|
warn "You can manually advance system time if needed (requires root/emulator)"
|
||||||
|
sleep 180 # Wait 3 minutes
|
||||||
|
|
||||||
|
substep "Step 3: Verify alarm time has passed"
|
||||||
|
info "Alarm time should now be in the past"
|
||||||
|
show_alarms
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 4: Reboot emulator"
|
||||||
|
warn "The emulator will reboot now. This will take 30-60 seconds."
|
||||||
|
pause
|
||||||
|
reboot_emulator
|
||||||
|
|
||||||
|
substep "Step 5: Collect boot recovery logs"
|
||||||
|
info "Collecting recovery logs from boot..."
|
||||||
|
sleep 2
|
||||||
|
local logs
|
||||||
|
logs="$(get_recovery_logs)"
|
||||||
|
echo "$logs"
|
||||||
|
|
||||||
|
local missed rescheduled verified errors scenario
|
||||||
|
# For TEST 2, we need the FIRST entry (which has missed count) not the last
|
||||||
|
# Boot recovery runs twice (LOCKED_BOOT_COMPLETED and BOOT_COMPLETED)
|
||||||
|
# First run marks missed alarms, second run only reschedules (missed=0)
|
||||||
|
missed="$(echo "$logs" | grep -E "missed=" | sed -E "s/.*missed=([0-9]+).*/\1/" | head -n1 || echo "0")"
|
||||||
|
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||||
|
verified="$(extract_field_from_logs "$logs" "verified")"
|
||||||
|
errors="$(extract_field_from_logs "$logs" "errors")"
|
||||||
|
scenario="$(extract_scenario_from_logs "$logs")"
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Parsed recovery summary:"
|
||||||
|
echo " scenario = ${scenario:-<none>}"
|
||||||
|
echo " missed = ${missed}"
|
||||||
|
echo " rescheduled= ${rescheduled}"
|
||||||
|
echo " verified = ${verified}"
|
||||||
|
echo " errors = ${errors}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ "$errors" -gt 0 ]]; then
|
||||||
|
error "Recovery reported errors>0 (errors=$errors)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$missed" -ge 1 && "$rescheduled" -ge 1 ]]; then
|
||||||
|
ok "TEST 2 PASSED: Past alarms detected and next occurrence scheduled (missed=$missed, rescheduled=$rescheduled)."
|
||||||
|
elif [[ "$missed" -ge 1 ]]; then
|
||||||
|
warn "TEST 2: Past alarms detected (missed=$missed) but rescheduled=$rescheduled. Check reschedule logic."
|
||||||
|
else
|
||||||
|
warn "TEST 2: No missed alarms detected. Verify alarm time actually passed before reboot."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# TEST 3 – Boot with No Schedules
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test3_boot_no_schedules() {
|
||||||
|
section "TEST 3: Boot with No Schedules"
|
||||||
|
|
||||||
|
echo "Purpose: Verify boot recovery handles empty database gracefully."
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 1: Uninstall app to clear DB/state"
|
||||||
|
set +e
|
||||||
|
$ADB_BIN uninstall "$APP_ID" >/dev/null 2>&1
|
||||||
|
set -e
|
||||||
|
ok "App uninstalled (state cleared)"
|
||||||
|
|
||||||
|
substep "Step 2: Reinstall app"
|
||||||
|
if $ADB_BIN install -r "$APK_PATH"; then
|
||||||
|
ok "App installed"
|
||||||
|
else
|
||||||
|
error "Reinstall failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Clearing logcat..."
|
||||||
|
$ADB_BIN logcat -c
|
||||||
|
ok "Logs cleared"
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 3: Reboot emulator WITHOUT scheduling anything"
|
||||||
|
warn "Do NOT schedule any notifications. The app should have no schedules in the database."
|
||||||
|
warn "The emulator will reboot now. This will take 30-60 seconds."
|
||||||
|
pause
|
||||||
|
reboot_emulator
|
||||||
|
|
||||||
|
substep "Step 4: Collect boot recovery logs"
|
||||||
|
info "Collecting recovery logs from boot..."
|
||||||
|
sleep 2
|
||||||
|
local logs
|
||||||
|
logs="$(get_recovery_logs)"
|
||||||
|
echo "$logs"
|
||||||
|
|
||||||
|
local scenario rescheduled missed
|
||||||
|
scenario="$(extract_scenario_from_logs "$logs")"
|
||||||
|
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||||
|
missed="$(extract_field_from_logs "$logs" "missed")"
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Parsed recovery summary:"
|
||||||
|
echo " scenario = ${scenario:-<none>}"
|
||||||
|
echo " rescheduled= ${rescheduled}"
|
||||||
|
echo " missed = ${missed}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ -z "$logs" ]]; then
|
||||||
|
ok "TEST 3 PASSED: No recovery logs when there are no schedules (safe behavior)."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$logs" | grep -qiE "No schedules found|No schedules present"; then
|
||||||
|
ok "TEST 3 PASSED: Explicit 'No schedules found' message logged with no rescheduling."
|
||||||
|
elif [[ "$scenario" == "$NONE_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
|
||||||
|
ok "TEST 3 PASSED: NONE scenario detected with no rescheduling."
|
||||||
|
elif [[ "$rescheduled" -gt 0 ]]; then
|
||||||
|
warn "TEST 3: rescheduled>0 on first launch / empty DB. Check that boot recovery isn't misfiring."
|
||||||
|
else
|
||||||
|
info "TEST 3: Logs present but no rescheduling; review scenario handling to ensure it's explicit about NONE / NO_SCHEDULES."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# TEST 4 – Silent Boot Recovery (App Never Opened)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test4_silent_boot_recovery() {
|
||||||
|
section "TEST 4: Silent Boot Recovery (App Never Opened)"
|
||||||
|
|
||||||
|
echo "Purpose: Verify boot recovery occurs even when the app is never opened after reboot."
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 1: Launch app & ensure plugin configured"
|
||||||
|
launch_app
|
||||||
|
|
||||||
|
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf needed, click 'Configure Plugin', then press Enter."
|
||||||
|
|
||||||
|
ui_prompt "Click 'Test Notification' to schedule a notification for a few minutes in the future."
|
||||||
|
|
||||||
|
substep "Step 2: Verify alarms are scheduled"
|
||||||
|
show_alarms
|
||||||
|
local before_count system_before
|
||||||
|
before_count="$(get_plugin_alarm_count)"
|
||||||
|
system_before="$(get_system_alarm_count)"
|
||||||
|
info "Plugin alarms before reboot: $before_count (expected: 1)"
|
||||||
|
info "System/other alarms: $system_before (for context)"
|
||||||
|
|
||||||
|
if [[ "$before_count" -eq 0 ]]; then
|
||||||
|
warn "No plugin alarms found; TEST 4 may not be meaningful."
|
||||||
|
elif [[ "$before_count" -eq 1 ]]; then
|
||||||
|
ok "Single plugin alarm confirmed (one per day)"
|
||||||
|
else
|
||||||
|
warn "Found $before_count plugin alarms (expected: 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
substep "Step 3: Reboot emulator (DO NOT open app after reboot)"
|
||||||
|
warn "IMPORTANT: After reboot, DO NOT open the app. Boot recovery should run silently."
|
||||||
|
warn "The emulator will reboot now. This will take 30-60 seconds."
|
||||||
|
pause
|
||||||
|
reboot_emulator
|
||||||
|
|
||||||
|
substep "Step 4: Collect boot recovery logs (without opening app)"
|
||||||
|
info "Collecting recovery logs from boot (app was NOT opened)..."
|
||||||
|
sleep 2
|
||||||
|
local logs
|
||||||
|
logs="$(get_recovery_logs)"
|
||||||
|
echo "$logs"
|
||||||
|
|
||||||
|
local missed rescheduled verified errors scenario
|
||||||
|
missed="$(extract_field_from_logs "$logs" "missed")"
|
||||||
|
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||||
|
verified="$(extract_field_from_logs "$logs" "verified")"
|
||||||
|
errors="$(extract_field_from_logs "$logs" "errors")"
|
||||||
|
scenario="$(extract_scenario_from_logs "$logs")"
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Parsed recovery summary:"
|
||||||
|
echo " scenario = ${scenario:-<none>}"
|
||||||
|
echo " missed = ${missed}"
|
||||||
|
echo " rescheduled= ${rescheduled}"
|
||||||
|
echo " verified = ${verified}"
|
||||||
|
echo " errors = ${errors}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
substep "Step 5: Verify alarms were recreated (without opening app)"
|
||||||
|
show_alarms
|
||||||
|
local after_count system_after
|
||||||
|
after_count="$(get_plugin_alarm_count)"
|
||||||
|
system_after="$(get_system_alarm_count)"
|
||||||
|
info "Plugin alarms after boot (app never opened): $after_count (expected: 1)"
|
||||||
|
info "System/other alarms: $system_after (for context)"
|
||||||
|
|
||||||
|
if [[ "$after_count" -gt 0 && "$rescheduled" -gt 0 ]]; then
|
||||||
|
ok "TEST 4 PASSED: Boot recovery occurred silently and alarms were recreated (rescheduled=$rescheduled) without app launch."
|
||||||
|
elif [[ "$rescheduled" -gt 0 ]]; then
|
||||||
|
ok "TEST 4 PASSED: Boot recovery occurred silently (rescheduled=$rescheduled), but alarm count check unclear."
|
||||||
|
elif echo "$logs" | grep -qi "Starting boot recovery\|boot recovery"; then
|
||||||
|
warn "TEST 4: Boot recovery ran but alarms may not have been recreated. Check logs and implementation."
|
||||||
|
else
|
||||||
|
warn "TEST 4: Boot recovery not detected. Verify boot receiver is registered and has BOOT_COMPLETED permission."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Allow selecting specific tests: e.g. `./test-phase3.sh 1 3`
|
||||||
|
if [[ "$#" -gt 0 && ( "$1" == "-h" || "$1" == "--help" ) ]]; then
|
||||||
|
echo "Usage: $0 [TEST_IDS...]"
|
||||||
|
echo
|
||||||
|
echo "If no TEST_IDS are given, all tests (1, 2, 3, 4) will run."
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 # run all tests"
|
||||||
|
echo " $0 1 # run only TEST 1"
|
||||||
|
echo " $0 2 3 # run only TEST 2 and TEST 3"
|
||||||
|
echo " $0 4 # run only TEST 4 (silent boot recovery)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SELECTED_TESTS=("$@")
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "========================================"
|
||||||
|
echo "Phase 3 Testing Script – Boot Recovery"
|
||||||
|
echo "========================================"
|
||||||
|
echo
|
||||||
|
echo "This script will guide you through Phase 3 tests."
|
||||||
|
echo "You'll be prompted when UI interaction is needed."
|
||||||
|
echo
|
||||||
|
echo "⚠️ WARNING: This script will reboot the emulator multiple times."
|
||||||
|
echo " Each reboot takes 30-60 seconds."
|
||||||
|
echo
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
||||||
|
require_adb_device
|
||||||
|
build_app
|
||||||
|
install_app
|
||||||
|
|
||||||
|
if should_run_test "1" SELECTED_TESTS; then
|
||||||
|
test1_boot_future_alarms
|
||||||
|
pause
|
||||||
|
fi
|
||||||
|
|
||||||
|
if should_run_test "2" SELECTED_TESTS; then
|
||||||
|
test2_boot_past_alarms
|
||||||
|
pause
|
||||||
|
fi
|
||||||
|
|
||||||
|
if should_run_test "3" SELECTED_TESTS; then
|
||||||
|
test3_boot_no_schedules
|
||||||
|
pause
|
||||||
|
fi
|
||||||
|
|
||||||
|
if should_run_test "4" SELECTED_TESTS; then
|
||||||
|
test4_silent_boot_recovery
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Testing Complete"
|
||||||
|
|
||||||
|
echo "Test Results Summary (see logs above for details):"
|
||||||
|
echo
|
||||||
|
echo "TEST 1: Boot with Future Alarms"
|
||||||
|
echo " - Check logs for scenario=$BOOT_SCENARIO_VALUE and rescheduled>0"
|
||||||
|
echo
|
||||||
|
echo "TEST 2: Boot with Past Alarms"
|
||||||
|
echo " - Check that missed>=1 and rescheduled>=1"
|
||||||
|
echo
|
||||||
|
echo "TEST 3: Boot with No Schedules"
|
||||||
|
echo " - Check that no recovery runs, or NONE scenario is logged with rescheduled=0"
|
||||||
|
echo
|
||||||
|
echo "TEST 4: Silent Boot Recovery"
|
||||||
|
echo " - Check that boot recovery occurred and alarms were recreated without app launch"
|
||||||
|
echo
|
||||||
|
|
||||||
|
ok "Phase 3 testing script complete!"
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " - Review logs above"
|
||||||
|
echo " - Capture snippets into PHASE3-EMULATOR-TESTING.md"
|
||||||
|
echo " - Update PHASE3-VERIFICATION.md and unified directive status matrix"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -103,3 +103,91 @@ npm run build
|
|||||||
```sh
|
```sh
|
||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## ADB Commands for Android Testing
|
||||||
|
|
||||||
|
**Package Name**: `com.timesafari.dailynotification.test`
|
||||||
|
|
||||||
|
### Check Device Connection
|
||||||
|
|
||||||
|
```sh
|
||||||
|
adb devices
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install App
|
||||||
|
|
||||||
|
Install the debug APK to a connected device or emulator:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# From project root
|
||||||
|
cd android
|
||||||
|
./gradlew installDebug
|
||||||
|
|
||||||
|
# Or from test-apps/daily-notification-test directory
|
||||||
|
cd android && ./gradlew installDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Installed Packages
|
||||||
|
|
||||||
|
Check if the app is installed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# List all packages (filter for this app)
|
||||||
|
adb shell pm list packages | grep timesafari
|
||||||
|
|
||||||
|
# List only this app's package
|
||||||
|
adb shell pm list packages com.timesafari.dailynotification.test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Launch/Raise the App
|
||||||
|
|
||||||
|
Launch the app or bring it to foreground:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Launch the main activity
|
||||||
|
adb shell am start -n com.timesafari.dailynotification.test/.MainActivity
|
||||||
|
|
||||||
|
# Launch with explicit intent
|
||||||
|
adb shell am start -a android.intent.action.MAIN -n com.timesafari.dailynotification.test/.MainActivity
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uninstall App
|
||||||
|
|
||||||
|
Remove the app from the device:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Uninstall by package name
|
||||||
|
adb uninstall com.timesafari.dailynotification.test
|
||||||
|
|
||||||
|
# Force uninstall (if regular uninstall fails)
|
||||||
|
adb shell pm uninstall -k --user 0 com.timesafari.dailynotification.test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Useful ADB Commands
|
||||||
|
|
||||||
|
**View App Logs**:
|
||||||
|
```sh
|
||||||
|
# Filter logs for this app
|
||||||
|
adb logcat | grep -E "timesafari|DailyNotification|DNP"
|
||||||
|
|
||||||
|
# Clear logs before testing
|
||||||
|
adb logcat -c
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check App Info**:
|
||||||
|
```sh
|
||||||
|
# Get app version and info
|
||||||
|
adb shell dumpsys package com.timesafari.dailynotification.test | grep -A 5 "versionName\|versionCode"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Force Stop App**:
|
||||||
|
```sh
|
||||||
|
# Force stop the app (useful for testing recovery scenarios)
|
||||||
|
adb shell am force-stop com.timesafari.dailynotification.test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Clear App Data**:
|
||||||
|
```sh
|
||||||
|
# Clear app data (resets to fresh install state)
|
||||||
|
adb shell pm clear com.timesafari.dailynotification.test
|
||||||
|
```
|
||||||
|
|||||||
14
test-apps/daily-notification-test/package-lock.json
generated
14
test-apps/daily-notification-test/package-lock.json
generated
@@ -117,7 +117,6 @@
|
|||||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
@@ -417,7 +416,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
||||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.28.4"
|
"@babel/types": "^7.28.4"
|
||||||
},
|
},
|
||||||
@@ -636,7 +634,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
|
||||||
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
|
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -2068,7 +2065,6 @@
|
|||||||
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.1",
|
"@typescript-eslint/scope-manager": "8.46.1",
|
||||||
"@typescript-eslint/types": "8.46.1",
|
"@typescript-eslint/types": "8.46.1",
|
||||||
@@ -2667,7 +2663,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2916,7 +2911,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.9",
|
"baseline-browser-mapping": "^2.8.9",
|
||||||
"caniuse-lite": "^1.0.30001746",
|
"caniuse-lite": "^1.0.30001746",
|
||||||
@@ -3379,7 +3373,6 @@
|
|||||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3441,7 +3434,6 @@
|
|||||||
"integrity": "sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==",
|
"integrity": "sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@@ -4301,7 +4293,6 @@
|
|||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
@@ -5730,7 +5721,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5808,7 +5798,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6042,7 +6031,6 @@
|
|||||||
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -6313,7 +6301,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -6333,7 +6320,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
||||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.22",
|
"@vue/compiler-dom": "3.5.22",
|
||||||
"@vue/compiler-sfc": "3.5.22",
|
"@vue/compiler-sfc": "3.5.22",
|
||||||
|
|||||||
Reference in New Issue
Block a user