Commit Graph

15 Commits

Author SHA1 Message Date
Matthew Raymer
5bdb6979e1 fix(android): enforce one-per-day semantics in scheduleDailyNotification
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.
2025-12-08 06:36:16 +00:00
Matthew Raymer
fc2f64bae3 fix(notify): eliminate duplicate alarm scheduling and fix test harness counting
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.
2025-12-01 10:09:54 +00:00
Matthew Raymer
73301f7d1d feat(android): add getSchedulesWithStatus() and alarm list UI
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
2025-11-28 04:56:19 +00:00
Matthew Raymer
3151a1cc31 feat(android): implement Phase 1 cold start recovery
Implements cold start recovery for missed notifications and future alarm
verification/rescheduling as specified in Phase 1 directive.

Changes:
- Add ReactivationManager.kt with cold start recovery logic
- Integrate recovery into DailyNotificationPlugin.load()
- Fix NotifyReceiver to always store NotificationContentEntity for recovery
- Add Phase 1 emulator testing guide and verification doc
- Add test-phase1.sh automated test harness

Recovery behavior:
- Detects missed notifications on app launch
- Marks missed notifications in database
- Verifies future alarms are scheduled in AlarmManager
- Reschedules missing future alarms
- Completes within 2-second timeout (non-blocking)

Test harness:
- Automated script with 4 test cases
- UI prompts for plugin configuration
- Log parsing for recovery results
- Verified on Pixel 8 API 34 emulator

Related:
- Implements: android-implementation-directive-phase1.md
- Requirements: docs/alarms/03-plugin-requirements.md §3.1.2
- Testing: docs/alarms/PHASE1-EMULATOR-TESTING.md
- Verification: docs/alarms/PHASE1-VERIFICATION.md
2025-11-27 10:01:34 +00:00
Matthew Raymer
a8039d072d fix(android): improve channel status detection and UI refresh
- 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
2025-11-18 09:50:23 +00:00
Matthew Raymer
8f20da7e8d fix(android): support static reminder notifications and ensure channel exists
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.
2025-11-18 04:02:56 +00:00
Matthew Raymer
1b34f1f34a fix(android): configure native fetcher, use DailyNotificationFetchWorker, and cancel notifications on dismiss
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
2025-11-11 08:06:59 +00:00
Matthew Raymer
3fa167cba0 fix(android): improve exact alarm permission check with fallback strategies
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.
2025-11-10 06:12:22 +00:00
Matthew Raymer
5b61f18bd7 feat(android): add exact alarm permission request flow and fix receiver mismatch
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
2025-11-10 05:51:05 +00:00
Matthew Raymer
f31bae1563 feat(android): implement cancelAllNotifications() method
- 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.
2025-11-10 04:17:45 +00:00
Matthew Raymer
a19cb2ba61 fix(test-app): register NotifyReceiver in AndroidManifest
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.
2025-11-06 09:56:32 +00:00
Matthew Raymer
1a7ac200f1 fix(android): implement missing plugin methods and permission handling
- Add handleOnResume() fallback to resolve permission requests when
  Capacitor Bridge doesn't route results (requestCode 1001)
- Implement checkPermissions() with override modifier for Capacitor
  standard PermissionStatus format
- Implement getExactAlarmStatus() to return exact alarm capability info
- Implement updateStarredPlans() to store plan IDs in SharedPreferences
- Fix requestPermissions() override to properly delegate to
  requestNotificationPermissions()
- Fix handleRequestPermissionsResult() return type to Unit

These changes ensure permission requests resolve correctly even when
Capacitor's Bridge doesn't recognize our custom request code, and
implement all missing methods called by the test application.
2025-11-06 08:29:36 +00:00
Matthew Raymer
9f8e295234 fix(android): improve notification scheduling and UX
- Fix cron parsing to correctly calculate next run time based on hour/minute
- Always schedule prefetch 5 minutes before notification (even without URL)
- Make notifications dismissable with setAutoCancel(true)
- Add click action to launch app when notification is tapped
- Conditionally require network only when URL is provided for prefetch
- Generate mock content when no URL is specified

These changes ensure notifications fire at the correct time, are
user-friendly (dismissable and clickable), and prefetch works reliably
even without a content URL.
2025-11-06 07:52:40 +00:00
Matthew Raymer
18106e5ba8 feat(android): consolidate databases and add prefetch scheduling
Consolidate Java and Kotlin database implementations into unified
schema, add delayed prefetch scheduling, and fix notification
delivery issues.

Database Consolidation:
- Merge Java DailyNotificationDatabase into Kotlin DatabaseSchema
- Add migration path from v1 to v2 unified schema
- Include all entities: ContentCache, Schedule, Callback, History,
  NotificationContentEntity, NotificationDeliveryEntity,
  NotificationConfigEntity
- Add @JvmStatic getInstance() for Java interoperability
- Update DailyNotificationWorker and DailyNotificationStorageRoom
  to use unified database

Prefetch Functionality:
- Add scheduleDelayedFetch() to FetchWorker for 5-minute prefetch
  before notifications
- Support delayed WorkManager scheduling with initialDelay
- Update scheduleDailyNotification() to optionally schedule prefetch
  when URL is provided

Notification Delivery Fixes:
- Register NotifyReceiver in AndroidManifest.xml (was missing,
  causing notifications not to fire)
- Add safe database initialization with lazy getDatabase() helper
- Prevent PluginLoadException on database init failure

Build Configuration:
- Add kotlin-android and kotlin-kapt plugins
- Configure Room annotation processor (kapt) for Kotlin
- Add Room KTX dependency for coroutines support
- Fix Gradle settings with pluginManagement blocks

Plugin Methods Added:
- checkPermissionStatus() - detailed permission status
- requestNotificationPermissions() - request POST_NOTIFICATIONS
- scheduleDailyNotification() - schedule with AlarmManager
- configureNativeFetcher() - configure native content fetcher
- Various status and configuration methods

Code Cleanup:
- Remove duplicate BootReceiver.java (keep Kotlin version)
- Remove duplicate DailyNotificationPlugin.java (keep Kotlin version)
- Remove old Java database implementation
- Add native fetcher SPI registry (@JvmStatic methods)

The unified database ensures schedule persistence across reboots
and provides a single source of truth for all plugin data.
Prefetch scheduling enables content caching before notifications
fire, improving offline-first reliability.
2025-11-06 06:28:00 +00:00
Matthew Raymer
0bb5a8d218 feat(android)!: implement Phase 2 Android core with WorkManager + AlarmManager + SQLite
- Add complete SQLite schema with Room database (content_cache, schedules, callbacks, history)
- Implement WorkManager FetchWorker with exponential backoff and network constraints
- Add AlarmManager NotifyReceiver with TTL-at-fire logic and notification delivery
- Create BootReceiver for automatic rescheduling after device reboot
- Update AndroidManifest.xml with necessary permissions and receivers
- Add Room, WorkManager, and Kotlin coroutines dependencies to build.gradle

feat(callback-registry)!: implement callback registry with circuit breaker

- Add CallbackRegistryImpl with HTTP, local, and queue callback support
- Implement circuit breaker pattern with exponential backoff retry logic
- Add CallbackEvent interface with structured event types
- Support for exactly-once delivery semantics with retry queue
- Include callback status monitoring and health checks

feat(observability)!: add comprehensive observability and health monitoring

- Implement ObservabilityManager with structured logging and event codes
- Add performance metrics tracking (fetch, notify, callback times)
- Create health status API with circuit breaker monitoring
- Include log compaction and metrics reset functionality
- Support for DNP-* event codes throughout the system

feat(web)!: enhance web implementation with new functionality

- Integrate callback registry and observability into web platform
- Add mock implementations for dual scheduling methods
- Implement performance tracking and structured logging
- Support for local callback registration and management
- Enhanced error handling and event logging

BREAKING CHANGE: New Android dependencies require Room, WorkManager, and Kotlin coroutines
2025-09-22 09:02:04 +00:00