Implements automated testing for TEST 4 (Invalid Data Handling) to verify
recovery gracefully handles invalid database entries without crashing.
Changes:
- Add injectInvalidTestData plugin method for injecting invalid test data
(empty schedule IDs, null nextRunAt, empty notification IDs)
- Make test app debuggable to enable direct database access
- Enhance test-phase1.sh with automated database injection and verification:
* Detect debuggable app status (check for DEBUGGABLE flag)
* Inject invalid data via direct SQL (schedules and notifications)
* Handle WAL mode with checkpoint
* Verify data injection success
* Trigger recovery and check logs for "Skipping invalid" messages
* Report pass/fail/inconclusive results
Fixes database constraint issues discovered during testing:
- Include jitterMs and backoffPolicy in schedule inserts
- Include priority, vibration_enabled, sound_enabled in notification inserts
Test results: ✅ PASSED
- Invalid data successfully injected
- Cold start recovery correctly skips invalid entries
- Recovery completes without crashing
- Boot recovery processes invalid data (follow-up improvement needed)
This enables automated verification that recovery handles corrupted or
invalid database entries gracefully, preventing crashes in production.
Fix duplicate alarm bug where updating schedule time created multiple
schedules in database, violating "one notification per day" contract.
Plugin Changes:
- Use stable scheduleId "daily_notification" instead of timestamp-based IDs
- Delete all existing notification schedules before creating new one
- Cancel alarms in AlarmManager before database deletion
- Add detailed logging for cleanup operations
- Make scheduleDailyReminder delegate to scheduleDailyNotification
Test Harness Changes:
- Make TEST 2 fail when alarm count > 1 after schedule update
- Make TEST 2 fail when alarm count > 1 after recovery
- Add clear failure messages explaining "one per day" violation
- Add final verdict section with detailed failure summary
Results:
- Before: 2-3 alarms, 2 schedules in DB, "Pending: 2" in UI
- After: 1 alarm, 1 schedule in DB, "Pending: 1" in UI
- TEST 2 now correctly passes with proper validation
This ensures that updating schedule time maintains exactly one alarm
per day, preventing duplicate notifications and database bloat.
Add automatic app state reset for TEST 1 to ensure clean starting state when
lingering alarms from TEST 0 are detected. Create PHASE1_TEST1_GOLDEN.md with
actual values from successful run.
TEST 1 Auto-Reset:
- Detect lingering plugin alarms before TEST 1 starts
- Automatically uninstall/reinstall app to clear alarms
- Verify clean state (0 alarms) before proceeding
- Gracefully skip TEST 1 if clean state cannot be achieved
- Take failure screenshots when reset fails
- Wrap all TEST 1 steps in conditional to skip on reset failure
Documentation:
- Create PHASE1_TEST1_GOLDEN.md with actual values from passing run
- Document auto-reset behavior in golden run steps
- Add cross-references between TEST 0 and TEST 1 golden docs
- Include actual timestamps, scheduleIds, and recovery metrics
This ensures TEST 1 always starts from a known clean state, making test
results reliable and reproducible. The golden doc serves as a baseline for
comparing future TEST 1 runs.
Fix alarm counting to correctly parse dumpsys output where app ID and
action appear on different lines. Add screenshot capture for test
diagnostics and create golden run documentation.
Test Harness Improvements:
- Fix get_plugin_alarm_count() to track app ID and action separately
across alarm block lines (fixes false 0-count bug)
- Add show_plugin_alarms_compact() to display complete alarm blocks
- Add wait_for_stable_plugin_alarm_count() polling helper to reduce
race condition false negatives
- Add take_screenshot() and take_failure_screenshot() helpers for
automatic test state capture
- Integrate screenshots into TEST 0 at key checkpoints
- Update TEST 0 messaging to handle race conditions gracefully
- Add screenshots/ to .gitignore
Documentation:
- Create PHASE1_TEST0_GOLDEN.md with actual values from successful run
- Document expected script output, UI state, dumpsys shape, and logcat
patterns
- Include pass/fail checklist for future test runs
This fixes the issue where alarm counting always returned 0 because the
AWK logic required app ID and action on the same line, but dumpsys
output has them on separate lines (header line has app ID, tag line
has action).
Centralize all notification alarm scheduling through NotifyReceiver.scheduleExactNotification()
with idempotence checks to prevent duplicate alarms. Implement one-alarm policy using
setAlarmClock() only. Fix test harness alarm counting to deduplicate by Alarm handle.
Plugin Changes:
- Add ScheduleSource enum to track scheduling paths (INITIAL_SETUP, ROLLOVER_ON_FIRE, etc.)
- Add DB-level idempotence check before scheduling (prevents logical duplicates)
- Add explicit alarm cancellation before scheduling (safety net)
- Implement one-alarm policy: use setAlarmClock() only, no setExact* fallbacks for same event
- Add deep logging for all AlarmManager calls (variant, requestCode, pendingIntentHash)
- Update all rollover paths (DailyNotificationReceiver, DailyNotificationWorker) to use
centralized function with ROLLOVER_ON_FIRE source
- Add @JvmStatic annotation to scheduleExactNotification for Java interop
Test Harness Changes:
- Fix get_plugin_alarm_count() to deduplicate by Alarm handle (prevents double-counting
same alarm in main list and "Next wake from idle" section)
- Update TEST 0 messaging: treat 0 alarms as race condition (inconclusive, not failure)
- Make post-rollover check the authoritative assertion point (only fails on >1 or 0 alarms)
- Remove redundant "Found 0 alarms - test may not be accurate" messages
This fixes the duplicate alarm bug where two distinct AlarmManager entries were created
for the same daily notification, violating the "one notification per day" contract.
Remove the 'List Alarms' button and alarm list display functionality
from the Android test app UI. This feature was added for testing but
is no longer needed as alarm verification is handled by the test scripts.
Removed:
- '📋 List Alarms' button
- alarmListContainer div and alarm list display
- loadAlarmList() JavaScript function
- getSchedulesWithStatus() API call usage
The getSchedulesWithStatus() plugin method remains available for
programmatic use if needed in the future.
Fix ReactivationManager to use correct DAO method names and entity
constructor for NotificationContentEntity:
- Change getById() to getNotificationById()
- Change insert() to insertNotification()
- Update NotificationContentEntity construction to use Java class
constructor with positional parameters
- Set entity fields using Java property syntax after construction
This fixes compilation errors introduced when switching to Java-based
NotificationContentEntity class.
Extract common helper functions from test-phase1.sh, test-phase2.sh,
and test-phase3.sh into a shared library (alarm-test-lib.sh) to reduce
code duplication and improve maintainability.
Changes:
- Create alarm-test-lib.sh with shared configuration, UI helpers, ADB
helpers, log parsing, and test selection logic
- Refactor all three phase test scripts to source the shared library
- Remove ~200 lines of duplicated code across the three scripts
- Preserve all existing behavior, CLI arguments, and test semantics
- Maintain Phase 1 compatibility (print_* functions, VERIFY_FIRE flag)
- Update all adb references to use $ADB_BIN variable
- Standardize alarm counting to use shared count_alarms() function
Benefits:
- Single source of truth for shared helpers
- Easier maintenance (fix once, benefits all scripts)
- Consistent behavior across all test phases
- No functional changes to test execution or results
Adds ability to list alarms with AlarmManager status in web interface.
Changes:
- Add getSchedulesWithStatus() method to DailyNotificationPlugin
- Add ScheduleWithStatus TypeScript interface with isActuallyScheduled flag
- Add alarm list UI to android-test-app with status indicators
Backend:
- getSchedulesWithStatus() returns schedules with AlarmManager verification
- Checks isAlarmScheduled() for each notify schedule with nextRunAt
- Returns isActuallyScheduled boolean flag for each schedule
TypeScript:
- New ScheduleWithStatus interface extending Schedule
- Method signature with JSDoc and usage examples
UI:
- New "📋 List Alarms" button in test app
- Color-coded alarm cards (green=scheduled, orange=not scheduled)
- Shows schedule ID, next run time, pattern, and AlarmManager status
- Useful for debugging recovery scenarios and verifying alarm state
Use case:
- Verify which alarms are in database vs actually scheduled
- Debug Phase 1/2/3 recovery scenarios
- Visual confirmation of alarm state after app launch/boot
Related:
- Enhances: android-test-app for Phase 1-3 testing
- Supports: Recovery verification and debugging
Implements boot-time recovery that restores alarms after device reboot,
matching Phase 3 directive and test suite expectations.
Changes:
- Add ReactivationManager.runBootRecovery() companion method
- Update BootReceiver to delegate to ReactivationManager
- Handle past alarms: mark as missed, schedule next occurrence
- Handle future alarms: reschedule immediately
- Use DNP-REACTIVATION tag for all boot recovery logs
- Log summary: "Boot recovery complete: missed=X, rescheduled=Y, verified=0, errors=Z"
- Record history with scenario=BOOT
Recovery behavior:
- Loads all schedules from database on boot
- Detects past vs future scheduled times
- Marks missed notifications for past alarms
- Reschedules all alarms (past and future)
- Completes within 2-second timeout (non-blocking)
- Handles empty DB gracefully (logs "BOOT: No schedules found")
Implementation details:
- Uses companion object method for static access from BootReceiver
- Helper methods for schedule calculation, missed marking, rescheduling
- Error handling: non-fatal, continues processing other schedules
- History recording with boot_recovery kind and scenario=BOOT
Related:
- Implements: android-implementation-directive-phase3.md
- Requirements: docs/alarms/03-plugin-requirements.md §3.1.1
- Testing: docs/alarms/PHASE3-EMULATOR-TESTING.md
- Verification: docs/alarms/PHASE3-VERIFICATION.md
Adds documentation and test harness for Phase 2 (Force Stop Detection & Recovery).
Changes:
- Add PHASE2-EMULATOR-TESTING.md with detailed test procedures
- Add PHASE2-VERIFICATION.md with test matrix and verification template
- Add test-phase2.sh automated test harness
Test harness features:
- 3 test cases: force stop with cleared alarms, intact alarms, empty DB
- Automatic force stop simulation via adb
- Log parsing for scenario detection and recovery results
- UI prompts for plugin configuration and scheduling
Related:
- Directive: android-implementation-directive-phase2.md
- Requirements: docs/alarms/03-plugin-requirements.md §3.1.4
- Testing: docs/alarms/PHASE2-EMULATOR-TESTING.md
- Verification: docs/alarms/PHASE2-VERIFICATION.md
Remove duplicate status matrix from Section 3.3 and consolidate to Section 11
as single source of truth. Fix all section number references throughout
documentation.
Changes:
- Remove duplicate status matrix table from Section 3.3
- Update all references from "Section 3.3" and "Section 10" to "Section 11"
- Fix phase directive paths to use consistent ../ prefix format
- Fix P1 path typo (missing "directive" in filename)
- Update Doc C status in matrix to reflect completion
- Remove duplicate text in Doc B baseline scenarios
- Remove self-referencing links in Doc B
All status matrix references now point to Section 11, eliminating confusion
about which matrix is authoritative.
Create unified alarm documentation system with strict role separation:
- Doc A: Platform capability reference (canonical OS facts)
- Doc B: Plugin behavior exploration (executable test harness)
- Doc C: Plugin requirements (guarantees, JS/TS contract, traceability)
Changes:
- Add canonical rule to Doc A preventing platform fact duplication
- Convert Doc B to pure executable test spec with scenario tables
- Complete Doc C with guarantees matrix, JS/TS API contract, recovery
contract, unsupported behaviors, and traceability matrix
- Remove implementation details from unified directive
- Add compliance milestone tracking and iOS parity gates
- Add deprecation banners to legacy platform docs
All documents now enforce strict role separation with cross-references
to prevent duplication and ensure single source of truth.
- Add visual indicator when notification is received (shows for 30s)
- Poll notification status every 5 seconds to detect recent deliveries
- Add helpful message in test notification success about checking top of screen
- Improve user feedback for notification testing workflow
Fix configure() method to read parameters directly from CAPPluginCall
instead of expecting nested options object, matching Android implementation.
Improve build process to ensure canonical UI is always copied:
- iOS build script: Copy www/index.html to test app before build
- Android build.gradle: Add copyCanonicalUI task to run before build
- Ensures test apps always use latest UI from www/index.html
This fixes the issue where configure() was returning 'Configuration
options required' error because it expected a nested options object
when Capacitor passes parameters directly on the call object.
Implement UNUserNotificationCenterDelegate in AppDelegate to display
notifications when app is in foreground. Add visual feedback indicator
in test app UI to confirm notification delivery.
Changes:
- AppDelegate: Conform to UNUserNotificationCenterDelegate protocol
- AppDelegate: Implement willPresent and didReceive delegate methods
- AppDelegate: Set delegate at multiple lifecycle points to ensure
it's always active (immediate, after Capacitor init, on app active)
- UI: Add notification received indicator in status card
- UI: Add periodic check for notification delivery (every 5 seconds)
- UI: Add instructions on where to look for notification banner
- Docs: Add IOS_LOGGING_GUIDE.md for debugging iOS logs
This fixes the issue where scheduled notifications were not visible
when the app was in the foreground. The delegate method now properly
presents notifications with banner, sound, and badge options.
Verified working: Logs show delegate method called successfully when
notification fires, with proper presentation options set.
Fixed iOS 13.0 compatibility issue in test harness by replacing Logger
(iOS 14+) with os_log (iOS 13+). Fixed build script to correctly detect
and sync Capacitor config from App subdirectory. Unified both Android
and iOS test app UIs to use www/index.html as the canonical source.
Changes:
- DailyNotificationBackgroundTaskTestHarness: Replace Logger with os_log
for iOS 13.0 deployment target compatibility
- build-ios-test-app.sh: Fix Capacitor sync path detection to check
both current directory and App/ subdirectory for config files
- test-apps: Update both Android and iOS test apps to use www/index.html
as the canonical UI source for consistency
This ensures the plugin builds on iOS 13.0+ and both test apps provide
the same testing experience across platforms.
- Fix isChannelEnabled() to create channel if missing and re-fetch from system
to get actual state (handles previously blocked channels)
- Use correct channel ID 'timesafari.daily' instead of 'daily_notification_channel'
- Add detailed logging for channel status checks
- Fix UI to refresh channel status after notification permissions are granted
- Channel status now correctly reflects both app-level and channel-level settings
Static reminders scheduled via scheduleDailyNotification() with
isStaticReminder=true were being skipped because they don't have content
in storage - title/body are in Intent extras. Fixed by:
- DailyNotificationReceiver: Extract static reminder extras from Intent
and pass to WorkManager as input data
- DailyNotificationWorker: Check for static reminder flag in input data
and create NotificationContent from input data instead of loading from
storage
- DailyNotificationWorker: Ensure notification channel exists before
displaying (fixes "No Channel found" errors)
Also updated prefetch timing from 5 minutes to 2 minutes before notification
time in plugin code and web UI.
Fix documentation to address Xcode behavior where 'Simulate Background
Fetch' menu item only appears when app is NOT running.
Changes:
- Add explicit note about Xcode menu item availability
- Prioritize LLDB command method when app is running (recommended)
- Document three methods: LLDB command, Xcode menu, and UI button
- Add troubleshooting section for common issues
- Update quick start section to reference LLDB method
- Explicitly reference test-apps/ios-test-app path for clarity
This resolves confusion when 'Simulate Background Fetch' disappears
from Debug menu while app is running. LLDB command method works reliably
in all scenarios.
Add iOS prefetch testing guide with detailed procedures, log checklists,
and behavior classification. Enhance iOS test app requirements with
security constraints, sign-off checklists, and changelog structure.
Update main directive with testing strategy and method behavior mapping.
Changes:
- Add IOS_PREFETCH_TESTING.md with simulator/device test plans, log
diagnostics, telemetry expectations, and test run templates
- Add DailyNotificationBackgroundTaskTestHarness.swift as reference
implementation for BGTaskScheduler testing
- Enhance IOS_TEST_APP_REQUIREMENTS.md with security/privacy constraints,
review checklists, CI hints, and glossary cross-links
- Update 0003-iOS-Android-Parity-Directive.md with testing strategy
section, method behavior classification, and validation matrix updates
All documents now include changelog stubs, cross-references, and
completion criteria for Phase 1 implementation and testing.
Fixed scheduleDailyNotification to read parameters directly from CAPPluginCall
(matching Android pattern) instead of looking for wrapped "options" object.
Improved BGTaskScheduler error handling to clearly indicate simulator limitations.
Changes:
- Read parameters directly from call (call.getString("time"), etc.) instead of
call.getObject("options") - Capacitor passes options object directly as call data
- Improved BGTaskScheduler error handling with clear simulator limitation message
- Added priority parameter extraction (was missing)
- Error handling doesn't fail notification scheduling if background fetch fails
BGTaskScheduler Simulator Limitation:
- BGTaskSchedulerErrorDomain Code=1 (notPermitted) is expected on simulator
- Background fetch scheduling fails on simulator but works on real devices
- Notification scheduling still works correctly; prefetch won't run on simulator
- Error messages now clearly indicate this is expected behavior
Result: scheduleDailyNotification now works correctly. Notification scheduling
verified working on simulator. Background fetch error is expected and documented.
Files modified:
- ios/Plugin/DailyNotificationPlugin.swift: Parameter reading fix, error handling
- doc/directives/0003-iOS-Android-Parity-Directive.md: Implementation details documented
Capacitor iOS was not discovering DailyNotificationPlugin because it did not
conform to the CAPBridgedPlugin protocol required for runtime discovery.
Changes:
- Add @objc extension to DailyNotificationPlugin implementing CAPBridgedPlugin
with identifier, jsName, and pluginMethods properties
- Force-load plugin framework in AppDelegate before Capacitor initializes
- Remove duplicate BGTaskScheduler registration from AppDelegate (plugin handles it)
- Update podspec to use dynamic framework (static_framework = false)
- Add diagnostic logging to verify plugin discovery
Result: Plugin is now discovered by Capacitor and all methods are accessible
from JavaScript. Verified working with checkPermissionStatus() method.
Files modified:
- ios/Plugin/DailyNotificationPlugin.swift: Added CAPBridgedPlugin extension
- test-apps/ios-test-app/ios/App/App/AppDelegate.swift: Force-load + diagnostics
- ios/DailyNotificationPlugin.podspec: Dynamic framework setting
- doc/directives/0003-iOS-Android-Parity-Directive.md: Documented solution
Fix three critical issues in the Android notification system:
1. configureNativeFetcher() now actually calls nativeFetcher.configure() method
- Previously only stored config in database without configuring fetcher instance
- Added synchronous configure() call with proper error handling
- Stores valid but empty config entry if configure() fails to prevent downstream errors
- Adds FETCHER|CONFIGURE_START and FETCHER|CONFIGURE_COMPLETE instrumentation logs
2. Prefetch operations now use DailyNotificationFetchWorker instead of legacy FetchWorker
- Replaced FetchWorker.scheduleDelayedFetch() with WorkManager scheduling
- Uses correct input data format (scheduled_time, fetch_time, retry_count, immediate)
- Enables native fetcher SPI to be used for prefetch operations
- Handles both delayed and immediate prefetch scenarios
3. Notification dismiss now cancels notification from NotificationManager
- Added notification cancellation before removing from storage
- Uses notificationId.hashCode() to match display notification ID
- Ensures notification disappears immediately when dismiss button is clicked
- Adds DN|DISMISS_CANCEL_NOTIF instrumentation log
Version bump: 1.0.8 → 1.0.11
Fix issue where prefetch worker saved content to ContentCache but didn't
create NotificationContentEntity, causing notification worker to skip
notifications with "content_not_found" error.
Changes:
- Extract notificationTime from input data in doWork()
- Create NotificationContentEntity with matching notification_id when
notificationTime > 0 (prefetch operations)
- Add parsePayload() helper to extract title/body from JSON or plain text
- Save entity to Room database so notification worker can find it
The notification_id format matches NotifyReceiver.kt: "notify_${notificationTime}",
ensuring the notification worker can retrieve content when the alarm fires.
Fixes issue where alarms triggered correctly but notifications were skipped
because DailyNotificationWorker couldn't find content in storage.
Fix reflection-based permission check that was failing with NoSuchMethodException.
Add multiple fallback strategies to ensure permission check works reliably.
Changes:
- Add getDeclaredMethod() fallback when getMethod() fails
- Add heuristic fallback: if exact alarms not allowed, assume they can be requested
- Improve error handling: catch NoSuchMethodException separately from other exceptions
- Add debug logging to track which reflection path is taken
- Change reflection failure log level from ERROR to WARNING (we have fallback)
The heuristic fallback is safe because:
- If exact alarms are not currently allowed, we should try to request them
- Only edge case is permanently denied (rare), worst case is unnecessary Settings redirect
- Better than failing silently or blocking permission requests
Fixes reflection failures seen in logcat where Settings.canRequestScheduleExactAlarms()
method lookup was failing, causing unnecessary Settings redirects.
Add comprehensive exact alarm permission handling for Android 12+ (API 31+)
and fix critical bugs preventing notifications from triggering.
Features:
- Add checkExactAlarmPermission() and requestExactAlarmPermission() plugin methods
- Add canScheduleExactAlarms() and canRequestExactAlarmPermission() helper methods
- Update all scheduling methods to check/request permission before scheduling
- Use reflection for canRequestScheduleExactAlarms() to avoid compilation issues
Bug Fixes:
- Fix receiver mismatch: change alarm intents from NotifyReceiver to DailyNotificationReceiver
- Fix coroutine compilation error: wrap getLatest() suspend call in runBlocking
- Store notification content in database before scheduling alarms
- Update intent action to match manifest registration
The permission request flow opens Settings intent when SCHEDULE_EXACT_ALARM
permission is not granted, providing clear user guidance. All scheduling
methods now check permission status and request it if needed before proceeding.
Version bumped to 1.0.8
- Add cancelAllNotifications() method to DailyNotificationPlugin
- Cancels all AlarmManager alarms (exact and inexact)
- Cancels all WorkManager prefetch/fetch jobs by tag
- Clears notification schedules from database (sets enabled=false)
- Idempotent - safe to call multiple times
- Implementation details:
- Reads scheduled notifications from database
- Uses NotifyReceiver.cancelNotification() for each scheduled alarm
- Includes fallback cleanup for orphaned alarms
- Cancels WorkManager jobs with tags: prefetch, daily_notification_fetch,
daily_notification_maintenance, soft_refetch, daily_notification_display,
daily_notification_dismiss
- Disables all notification and fetch schedules in database
- Add required imports:
- android.app.PendingIntent for alarm cancellation
- androidx.work.WorkManager for job cancellation
- Error handling:
- Gracefully handles missing alarms/jobs (logs warnings, doesn't fail)
- Continues cleanup even if individual operations fail
- Comprehensive logging for debugging
Fixes:
- 'not implemented' error when host app calls cancelAllNotifications()
- Enables users to update notification time without errors
- Allows users to disable notifications completely
- Prevents orphaned alarms and jobs after cancellation
The method matches TypeScript interface and is ready for use.
- Fix MainActivity ClassNotFoundException by using dynamic package launcher intent
- Replace hardcoded MainActivity class references with getLaunchIntent() helper
- Uses packageManager.getLaunchIntentForPackage() to work with any host app
- Removes dependency on specific MainActivity package/class name
- Fixes 3 occurrences in NotifyReceiver.kt (alarm clock, notification click, reminder click)
- Add exact alarm permission check before scheduling (Android 12+)
- Add canScheduleExactAlarms() helper to check SCHEDULE_EXACT_ALARM permission
- Check permission before scheduling exact alarms in scheduleExactNotification()
- Gracefully fall back to inexact alarms when permission not granted
- Prevents SecurityException and provides clear logging
- Bump version to 1.0.2
Fixes:
- ClassNotFoundException when plugin tries to resolve hardcoded MainActivity path
- SecurityException on Android 12+ when exact alarm permission not granted
- Plugin now works with any host app regardless of MainActivity package/class
All changes maintain backward compatibility and improve reliability.
The Vue test app was missing the NotifyReceiver registration in
AndroidManifest.xml, preventing alarm broadcasts from being delivered
to the BroadcastReceiver. This caused notifications scheduled via
setAlarmClock() to fire but not display.
Added NotifyReceiver registration matching the working android-test-app
configuration. Also includes supporting improvements:
- Enhanced alarm scheduling with setAlarmClock() for Doze exemption
- Unique request codes based on trigger time to prevent PendingIntent conflicts
- Diagnostic methods (isAlarmScheduled, getNextAlarmTime, testAlarm)
- TypeScript definitions for new methods
Verified: Notification successfully fired at 09:41:00 and was displayed.