Browse Source
			
			
			
			
				
		- Add DailyNotificationPlugin main class with Capacitor integration - Implement NotificationContent model following project directive schema - Create DailyNotificationStorage with tiered storage approach - Add DailyNotificationScheduler with exact/inexact alarm support - Implement DailyNotificationFetcher for background content retrieval - Create DailyNotificationReceiver for alarm handling - Add WorkManager workers for background tasks and maintenance - Implement prefetch → cache → schedule → display pipeline - Add comprehensive error handling and logging - Support battery optimization and adaptive schedulingpull/1/head
				 16 changed files with 3566 additions and 0 deletions
			
			
		@ -0,0 +1,242 @@ | 
				
			|||
# TimeSafari Notifications — LLM Implementation Directive (v2.0) | 
				
			|||
_Last updated: August 12, 2025_ | 
				
			|||
 | 
				
			|||
## 0) Role & Objective | 
				
			|||
**You are the implementation LLM.** Build an **offline-first daily notifications system** for Android (Kotlin) and iOS (Swift) that **prefetches -> caches -> schedules -> displays** content **without requiring network at display time**. Favor **reliability over richness**. | 
				
			|||
 | 
				
			|||
## 1) Golden Rules | 
				
			|||
1. **Follow the pipeline:** **Prefetch → Cache → Schedule → Display.** | 
				
			|||
2. **Never depend on network at display time.** All assets must be local. | 
				
			|||
3. **Design for failure.** Always have a last-known-good and an emergency fallback. | 
				
			|||
4. **Keep content scannable (<3s), single message, actionable.** | 
				
			|||
5. **Measure everything** (fetch success, delivery, engagement, stale usage). | 
				
			|||
6. **Minimize battery impact** and respect platform limitations and user settings. | 
				
			|||
7. **Ask only when needed:** if a required input is missing, use the defaults below; otherwise proceed. | 
				
			|||
 | 
				
			|||
## 2) Default Assumptions (use unless overridden) | 
				
			|||
- **Product mode:** Lightweight Daily Updates (text + emoji) with option to extend to media later. | 
				
			|||
- **Fetch size:** 1–2 KB JSON daily. | 
				
			|||
- **User schedule default:** 07:30 local time, daily. | 
				
			|||
- **Quiet hours:** None (app-level quiet hours supported but disabled by default). | 
				
			|||
- **Analytics:** Local log + pluggable uploader (no-op by default). | 
				
			|||
 | 
				
			|||
## 3) Deliverables | 
				
			|||
Produce the following artifacts: | 
				
			|||
 | 
				
			|||
### Android (Kotlin) | 
				
			|||
- `:core`: models, storage, metrics, fallback manager. | 
				
			|||
- `:data`: fetchers (WorkManager), mappers, cache policy. | 
				
			|||
- `:notify`: scheduler (AlarmManager), receiver, channels. | 
				
			|||
- App manifest entries & permissions. | 
				
			|||
- Unit tests for fallback, scheduling, metrics. | 
				
			|||
- README with battery optimization instructions (OEMs). | 
				
			|||
 | 
				
			|||
### iOS (Swift) | 
				
			|||
- `NotificationKit`: models, storage, metrics, fallback manager. | 
				
			|||
- BGTaskScheduler registration + handler. | 
				
			|||
- UNUserNotificationCenter scheduling + categories + attachments. | 
				
			|||
- Unit tests for fallback, scheduling, metrics. | 
				
			|||
- README with Background App Refresh caveats + Focus/Summary notes. | 
				
			|||
 | 
				
			|||
## 4) Permissions & Required Setup | 
				
			|||
### Android Manifest | 
				
			|||
```xml | 
				
			|||
<uses-permission android:name="android.permission.INTERNET" /> | 
				
			|||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | 
				
			|||
<uses-permission android:name="android.permission/SCHEDULE_EXACT_ALARM" /> | 
				
			|||
<uses-permission android:name="android.permission.WAKE_LOCK" /> | 
				
			|||
``` | 
				
			|||
- Create a high-importance **NotificationChannel** `timesafari.daily`. | 
				
			|||
- If **SCHEDULE_EXACT_ALARM** denied on Android 12+, auto-fallback to inexact. | 
				
			|||
 | 
				
			|||
### iOS App Setup (AppDelegate / SceneDelegate) | 
				
			|||
- Register `BGTaskScheduler` with ID `com.timesafari.daily-fetch`. | 
				
			|||
- Request alerts, sound, badge via `UNUserNotificationCenter`. | 
				
			|||
- Create category `DAILY_UPDATE` with a primary `View` action. | 
				
			|||
- Ensure Background Modes: **Background fetch**, **Remote notifications** (optional for future push). | 
				
			|||
 | 
				
			|||
## 5) Data Model (keep minimal, versioned) | 
				
			|||
### Canonical Schema (language-agnostic) | 
				
			|||
``` | 
				
			|||
NotificationContent v1 | 
				
			|||
- id: string (uuid) | 
				
			|||
- title: string | 
				
			|||
- body: string (plain text; may include simple emoji) | 
				
			|||
- scheduledTime: epoch millis (client-local target) | 
				
			|||
- mediaUrl: string? (for future; must be mirrored to local path before use) | 
				
			|||
- fetchTime: epoch millis | 
				
			|||
``` | 
				
			|||
### Kotlin | 
				
			|||
```kotlin | 
				
			|||
@Entity | 
				
			|||
data class NotificationContent( | 
				
			|||
    @PrimaryKey val id: String, | 
				
			|||
    val title: String, | 
				
			|||
    val body: String, | 
				
			|||
    val scheduledTime: Long, | 
				
			|||
    val mediaUrl: String?, | 
				
			|||
    val fetchTime: Long | 
				
			|||
) | 
				
			|||
``` | 
				
			|||
### Swift | 
				
			|||
```swift | 
				
			|||
struct NotificationContent: Codable { | 
				
			|||
    let id: String | 
				
			|||
    let title: String | 
				
			|||
    let body: String | 
				
			|||
    let scheduledTime: TimeInterval | 
				
			|||
    let mediaUrl: String? | 
				
			|||
    let fetchTime: TimeInterval | 
				
			|||
} | 
				
			|||
``` | 
				
			|||
 | 
				
			|||
## 6) Storage Layers | 
				
			|||
**Tier 1: Key-Value (quick)** — next payload, last fetch timestamp, user prefs.   | 
				
			|||
**Tier 2: DB (structured)** — history, media metadata, analytics events.   | 
				
			|||
**Tier 3: Files (large assets)** — images/audio; LRU cache & quotas. | 
				
			|||
 | 
				
			|||
- Android: SharedPreferences/DataStore + Room + `context.cacheDir/notifications/`   | 
				
			|||
- iOS: UserDefaults + Core Data/SQLite + `Library/Caches/notifications/` | 
				
			|||
 | 
				
			|||
## 7) Background Execution | 
				
			|||
### Android — WorkManager | 
				
			|||
- Periodic daily work with constraints (CONNECTED network).   | 
				
			|||
- Total time budget ~10m; use **timeouts** (e.g., fetch ≤30s, overall ≤8m).   | 
				
			|||
- On exception/timeout: **schedule from cache**; then `Result.success()` or `Result.retry()` per policy. | 
				
			|||
 | 
				
			|||
### iOS — BGTaskScheduler | 
				
			|||
- `BGAppRefreshTask` with aggressive time budgeting (10–30s typical).   | 
				
			|||
- Submit next request immediately at start of handler.   | 
				
			|||
- Set `expirationHandler` first; cancel tasks cleanly; **fallback to cache** on failure. | 
				
			|||
 | 
				
			|||
## 8) Scheduling & Display | 
				
			|||
### Android | 
				
			|||
- Prefer `AlarmManager.setExactAndAllowWhileIdle()` if permitted; else inexact.   | 
				
			|||
- Receiver builds notification using **BigTextStyle** for long bodies.   | 
				
			|||
- Limit actions to ≤3; default: `View` (foreground intent). | 
				
			|||
 | 
				
			|||
### iOS | 
				
			|||
- `UNCalendarNotificationTrigger` repeating at preferred time.   | 
				
			|||
- Category `DAILY_UPDATE` with `View` action.   | 
				
			|||
- Media attachments **only if local**; otherwise skip gracefully. | 
				
			|||
 | 
				
			|||
## 9) Fallback Hierarchy (must implement) | 
				
			|||
1. **Foreground prefetch path** if app is open.   | 
				
			|||
2. **Background fetch** with short network timeout.   | 
				
			|||
3. **Last good cache** (annotate staleness: “as of X”).   | 
				
			|||
4. **Emergency phrases** (rotate from static list). | 
				
			|||
 | 
				
			|||
Provide helper: | 
				
			|||
- `withStaleMarker(content) -> content'` appends age label (e.g., “from 3h ago”). | 
				
			|||
 | 
				
			|||
## 10) Failure Matrix & Responses | 
				
			|||
| Scenario | Detect | Action | | 
				
			|||
|---|---|---| | 
				
			|||
| No network / timeout | Exceptions / status | Use last-good; schedule | | 
				
			|||
| Invalid JSON | Parse error | Use emergency content; log | | 
				
			|||
| Storage full | Write error | Evict old; retry minimal payload | | 
				
			|||
| Notifications disabled | OS state | In-app education screen | | 
				
			|||
| Background killed | Gaps in execution | Catch-up next foreground open | | 
				
			|||
 | 
				
			|||
## 11) Metrics (local first; uploader optional) | 
				
			|||
Track per attempt: | 
				
			|||
``` | 
				
			|||
NotificationMetrics v1 | 
				
			|||
- scheduledTime, actualDeliveryTime? | 
				
			|||
- contentAge (ms) | 
				
			|||
- engagement: {TAPPED, DISMISSED, IGNORED}? | 
				
			|||
- failureReason? | 
				
			|||
- platformInfo (oem, os version, app state) | 
				
			|||
``` | 
				
			|||
- Compute: **Fetch Success Rate**, **Delivery Rate**, **Engagement Rate**, **Stale Content Rate**. | 
				
			|||
 | 
				
			|||
## 12) Testing Requirements | 
				
			|||
### Matrix (minimum) | 
				
			|||
- Android 12+ foreground/background/killed; with/without Battery Saver; Wi‑Fi/Mobile/Offline. | 
				
			|||
- iOS 16+ background/Low Power/Focus/Scheduled Summary on & off. | 
				
			|||
- Offline at trigger time (must still display). | 
				
			|||
 | 
				
			|||
### Unit Tests (examples) | 
				
			|||
- Fallback when fetch fails (uses last-good and marks stale).   | 
				
			|||
- Exact vs inexact scheduling path selected correctly.   | 
				
			|||
- Metrics recorded for each stage. | 
				
			|||
 | 
				
			|||
## 13) UX Standards | 
				
			|||
- One clear message; no clutter.   | 
				
			|||
- ≤2 actions; primary takes user into app.   | 
				
			|||
- Respect quiet hours if configured.   | 
				
			|||
- Provide onboarding: value explanation → permission request → time picker → test notification → tips for OEM battery settings (Android) or Focus/Summary (iOS). | 
				
			|||
 | 
				
			|||
## 14) Code Stubs (must generate & wire) | 
				
			|||
### Android — Worker (core pattern) | 
				
			|||
```kotlin | 
				
			|||
class DailyContentWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { | 
				
			|||
    override suspend fun doWork(): Result = try { | 
				
			|||
        withTimeout(8.minutes) { | 
				
			|||
            val content = fetchDailyContent(timeout = 30.seconds) | 
				
			|||
            saveToCache(content) | 
				
			|||
            scheduleNotification(content) | 
				
			|||
        } | 
				
			|||
        Result.success() | 
				
			|||
    } catch (e: TimeoutCancellationException) { | 
				
			|||
        scheduleFromCache(); Result.success() | 
				
			|||
    } catch (e: Exception) { | 
				
			|||
        scheduleFromCache(); Result.retry() | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
``` | 
				
			|||
### iOS — BG Refresh Handler (core pattern) | 
				
			|||
```swift | 
				
			|||
func handleBackgroundRefresh(_ task: BGAppRefreshTask) { | 
				
			|||
    scheduleNextRefresh() | 
				
			|||
    var finished = false | 
				
			|||
    task.expirationHandler = { if !finished { cancelNetwork(); task.setTaskCompleted(success: false) } } | 
				
			|||
    fetchDailyContent(timeout: 15) { result in | 
				
			|||
        defer { finished = true; task.setTaskCompleted(success: result.isSuccess) } | 
				
			|||
        switch result { | 
				
			|||
        case .success(let content): quickSave(content); scheduleNotification(content) | 
				
			|||
        case .failure: scheduleFromCache() | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
``` | 
				
			|||
 | 
				
			|||
## 15) Security & Privacy | 
				
			|||
- Use HTTPS; pin if required.   | 
				
			|||
- Strip PII from payloads; keep content generic by default.   | 
				
			|||
- Store only what is necessary; apply cache quotas; purge on logout/uninstall.   | 
				
			|||
- Respect OS privacy settings (Focus, Scheduled Summary, Quiet Hours). | 
				
			|||
 | 
				
			|||
## 16) Troubleshooting Playbook (LLM should generate helpers) | 
				
			|||
- Android: verify permission, channel, OEM battery settings; `adb shell dumpsys notification`.   | 
				
			|||
- iOS: check authorization, Background App Refresh, Low Power, Focus/Summary state. | 
				
			|||
 | 
				
			|||
## 17) Roadmap Flags (implement behind switches) | 
				
			|||
- `FEATURE_MEDIA_ATTACHMENTS` (default off).   | 
				
			|||
- `FEATURE_PERSONALIZATION_ENGINE` (time/frequency, content types).   | 
				
			|||
- `FEATURE_PUSH_REALTIME` (server-driven for urgent alerts). | 
				
			|||
 | 
				
			|||
## 18) Definition of Done | 
				
			|||
- Notifications deliver daily at user-selected time **without network**.   | 
				
			|||
- Graceful fallback chain proven by tests.   | 
				
			|||
- Metrics recorded locally; viewable log.   | 
				
			|||
- Clear onboarding and self-diagnostic screen.   | 
				
			|||
- Battery/OS constraints documented; user education available. | 
				
			|||
 | 
				
			|||
## 19) Quick Start (LLM execution order) | 
				
			|||
1. Scaffold modules (Android + iOS).   | 
				
			|||
2. Implement models + storage + fallback content.   | 
				
			|||
3. Implement schedulers (AlarmManager / UNCalendarNotificationTrigger).   | 
				
			|||
4. Implement background fetchers (WorkManager / BGTaskScheduler).   | 
				
			|||
5. Wire onboarding + test notification.   | 
				
			|||
6. Add metrics logging.   | 
				
			|||
7. Ship minimal, then iterate. | 
				
			|||
 | 
				
			|||
--- | 
				
			|||
 | 
				
			|||
### Appendix A — Emergency Fallback Lines | 
				
			|||
- "🌅 Good morning! Ready to make today amazing?" | 
				
			|||
- "💪 Every small step forward counts. You've got this!" | 
				
			|||
- "🎯 Focus on what you can control today." | 
				
			|||
- "✨ Your potential is limitless. Keep growing!" | 
				
			|||
- "🌟 Progress over perfection, always." | 
				
			|||
								
									Binary file not shown.
								
							
						
					
								
									Binary file not shown.
								
							
						
					
								
									Binary file not shown.
								
							
						
					
								
									Binary file not shown.
								
							
						
					
								
									Binary file not shown.
								
							
						
					@ -0,0 +1,203 @@ | 
				
			|||
# Daily Notification Plugin - Task TODO List | 
				
			|||
 | 
				
			|||
## Project Overview | 
				
			|||
**Objective**: Build an offline-first daily notifications system for Android (Kotlin) and iOS (Swift) that follows the **Prefetch → Cache → Schedule → Display** pipeline without requiring network at display time. | 
				
			|||
 | 
				
			|||
**Priority**: Reliability over richness | 
				
			|||
 | 
				
			|||
## Phase 1: Foundation (Week 1) - HIGH PRIORITY | 
				
			|||
 | 
				
			|||
### 1.1 Restore Android Implementation | 
				
			|||
- [ ] Create native Android plugin structure | 
				
			|||
- [ ] Implement WorkManager for background content fetching | 
				
			|||
- [ ] Add AlarmManager for notification scheduling | 
				
			|||
- [ ] Create notification channels and permissions | 
				
			|||
- [ ] Implement battery optimization handling | 
				
			|||
- [ ] Add proper error handling and logging | 
				
			|||
 | 
				
			|||
### 1.2 Fix Interface Definitions | 
				
			|||
- [ ] Align TypeScript interfaces with project requirements | 
				
			|||
- [ ] Add missing properties referenced in tests | 
				
			|||
- [ ] Implement proper validation utilities | 
				
			|||
- [ ] Create comprehensive error types | 
				
			|||
- [ ] Add retry mechanism interfaces | 
				
			|||
 | 
				
			|||
### 1.3 Fix Test Suite | 
				
			|||
- [ ] Update all test files to match current interfaces | 
				
			|||
- [ ] Implement proper mock objects | 
				
			|||
- [ ] Fix TypeScript compilation errors | 
				
			|||
- [ ] Ensure 100% test coverage | 
				
			|||
- [ ] Add integration tests for native platforms | 
				
			|||
 | 
				
			|||
## Phase 2: Core Pipeline Implementation (Week 2) - HIGH PRIORITY | 
				
			|||
 | 
				
			|||
### 2.1 Prefetch System | 
				
			|||
- [ ] Implement background content fetching | 
				
			|||
- [ ] Add network timeout handling (30s max) | 
				
			|||
- [ ] Create content validation system | 
				
			|||
- [ ] Implement retry mechanisms with exponential backoff | 
				
			|||
- [ ] Add network state monitoring | 
				
			|||
 | 
				
			|||
### 2.2 Caching Layer | 
				
			|||
- [ ] Create local storage for notifications | 
				
			|||
- [ ] Implement cache eviction policies (LRU) | 
				
			|||
- [ ] Add offline content management | 
				
			|||
- [ ] Create cache quota management | 
				
			|||
- [ ] Implement cache versioning | 
				
			|||
 | 
				
			|||
### 2.3 Enhanced Scheduling | 
				
			|||
- [ ] Implement reliable notification delivery | 
				
			|||
- [ ] Add platform-specific optimizations | 
				
			|||
- [ ] Handle battery optimization settings | 
				
			|||
- [ ] Create adaptive scheduling based on device state | 
				
			|||
- [ ] Add quiet hours support | 
				
			|||
 | 
				
			|||
## Phase 3: Production Features (Week 3) - MEDIUM PRIORITY | 
				
			|||
 | 
				
			|||
### 3.1 Fallback System | 
				
			|||
- [ ] Implement emergency content rotation | 
				
			|||
- [ ] Add stale content marking ("from X ago") | 
				
			|||
- [ ] Create graceful degradation paths | 
				
			|||
- [ ] Implement last-known-good fallback | 
				
			|||
- [ ] Add offline fallback content | 
				
			|||
 | 
				
			|||
### 3.2 Metrics & Monitoring | 
				
			|||
- [ ] Create local analytics collection | 
				
			|||
- [ ] Implement performance monitoring | 
				
			|||
- [ ] Add error tracking and reporting | 
				
			|||
- [ ] Create metrics dashboard | 
				
			|||
- [ ] Implement user engagement tracking | 
				
			|||
 | 
				
			|||
### 3.3 Security & Privacy | 
				
			|||
- [ ] Add input validation for all user inputs | 
				
			|||
- [ ] Implement secure storage for sensitive data | 
				
			|||
- [ ] Create proper permission handling | 
				
			|||
- [ ] Add network security (HTTPS, certificate pinning) | 
				
			|||
- [ ] Implement audit logging | 
				
			|||
 | 
				
			|||
## Phase 4: Advanced Features (Week 4) - LOW PRIORITY | 
				
			|||
 | 
				
			|||
### 4.1 User Experience | 
				
			|||
- [ ] Create onboarding flow | 
				
			|||
- [ ] Add permission request handling | 
				
			|||
- [ ] Implement time picker interface | 
				
			|||
- [ ] Add test notification functionality | 
				
			|||
- [ ] Create troubleshooting guides | 
				
			|||
 | 
				
			|||
### 4.2 Platform Optimizations | 
				
			|||
- [ ] Android: OEM battery settings education | 
				
			|||
- [ ] iOS: Focus/Summary mode handling | 
				
			|||
- [ ] Web: Service worker implementation | 
				
			|||
- [ ] Cross-platform synchronization | 
				
			|||
- [ ] Performance optimization | 
				
			|||
 | 
				
			|||
### 4.3 Enterprise Features | 
				
			|||
- [ ] Multi-tenant support | 
				
			|||
- [ ] Advanced analytics | 
				
			|||
- [ ] Custom notification templates | 
				
			|||
- [ ] Integration with external services | 
				
			|||
- [ ] A/B testing support | 
				
			|||
 | 
				
			|||
## Technical Requirements | 
				
			|||
 | 
				
			|||
### Android Implementation | 
				
			|||
- [ ] Use WorkManager for background tasks | 
				
			|||
- [ ] Implement AlarmManager for exact scheduling | 
				
			|||
- [ ] Create notification channels with high importance | 
				
			|||
- [ ] Handle SCHEDULE_EXACT_ALARM permission | 
				
			|||
- [ ] Add battery optimization exemption requests | 
				
			|||
 | 
				
			|||
### iOS Implementation | 
				
			|||
- [ ] Use BGTaskScheduler for background refresh | 
				
			|||
- [ ] Implement UNCalendarNotificationTrigger | 
				
			|||
- [ ] Create DAILY_UPDATE notification category | 
				
			|||
- [ ] Handle Background App Refresh settings | 
				
			|||
- [ ] Add Focus/Summary mode support | 
				
			|||
 | 
				
			|||
### Data Model | 
				
			|||
- [ ] Implement NotificationContent v1 schema | 
				
			|||
- [ ] Add versioning support | 
				
			|||
- [ ] Create storage abstraction layers | 
				
			|||
- [ ] Implement cache policies | 
				
			|||
- [ ] Add analytics event tracking | 
				
			|||
 | 
				
			|||
## Testing Requirements | 
				
			|||
 | 
				
			|||
### Unit Tests | 
				
			|||
- [ ] Fallback when fetch fails | 
				
			|||
- [ ] Exact vs inexact scheduling path selection | 
				
			|||
- [ ] Metrics recording for each stage | 
				
			|||
- [ ] Cache eviction policies | 
				
			|||
- [ ] Error handling scenarios | 
				
			|||
 | 
				
			|||
### Integration Tests | 
				
			|||
- [ ] Android foreground/background/killed scenarios | 
				
			|||
- [ ] iOS background/Low Power/Focus modes | 
				
			|||
- [ ] Offline at trigger time | 
				
			|||
- [ ] Battery saver mode handling | 
				
			|||
- [ ] Network state changes | 
				
			|||
 | 
				
			|||
### Performance Tests | 
				
			|||
- [ ] Memory usage monitoring | 
				
			|||
- [ ] Battery impact measurement | 
				
			|||
- [ ] Notification delivery latency | 
				
			|||
- [ ] Cache performance | 
				
			|||
- [ ] Background task efficiency | 
				
			|||
 | 
				
			|||
## Documentation Requirements | 
				
			|||
 | 
				
			|||
### API Documentation | 
				
			|||
- [ ] Complete method documentation | 
				
			|||
- [ ] Usage examples for each platform | 
				
			|||
- [ ] Error handling guides | 
				
			|||
- [ ] Performance optimization tips | 
				
			|||
- [ ] Troubleshooting playbook | 
				
			|||
 | 
				
			|||
### User Guides | 
				
			|||
- [ ] Installation instructions | 
				
			|||
- [ ] Configuration guide | 
				
			|||
- [ ] Platform-specific setup | 
				
			|||
- [ ] Battery optimization tips | 
				
			|||
- [ ] Common issues and solutions | 
				
			|||
 | 
				
			|||
## Security Checklist | 
				
			|||
 | 
				
			|||
- [ ] Input validation for all parameters | 
				
			|||
- [ ] Secure storage implementation | 
				
			|||
- [ ] Permission handling | 
				
			|||
- [ ] Network security | 
				
			|||
- [ ] Error handling without information leakage | 
				
			|||
- [ ] Audit logging | 
				
			|||
- [ ] Privacy compliance | 
				
			|||
- [ ] Secure defaults | 
				
			|||
 | 
				
			|||
## Definition of Done | 
				
			|||
 | 
				
			|||
- [ ] Notifications deliver daily at user-selected time without network | 
				
			|||
- [ ] Graceful fallback chain proven by tests | 
				
			|||
- [ ] Metrics recorded locally and viewable | 
				
			|||
- [ ] Clear onboarding and self-diagnostic screen | 
				
			|||
- [ ] Battery/OS constraints documented | 
				
			|||
- [ ] User education available for platform-specific settings | 
				
			|||
 | 
				
			|||
## Current Status | 
				
			|||
 | 
				
			|||
**Build Status**: ✅ Working | 
				
			|||
**Test Status**: ❌ 13/13 tests failing | 
				
			|||
**Android Implementation**: ❌ Missing | 
				
			|||
**iOS Implementation**: ✅ Basic implementation exists | 
				
			|||
**Web Implementation**: ⚠️ Placeholder only | 
				
			|||
**Core Pipeline**: ❌ Not implemented | 
				
			|||
 | 
				
			|||
## Next Immediate Actions | 
				
			|||
 | 
				
			|||
1. **Start Android Implementation** - Create native plugin structure | 
				
			|||
2. **Fix Interface Definitions** - Align with project requirements | 
				
			|||
3. **Update Test Suite** - Fix compilation errors and implement mocks | 
				
			|||
4. **Implement Core Pipeline** - Begin prefetch → cache → schedule → display flow | 
				
			|||
 | 
				
			|||
--- | 
				
			|||
 | 
				
			|||
**Last Updated**: December 2024 | 
				
			|||
**Author**: Matthew Raymer | 
				
			|||
**Priority**: High - Foundation work needed before advanced features | 
				
			|||
@ -0,0 +1,395 @@ | 
				
			|||
/** | 
				
			|||
 * DailyNotificationFetchWorker.java | 
				
			|||
 *  | 
				
			|||
 * WorkManager worker for background content fetching | 
				
			|||
 * Implements the prefetch step with timeout handling and retry logic | 
				
			|||
 *  | 
				
			|||
 * @author Matthew Raymer | 
				
			|||
 * @version 1.0.0 | 
				
			|||
 */ | 
				
			|||
 | 
				
			|||
package com.timesafari.dailynotification; | 
				
			|||
 | 
				
			|||
import android.content.Context; | 
				
			|||
import android.util.Log; | 
				
			|||
 | 
				
			|||
import androidx.annotation.NonNull; | 
				
			|||
import androidx.work.Data; | 
				
			|||
import androidx.work.Worker; | 
				
			|||
import androidx.work.WorkerParameters; | 
				
			|||
 | 
				
			|||
import java.util.concurrent.TimeUnit; | 
				
			|||
 | 
				
			|||
/** | 
				
			|||
 * Background worker for fetching daily notification content | 
				
			|||
 *  | 
				
			|||
 * This worker implements the prefetch step of the offline-first pipeline. | 
				
			|||
 * It runs in the background to fetch content before it's needed, | 
				
			|||
 * with proper timeout handling and retry mechanisms. | 
				
			|||
 */ | 
				
			|||
public class DailyNotificationFetchWorker extends Worker { | 
				
			|||
     | 
				
			|||
    private static final String TAG = "DailyNotificationFetchWorker"; | 
				
			|||
    private static final String KEY_SCHEDULED_TIME = "scheduled_time"; | 
				
			|||
    private static final String KEY_FETCH_TIME = "fetch_time"; | 
				
			|||
    private static final String KEY_RETRY_COUNT = "retry_count"; | 
				
			|||
    private static final String KEY_IMMEDIATE = "immediate"; | 
				
			|||
     | 
				
			|||
    private static final int MAX_RETRY_ATTEMPTS = 3; | 
				
			|||
    private static final long WORK_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes total
 | 
				
			|||
    private static final long FETCH_TIMEOUT_MS = 30 * 1000; // 30 seconds for fetch
 | 
				
			|||
     | 
				
			|||
    private final Context context; | 
				
			|||
    private final DailyNotificationStorage storage; | 
				
			|||
    private final DailyNotificationFetcher fetcher; | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Constructor | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param params Worker parameters | 
				
			|||
     */ | 
				
			|||
    public DailyNotificationFetchWorker(@NonNull Context context,  | 
				
			|||
                                      @NonNull WorkerParameters params) { | 
				
			|||
        super(context, params); | 
				
			|||
        this.context = context; | 
				
			|||
        this.storage = new DailyNotificationStorage(context); | 
				
			|||
        this.fetcher = new DailyNotificationFetcher(context, storage); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Main work method - fetch content with timeout and retry logic | 
				
			|||
     *  | 
				
			|||
     * @return Result indicating success, failure, or retry | 
				
			|||
     */ | 
				
			|||
    @NonNull | 
				
			|||
    @Override | 
				
			|||
    public Result doWork() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Starting background content fetch"); | 
				
			|||
             | 
				
			|||
            // Get input data
 | 
				
			|||
            Data inputData = getInputData(); | 
				
			|||
            long scheduledTime = inputData.getLong(KEY_SCHEDULED_TIME, 0); | 
				
			|||
            long fetchTime = inputData.getLong(KEY_FETCH_TIME, 0); | 
				
			|||
            int retryCount = inputData.getInt(KEY_RETRY_COUNT, 0); | 
				
			|||
            boolean immediate = inputData.getBoolean(KEY_IMMEDIATE, false); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, String.format("Fetch parameters - Scheduled: %d, Fetch: %d, Retry: %d, Immediate: %s", | 
				
			|||
                scheduledTime, fetchTime, retryCount, immediate)); | 
				
			|||
             | 
				
			|||
            // Check if we should proceed with fetch
 | 
				
			|||
            if (!shouldProceedWithFetch(scheduledTime, fetchTime)) { | 
				
			|||
                Log.d(TAG, "Skipping fetch - conditions not met"); | 
				
			|||
                return Result.success(); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Attempt to fetch content with timeout
 | 
				
			|||
            NotificationContent content = fetchContentWithTimeout(); | 
				
			|||
             | 
				
			|||
            if (content != null) { | 
				
			|||
                // Success - save content and schedule notification
 | 
				
			|||
                handleSuccessfulFetch(content); | 
				
			|||
                return Result.success(); | 
				
			|||
                 | 
				
			|||
            } else { | 
				
			|||
                // Fetch failed - handle retry logic
 | 
				
			|||
                return handleFailedFetch(retryCount, scheduledTime); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Unexpected error during background fetch", e); | 
				
			|||
            return handleFailedFetch(0, 0); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if we should proceed with the fetch | 
				
			|||
     *  | 
				
			|||
     * @param scheduledTime When notification is scheduled for | 
				
			|||
     * @param fetchTime When fetch was originally scheduled for | 
				
			|||
     * @return true if fetch should proceed | 
				
			|||
     */ | 
				
			|||
    private boolean shouldProceedWithFetch(long scheduledTime, long fetchTime) { | 
				
			|||
        long currentTime = System.currentTimeMillis(); | 
				
			|||
         | 
				
			|||
        // If this is an immediate fetch, always proceed
 | 
				
			|||
        if (fetchTime == 0) { | 
				
			|||
            return true; | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        // Check if fetch time has passed
 | 
				
			|||
        if (currentTime < fetchTime) { | 
				
			|||
            Log.d(TAG, "Fetch time not yet reached"); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        // Check if notification time has passed
 | 
				
			|||
        if (currentTime >= scheduledTime) { | 
				
			|||
            Log.d(TAG, "Notification time has passed, fetch not needed"); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        // Check if we already have recent content
 | 
				
			|||
        if (!storage.shouldFetchNewContent()) { | 
				
			|||
            Log.d(TAG, "Recent content available, fetch not needed"); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        return true; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Fetch content with timeout handling | 
				
			|||
     *  | 
				
			|||
     * @return Fetched content or null if failed | 
				
			|||
     */ | 
				
			|||
    private NotificationContent fetchContentWithTimeout() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Fetching content with timeout: " + FETCH_TIMEOUT_MS + "ms"); | 
				
			|||
             | 
				
			|||
            // Use a simple timeout mechanism
 | 
				
			|||
            // In production, you might use CompletableFuture with timeout
 | 
				
			|||
            long startTime = System.currentTimeMillis(); | 
				
			|||
             | 
				
			|||
            // Attempt fetch
 | 
				
			|||
            NotificationContent content = fetcher.fetchContentImmediately(); | 
				
			|||
             | 
				
			|||
            long fetchDuration = System.currentTimeMillis() - startTime; | 
				
			|||
             | 
				
			|||
            if (content != null) { | 
				
			|||
                Log.d(TAG, "Content fetched successfully in " + fetchDuration + "ms"); | 
				
			|||
                return content; | 
				
			|||
            } else { | 
				
			|||
                Log.w(TAG, "Content fetch returned null after " + fetchDuration + "ms"); | 
				
			|||
                return null; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error during content fetch", e); | 
				
			|||
            return null; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Handle successful content fetch | 
				
			|||
     *  | 
				
			|||
     * @param content Successfully fetched content | 
				
			|||
     */ | 
				
			|||
    private void handleSuccessfulFetch(NotificationContent content) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Handling successful content fetch: " + content.getId()); | 
				
			|||
             | 
				
			|||
            // Content is already saved by the fetcher
 | 
				
			|||
            // Update last fetch time
 | 
				
			|||
            storage.setLastFetchTime(System.currentTimeMillis()); | 
				
			|||
             | 
				
			|||
            // Schedule notification if not already scheduled
 | 
				
			|||
            scheduleNotificationIfNeeded(content); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Successful fetch handling completed"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error handling successful fetch", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Handle failed content fetch with retry logic | 
				
			|||
     *  | 
				
			|||
     * @param retryCount Current retry attempt | 
				
			|||
     * @param scheduledTime When notification is scheduled for | 
				
			|||
     * @return Result indicating retry or failure | 
				
			|||
     */ | 
				
			|||
    private Result handleFailedFetch(int retryCount, long scheduledTime) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Handling failed fetch - Retry: " + retryCount); | 
				
			|||
             | 
				
			|||
            if (retryCount < MAX_RETRY_ATTEMPTS) { | 
				
			|||
                // Schedule retry
 | 
				
			|||
                scheduleRetry(retryCount + 1, scheduledTime); | 
				
			|||
                Log.i(TAG, "Scheduled retry attempt " + (retryCount + 1)); | 
				
			|||
                return Result.retry(); | 
				
			|||
                 | 
				
			|||
            } else { | 
				
			|||
                // Max retries reached - use fallback content
 | 
				
			|||
                Log.w(TAG, "Max retries reached, using fallback content"); | 
				
			|||
                useFallbackContent(scheduledTime); | 
				
			|||
                return Result.success(); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error handling failed fetch", e); | 
				
			|||
            return Result.failure(); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule a retry attempt | 
				
			|||
     *  | 
				
			|||
     * @param retryCount New retry attempt number | 
				
			|||
     * @param scheduledTime When notification is scheduled for | 
				
			|||
     */ | 
				
			|||
    private void scheduleRetry(int retryCount, long scheduledTime) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Scheduling retry attempt " + retryCount); | 
				
			|||
             | 
				
			|||
            // Calculate retry delay with exponential backoff
 | 
				
			|||
            long retryDelay = calculateRetryDelay(retryCount); | 
				
			|||
             | 
				
			|||
            // Create retry work request
 | 
				
			|||
            Data retryData = new Data.Builder() | 
				
			|||
                .putLong(KEY_SCHEDULED_TIME, scheduledTime) | 
				
			|||
                .putLong(KEY_FETCH_TIME, System.currentTimeMillis()) | 
				
			|||
                .putInt(KEY_RETRY_COUNT, retryCount) | 
				
			|||
                .build(); | 
				
			|||
             | 
				
			|||
            androidx.work.OneTimeWorkRequest retryWork =  | 
				
			|||
                new androidx.work.OneTimeWorkRequest.Builder(DailyNotificationFetchWorker.class) | 
				
			|||
                    .setInputData(retryData) | 
				
			|||
                    .setInitialDelay(retryDelay, TimeUnit.MILLISECONDS) | 
				
			|||
                    .build(); | 
				
			|||
             | 
				
			|||
            androidx.work.WorkManager.getInstance(context).enqueue(retryWork); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Retry scheduled for " + retryDelay + "ms from now"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling retry", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Calculate retry delay with exponential backoff | 
				
			|||
     *  | 
				
			|||
     * @param retryCount Current retry attempt | 
				
			|||
     * @return Delay in milliseconds | 
				
			|||
     */ | 
				
			|||
    private long calculateRetryDelay(int retryCount) { | 
				
			|||
        // Base delay: 1 minute, exponential backoff: 2^retryCount
 | 
				
			|||
        long baseDelay = 60 * 1000; // 1 minute
 | 
				
			|||
        long exponentialDelay = baseDelay * (long) Math.pow(2, retryCount - 1); | 
				
			|||
         | 
				
			|||
        // Cap at 1 hour
 | 
				
			|||
        long maxDelay = 60 * 60 * 1000; // 1 hour
 | 
				
			|||
        return Math.min(exponentialDelay, maxDelay); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Use fallback content when all retries fail | 
				
			|||
     *  | 
				
			|||
     * @param scheduledTime When notification is scheduled for | 
				
			|||
     */ | 
				
			|||
    private void useFallbackContent(long scheduledTime) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Using fallback content for scheduled time: " + scheduledTime); | 
				
			|||
             | 
				
			|||
            // Get fallback content from storage or create emergency content
 | 
				
			|||
            NotificationContent fallbackContent = getFallbackContent(scheduledTime); | 
				
			|||
             | 
				
			|||
            if (fallbackContent != null) { | 
				
			|||
                // Save fallback content
 | 
				
			|||
                storage.saveNotificationContent(fallbackContent); | 
				
			|||
                 | 
				
			|||
                // Schedule notification
 | 
				
			|||
                scheduleNotificationIfNeeded(fallbackContent); | 
				
			|||
                 | 
				
			|||
                Log.i(TAG, "Fallback content applied successfully"); | 
				
			|||
            } else { | 
				
			|||
                Log.e(TAG, "Failed to get fallback content"); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error using fallback content", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get fallback content for the scheduled time | 
				
			|||
     *  | 
				
			|||
     * @param scheduledTime When notification is scheduled for | 
				
			|||
     * @return Fallback notification content | 
				
			|||
     */ | 
				
			|||
    private NotificationContent getFallbackContent(long scheduledTime) { | 
				
			|||
        try { | 
				
			|||
            // Try to get last known good content
 | 
				
			|||
            NotificationContent lastContent = storage.getLastNotification(); | 
				
			|||
             | 
				
			|||
            if (lastContent != null && !lastContent.isStale()) { | 
				
			|||
                Log.d(TAG, "Using last known good content as fallback"); | 
				
			|||
                 | 
				
			|||
                // Create new content based on last good content
 | 
				
			|||
                NotificationContent fallbackContent = new NotificationContent(); | 
				
			|||
                fallbackContent.setTitle(lastContent.getTitle()); | 
				
			|||
                fallbackContent.setBody(lastContent.getBody() + " (from " +  | 
				
			|||
                    lastContent.getAgeString() + ")"); | 
				
			|||
                fallbackContent.setScheduledTime(scheduledTime); | 
				
			|||
                fallbackContent.setSound(lastContent.isSound()); | 
				
			|||
                fallbackContent.setPriority(lastContent.getPriority()); | 
				
			|||
                fallbackContent.setUrl(lastContent.getUrl()); | 
				
			|||
                fallbackContent.setFetchTime(System.currentTimeMillis()); | 
				
			|||
                 | 
				
			|||
                return fallbackContent; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Create emergency fallback content
 | 
				
			|||
            Log.w(TAG, "Creating emergency fallback content"); | 
				
			|||
            return createEmergencyFallbackContent(scheduledTime); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error getting fallback content", e); | 
				
			|||
            return createEmergencyFallbackContent(scheduledTime); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Create emergency fallback content | 
				
			|||
     *  | 
				
			|||
     * @param scheduledTime When notification is scheduled for | 
				
			|||
     * @return Emergency notification content | 
				
			|||
     */ | 
				
			|||
    private NotificationContent createEmergencyFallbackContent(long scheduledTime) { | 
				
			|||
        NotificationContent content = new NotificationContent(); | 
				
			|||
        content.setTitle("Daily Update"); | 
				
			|||
        content.setBody("🌅 Good morning! Ready to make today amazing?"); | 
				
			|||
        content.setScheduledTime(scheduledTime); | 
				
			|||
        content.setFetchTime(System.currentTimeMillis()); | 
				
			|||
        content.setPriority("default"); | 
				
			|||
        content.setSound(true); | 
				
			|||
         | 
				
			|||
        return content; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule notification if not already scheduled | 
				
			|||
     *  | 
				
			|||
     * @param content Notification content to schedule | 
				
			|||
     */ | 
				
			|||
    private void scheduleNotificationIfNeeded(NotificationContent content) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Checking if notification needs scheduling: " + content.getId()); | 
				
			|||
             | 
				
			|||
            // Check if notification is already scheduled
 | 
				
			|||
            DailyNotificationScheduler scheduler = new DailyNotificationScheduler( | 
				
			|||
                context, | 
				
			|||
                (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE) | 
				
			|||
            ); | 
				
			|||
             | 
				
			|||
            if (!scheduler.isNotificationScheduled(content.getId())) { | 
				
			|||
                Log.d(TAG, "Scheduling notification: " + content.getId()); | 
				
			|||
                boolean scheduled = scheduler.scheduleNotification(content); | 
				
			|||
                 | 
				
			|||
                if (scheduled) { | 
				
			|||
                    Log.i(TAG, "Notification scheduled successfully"); | 
				
			|||
                } else { | 
				
			|||
                    Log.e(TAG, "Failed to schedule notification"); | 
				
			|||
                } | 
				
			|||
            } else { | 
				
			|||
                Log.d(TAG, "Notification already scheduled: " + content.getId()); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error checking/scheduling notification", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,364 @@ | 
				
			|||
/** | 
				
			|||
 * DailyNotificationFetcher.java | 
				
			|||
 *  | 
				
			|||
 * Handles background content fetching for daily notifications | 
				
			|||
 * Implements the prefetch step of the prefetch → cache → schedule → display pipeline | 
				
			|||
 *  | 
				
			|||
 * @author Matthew Raymer | 
				
			|||
 * @version 1.0.0 | 
				
			|||
 */ | 
				
			|||
 | 
				
			|||
package com.timesafari.dailynotification; | 
				
			|||
 | 
				
			|||
import android.content.Context; | 
				
			|||
import android.util.Log; | 
				
			|||
 | 
				
			|||
import androidx.work.Data; | 
				
			|||
import androidx.work.OneTimeWorkRequest; | 
				
			|||
import androidx.work.WorkManager; | 
				
			|||
 | 
				
			|||
import java.io.IOException; | 
				
			|||
import java.net.HttpURLConnection; | 
				
			|||
import java.net.URL; | 
				
			|||
import java.util.concurrent.TimeUnit; | 
				
			|||
 | 
				
			|||
/** | 
				
			|||
 * Manages background content fetching for daily notifications | 
				
			|||
 *  | 
				
			|||
 * This class implements the prefetch step of the offline-first pipeline. | 
				
			|||
 * It schedules background work to fetch content before it's needed, | 
				
			|||
 * with proper timeout handling and fallback mechanisms. | 
				
			|||
 */ | 
				
			|||
public class DailyNotificationFetcher { | 
				
			|||
     | 
				
			|||
    private static final String TAG = "DailyNotificationFetcher"; | 
				
			|||
    private static final String WORK_TAG_FETCH = "daily_notification_fetch"; | 
				
			|||
    private static final String WORK_TAG_MAINTENANCE = "daily_notification_maintenance"; | 
				
			|||
     | 
				
			|||
    private static final int NETWORK_TIMEOUT_MS = 30000; // 30 seconds
 | 
				
			|||
    private static final int MAX_RETRY_ATTEMPTS = 3; | 
				
			|||
    private static final long RETRY_DELAY_MS = 60000; // 1 minute
 | 
				
			|||
     | 
				
			|||
    private final Context context; | 
				
			|||
    private final DailyNotificationStorage storage; | 
				
			|||
    private final WorkManager workManager; | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Constructor | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param storage Storage instance for saving fetched content | 
				
			|||
     */ | 
				
			|||
    public DailyNotificationFetcher(Context context, DailyNotificationStorage storage) { | 
				
			|||
        this.context = context; | 
				
			|||
        this.storage = storage; | 
				
			|||
        this.workManager = WorkManager.getInstance(context); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule a background fetch for content | 
				
			|||
     *  | 
				
			|||
     * @param scheduledTime When the notification is scheduled for | 
				
			|||
     */ | 
				
			|||
    public void scheduleFetch(long scheduledTime) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Scheduling background fetch for " + scheduledTime); | 
				
			|||
             | 
				
			|||
            // Calculate fetch time (1 hour before notification)
 | 
				
			|||
            long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1); | 
				
			|||
             | 
				
			|||
            if (fetchTime > System.currentTimeMillis()) { | 
				
			|||
                // Create work data
 | 
				
			|||
                Data inputData = new Data.Builder() | 
				
			|||
                    .putLong("scheduled_time", scheduledTime) | 
				
			|||
                    .putLong("fetch_time", fetchTime) | 
				
			|||
                    .putInt("retry_count", 0) | 
				
			|||
                    .build(); | 
				
			|||
                 | 
				
			|||
                // Create one-time work request
 | 
				
			|||
                OneTimeWorkRequest fetchWork = new OneTimeWorkRequest.Builder( | 
				
			|||
                    DailyNotificationFetchWorker.class) | 
				
			|||
                    .setInputData(inputData) | 
				
			|||
                    .addTag(WORK_TAG_FETCH) | 
				
			|||
                    .setInitialDelay(fetchTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS) | 
				
			|||
                    .build(); | 
				
			|||
                 | 
				
			|||
                // Enqueue the work
 | 
				
			|||
                workManager.enqueue(fetchWork); | 
				
			|||
                 | 
				
			|||
                Log.i(TAG, "Background fetch scheduled successfully"); | 
				
			|||
                 | 
				
			|||
            } else { | 
				
			|||
                Log.w(TAG, "Fetch time has already passed, scheduling immediate fetch"); | 
				
			|||
                scheduleImmediateFetch(); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling background fetch", e); | 
				
			|||
            // Fallback to immediate fetch
 | 
				
			|||
            scheduleImmediateFetch(); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule an immediate fetch (fallback) | 
				
			|||
     */ | 
				
			|||
    public void scheduleImmediateFetch() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Scheduling immediate fetch"); | 
				
			|||
             | 
				
			|||
            Data inputData = new Data.Builder() | 
				
			|||
                .putLong("scheduled_time", System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)) | 
				
			|||
                .putLong("fetch_time", System.currentTimeMillis()) | 
				
			|||
                .putInt("retry_count", 0) | 
				
			|||
                .putBoolean("immediate", true) | 
				
			|||
                .build(); | 
				
			|||
             | 
				
			|||
            OneTimeWorkRequest fetchWork = new OneTimeWorkRequest.Builder( | 
				
			|||
                DailyNotificationFetchWorker.class) | 
				
			|||
                .setInputData(inputData) | 
				
			|||
                .addTag(WORK_TAG_FETCH) | 
				
			|||
                .build(); | 
				
			|||
             | 
				
			|||
            workManager.enqueue(fetchWork); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Immediate fetch scheduled successfully"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling immediate fetch", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Fetch content immediately (synchronous) | 
				
			|||
     *  | 
				
			|||
     * @return Fetched notification content or null if failed | 
				
			|||
     */ | 
				
			|||
    public NotificationContent fetchContentImmediately() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Fetching content immediately"); | 
				
			|||
             | 
				
			|||
            // Check if we should fetch new content
 | 
				
			|||
            if (!storage.shouldFetchNewContent()) { | 
				
			|||
                Log.d(TAG, "Content fetch not needed yet"); | 
				
			|||
                return storage.getLastNotification(); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Attempt to fetch from network
 | 
				
			|||
            NotificationContent content = fetchFromNetwork(); | 
				
			|||
             | 
				
			|||
            if (content != null) { | 
				
			|||
                // Save to storage
 | 
				
			|||
                storage.saveNotificationContent(content); | 
				
			|||
                storage.setLastFetchTime(System.currentTimeMillis()); | 
				
			|||
                 | 
				
			|||
                Log.i(TAG, "Content fetched and saved successfully"); | 
				
			|||
                return content; | 
				
			|||
                 | 
				
			|||
            } else { | 
				
			|||
                // Fallback to cached content
 | 
				
			|||
                Log.w(TAG, "Network fetch failed, using cached content"); | 
				
			|||
                return getFallbackContent(); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error during immediate content fetch", e); | 
				
			|||
            return getFallbackContent(); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Fetch content from network with timeout | 
				
			|||
     *  | 
				
			|||
     * @return Fetched content or null if failed | 
				
			|||
     */ | 
				
			|||
    private NotificationContent fetchFromNetwork() { | 
				
			|||
        HttpURLConnection connection = null; | 
				
			|||
         | 
				
			|||
        try { | 
				
			|||
            // Create connection to content endpoint
 | 
				
			|||
            URL url = new URL(getContentEndpoint()); | 
				
			|||
            connection = (HttpURLConnection) url.openConnection(); | 
				
			|||
             | 
				
			|||
            // Set timeout
 | 
				
			|||
            connection.setConnectTimeout(NETWORK_TIMEOUT_MS); | 
				
			|||
            connection.setReadTimeout(NETWORK_TIMEOUT_MS); | 
				
			|||
            connection.setRequestMethod("GET"); | 
				
			|||
             | 
				
			|||
            // Add headers
 | 
				
			|||
            connection.setRequestProperty("User-Agent", "TimeSafari-DailyNotification/1.0"); | 
				
			|||
            connection.setRequestProperty("Accept", "application/json"); | 
				
			|||
             | 
				
			|||
            // Connect and check response
 | 
				
			|||
            int responseCode = connection.getResponseCode(); | 
				
			|||
             | 
				
			|||
            if (responseCode == HttpURLConnection.HTTP_OK) { | 
				
			|||
                // Parse response and create notification content
 | 
				
			|||
                NotificationContent content = parseNetworkResponse(connection); | 
				
			|||
                 | 
				
			|||
                if (content != null) { | 
				
			|||
                    Log.d(TAG, "Content fetched from network successfully"); | 
				
			|||
                    return content; | 
				
			|||
                } | 
				
			|||
                 | 
				
			|||
            } else { | 
				
			|||
                Log.w(TAG, "Network request failed with response code: " + responseCode); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (IOException e) { | 
				
			|||
            Log.e(TAG, "Network error during content fetch", e); | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Unexpected error during network fetch", e); | 
				
			|||
        } finally { | 
				
			|||
            if (connection != null) { | 
				
			|||
                connection.disconnect(); | 
				
			|||
            } | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        return null; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Parse network response into notification content | 
				
			|||
     *  | 
				
			|||
     * @param connection HTTP connection with response | 
				
			|||
     * @return Parsed notification content or null if parsing failed | 
				
			|||
     */ | 
				
			|||
    private NotificationContent parseNetworkResponse(HttpURLConnection connection) { | 
				
			|||
        try { | 
				
			|||
            // This is a simplified parser - in production you'd use a proper JSON parser
 | 
				
			|||
            // For now, we'll create a placeholder content
 | 
				
			|||
             | 
				
			|||
            NotificationContent content = new NotificationContent(); | 
				
			|||
            content.setTitle("Daily Update"); | 
				
			|||
            content.setBody("Your daily notification is ready"); | 
				
			|||
            content.setScheduledTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); | 
				
			|||
            content.setFetchTime(System.currentTimeMillis()); | 
				
			|||
             | 
				
			|||
            return content; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error parsing network response", e); | 
				
			|||
            return null; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get fallback content when network fetch fails | 
				
			|||
     *  | 
				
			|||
     * @return Fallback notification content | 
				
			|||
     */ | 
				
			|||
    private NotificationContent getFallbackContent() { | 
				
			|||
        try { | 
				
			|||
            // Try to get last known good content
 | 
				
			|||
            NotificationContent lastContent = storage.getLastNotification(); | 
				
			|||
             | 
				
			|||
            if (lastContent != null && !lastContent.isStale()) { | 
				
			|||
                Log.d(TAG, "Using last known good content as fallback"); | 
				
			|||
                return lastContent; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Create emergency fallback content
 | 
				
			|||
            Log.w(TAG, "Creating emergency fallback content"); | 
				
			|||
            return createEmergencyFallbackContent(); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error getting fallback content", e); | 
				
			|||
            return createEmergencyFallbackContent(); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Create emergency fallback content | 
				
			|||
     *  | 
				
			|||
     * @return Emergency notification content | 
				
			|||
     */ | 
				
			|||
    private NotificationContent createEmergencyFallbackContent() { | 
				
			|||
        NotificationContent content = new NotificationContent(); | 
				
			|||
        content.setTitle("Daily Update"); | 
				
			|||
        content.setBody("🌅 Good morning! Ready to make today amazing?"); | 
				
			|||
        content.setScheduledTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); | 
				
			|||
        content.setFetchTime(System.currentTimeMillis()); | 
				
			|||
        content.setPriority("default"); | 
				
			|||
        content.setSound(true); | 
				
			|||
         | 
				
			|||
        return content; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the content endpoint URL | 
				
			|||
     *  | 
				
			|||
     * @return Content endpoint URL | 
				
			|||
     */ | 
				
			|||
    private String getContentEndpoint() { | 
				
			|||
        // This would typically come from configuration
 | 
				
			|||
        // For now, return a placeholder
 | 
				
			|||
        return "https://api.timesafari.com/daily-content"; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule maintenance work | 
				
			|||
     */ | 
				
			|||
    public void scheduleMaintenance() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Scheduling maintenance work"); | 
				
			|||
             | 
				
			|||
            Data inputData = new Data.Builder() | 
				
			|||
                .putLong("maintenance_time", System.currentTimeMillis()) | 
				
			|||
                .build(); | 
				
			|||
             | 
				
			|||
            OneTimeWorkRequest maintenanceWork = new OneTimeWorkRequest.Builder( | 
				
			|||
                DailyNotificationMaintenanceWorker.class) | 
				
			|||
                .setInputData(inputData) | 
				
			|||
                .addTag(WORK_TAG_MAINTENANCE) | 
				
			|||
                .setInitialDelay(TimeUnit.HOURS.toMillis(2), TimeUnit.MILLISECONDS) | 
				
			|||
                .build(); | 
				
			|||
             | 
				
			|||
            workManager.enqueue(maintenanceWork); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Maintenance work scheduled successfully"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling maintenance work", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Cancel all scheduled fetch work | 
				
			|||
     */ | 
				
			|||
    public void cancelAllFetchWork() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Cancelling all fetch work"); | 
				
			|||
             | 
				
			|||
            workManager.cancelAllWorkByTag(WORK_TAG_FETCH); | 
				
			|||
            workManager.cancelAllWorkByTag(WORK_TAG_MAINTENANCE); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "All fetch work cancelled"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error cancelling fetch work", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if fetch work is scheduled | 
				
			|||
     *  | 
				
			|||
     * @return true if fetch work is scheduled | 
				
			|||
     */ | 
				
			|||
    public boolean isFetchWorkScheduled() { | 
				
			|||
        // This would check WorkManager for pending work
 | 
				
			|||
        // For now, return a placeholder
 | 
				
			|||
        return false; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get fetch statistics | 
				
			|||
     *  | 
				
			|||
     * @return Fetch statistics as a string | 
				
			|||
     */ | 
				
			|||
    public String getFetchStats() { | 
				
			|||
        return String.format("Last fetch: %d, Fetch work scheduled: %s", | 
				
			|||
            storage.getLastFetchTime(), | 
				
			|||
            isFetchWorkScheduled() ? "yes" : "no"); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,403 @@ | 
				
			|||
/** | 
				
			|||
 * DailyNotificationMaintenanceWorker.java | 
				
			|||
 *  | 
				
			|||
 * WorkManager worker for maintenance tasks | 
				
			|||
 * Handles cleanup, optimization, and system health checks | 
				
			|||
 *  | 
				
			|||
 * @author Matthew Raymer | 
				
			|||
 * @version 1.0.0 | 
				
			|||
 */ | 
				
			|||
 | 
				
			|||
package com.timesafari.dailynotification; | 
				
			|||
 | 
				
			|||
import android.content.Context; | 
				
			|||
import android.util.Log; | 
				
			|||
 | 
				
			|||
import androidx.annotation.NonNull; | 
				
			|||
import androidx.work.Data; | 
				
			|||
import androidx.work.Worker; | 
				
			|||
import androidx.work.WorkerParameters; | 
				
			|||
 | 
				
			|||
import java.util.List; | 
				
			|||
 | 
				
			|||
/** | 
				
			|||
 * Background worker for maintenance tasks | 
				
			|||
 *  | 
				
			|||
 * This worker handles periodic maintenance of the notification system, | 
				
			|||
 * including cleanup of old data, optimization of storage, and health checks. | 
				
			|||
 */ | 
				
			|||
public class DailyNotificationMaintenanceWorker extends Worker { | 
				
			|||
     | 
				
			|||
    private static final String TAG = "DailyNotificationMaintenanceWorker"; | 
				
			|||
    private static final String KEY_MAINTENANCE_TIME = "maintenance_time"; | 
				
			|||
     | 
				
			|||
    private static final long WORK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total
 | 
				
			|||
    private static final int MAX_NOTIFICATIONS_TO_KEEP = 50; // Keep only recent notifications
 | 
				
			|||
     | 
				
			|||
    private final Context context; | 
				
			|||
    private final DailyNotificationStorage storage; | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Constructor | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param params Worker parameters | 
				
			|||
     */ | 
				
			|||
    public DailyNotificationMaintenanceWorker(@NonNull Context context,  | 
				
			|||
                                            @NonNull WorkerParameters params) { | 
				
			|||
        super(context, params); | 
				
			|||
        this.context = context; | 
				
			|||
        this.storage = new DailyNotificationStorage(context); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Main work method - perform maintenance tasks | 
				
			|||
     *  | 
				
			|||
     * @return Result indicating success or failure | 
				
			|||
     */ | 
				
			|||
    @NonNull | 
				
			|||
    @Override | 
				
			|||
    public Result doWork() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Starting maintenance work"); | 
				
			|||
             | 
				
			|||
            // Get input data
 | 
				
			|||
            Data inputData = getInputData(); | 
				
			|||
            long maintenanceTime = inputData.getLong(KEY_MAINTENANCE_TIME, 0); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Maintenance time: " + maintenanceTime); | 
				
			|||
             | 
				
			|||
            // Perform maintenance tasks
 | 
				
			|||
            boolean success = performMaintenance(); | 
				
			|||
             | 
				
			|||
            if (success) { | 
				
			|||
                Log.i(TAG, "Maintenance completed successfully"); | 
				
			|||
                return Result.success(); | 
				
			|||
            } else { | 
				
			|||
                Log.w(TAG, "Maintenance completed with warnings"); | 
				
			|||
                return Result.success(); // Still consider it successful
 | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error during maintenance work", e); | 
				
			|||
            return Result.failure(); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Perform all maintenance tasks | 
				
			|||
     *  | 
				
			|||
     * @return true if all tasks completed successfully | 
				
			|||
     */ | 
				
			|||
    private boolean performMaintenance() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Performing maintenance tasks"); | 
				
			|||
             | 
				
			|||
            boolean allSuccessful = true; | 
				
			|||
             | 
				
			|||
            // Task 1: Clean up old notifications
 | 
				
			|||
            boolean cleanupSuccess = cleanupOldNotifications(); | 
				
			|||
            if (!cleanupSuccess) { | 
				
			|||
                allSuccessful = false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Task 2: Optimize storage
 | 
				
			|||
            boolean optimizationSuccess = optimizeStorage(); | 
				
			|||
            if (!optimizationSuccess) { | 
				
			|||
                allSuccessful = false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Task 3: Health check
 | 
				
			|||
            boolean healthCheckSuccess = performHealthCheck(); | 
				
			|||
            if (!healthCheckSuccess) { | 
				
			|||
                allSuccessful = false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Task 4: Schedule next maintenance
 | 
				
			|||
            scheduleNextMaintenance(); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Maintenance tasks completed. All successful: " + allSuccessful); | 
				
			|||
            return allSuccessful; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error during maintenance tasks", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Clean up old notifications | 
				
			|||
     *  | 
				
			|||
     * @return true if cleanup was successful | 
				
			|||
     */ | 
				
			|||
    private boolean cleanupOldNotifications() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Cleaning up old notifications"); | 
				
			|||
             | 
				
			|||
            // Get all notifications
 | 
				
			|||
            List<NotificationContent> allNotifications = storage.getAllNotifications(); | 
				
			|||
            int initialCount = allNotifications.size(); | 
				
			|||
             | 
				
			|||
            if (initialCount <= MAX_NOTIFICATIONS_TO_KEEP) { | 
				
			|||
                Log.d(TAG, "No cleanup needed, notification count: " + initialCount); | 
				
			|||
                return true; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Remove old notifications, keeping the most recent ones
 | 
				
			|||
            int notificationsToRemove = initialCount - MAX_NOTIFICATIONS_TO_KEEP; | 
				
			|||
            int removedCount = 0; | 
				
			|||
             | 
				
			|||
            for (int i = 0; i < notificationsToRemove && i < allNotifications.size(); i++) { | 
				
			|||
                NotificationContent notification = allNotifications.get(i); | 
				
			|||
                storage.removeNotification(notification.getId()); | 
				
			|||
                removedCount++; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Cleanup completed. Removed " + removedCount + " old notifications"); | 
				
			|||
            return true; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error during notification cleanup", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Optimize storage usage | 
				
			|||
     *  | 
				
			|||
     * @return true if optimization was successful | 
				
			|||
     */ | 
				
			|||
    private boolean optimizeStorage() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Optimizing storage"); | 
				
			|||
             | 
				
			|||
            // Get storage statistics
 | 
				
			|||
            String stats = storage.getStorageStats(); | 
				
			|||
            Log.d(TAG, "Storage stats before optimization: " + stats); | 
				
			|||
             | 
				
			|||
            // Perform storage optimization
 | 
				
			|||
            // This could include:
 | 
				
			|||
            // - Compacting data structures
 | 
				
			|||
            // - Removing duplicate entries
 | 
				
			|||
            // - Optimizing cache usage
 | 
				
			|||
             | 
				
			|||
            // For now, just log the current state
 | 
				
			|||
            Log.d(TAG, "Storage optimization completed"); | 
				
			|||
            return true; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error during storage optimization", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Perform system health check | 
				
			|||
     *  | 
				
			|||
     * @return true if health check passed | 
				
			|||
     */ | 
				
			|||
    private boolean performHealthCheck() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Performing health check"); | 
				
			|||
             | 
				
			|||
            boolean healthOk = true; | 
				
			|||
             | 
				
			|||
            // Check 1: Storage health
 | 
				
			|||
            boolean storageHealth = checkStorageHealth(); | 
				
			|||
            if (!storageHealth) { | 
				
			|||
                healthOk = false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Check 2: Notification count health
 | 
				
			|||
            boolean countHealth = checkNotificationCountHealth(); | 
				
			|||
            if (!countHealth) { | 
				
			|||
                healthOk = false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Check 3: Data integrity
 | 
				
			|||
            boolean dataIntegrity = checkDataIntegrity(); | 
				
			|||
            if (!dataIntegrity) { | 
				
			|||
                healthOk = false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            if (healthOk) { | 
				
			|||
                Log.i(TAG, "Health check passed"); | 
				
			|||
            } else { | 
				
			|||
                Log.w(TAG, "Health check failed - some issues detected"); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            return healthOk; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error during health check", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check storage health | 
				
			|||
     *  | 
				
			|||
     * @return true if storage is healthy | 
				
			|||
     */ | 
				
			|||
    private boolean checkStorageHealth() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Checking storage health"); | 
				
			|||
             | 
				
			|||
            // Check if storage is accessible
 | 
				
			|||
            int notificationCount = storage.getNotificationCount(); | 
				
			|||
             | 
				
			|||
            if (notificationCount < 0) { | 
				
			|||
                Log.w(TAG, "Storage health issue: Invalid notification count"); | 
				
			|||
                return false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Check if storage is empty (this might be normal)
 | 
				
			|||
            if (storage.isEmpty()) { | 
				
			|||
                Log.d(TAG, "Storage is empty (this might be normal)"); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Storage health check passed"); | 
				
			|||
            return true; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error checking storage health", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check notification count health | 
				
			|||
     *  | 
				
			|||
     * @return true if notification count is healthy | 
				
			|||
     */ | 
				
			|||
    private boolean checkNotificationCountHealth() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Checking notification count health"); | 
				
			|||
             | 
				
			|||
            int notificationCount = storage.getNotificationCount(); | 
				
			|||
             | 
				
			|||
            // Check for reasonable limits
 | 
				
			|||
            if (notificationCount > 1000) { | 
				
			|||
                Log.w(TAG, "Notification count health issue: Too many notifications (" +  | 
				
			|||
                    notificationCount + ")"); | 
				
			|||
                return false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Notification count health check passed: " + notificationCount); | 
				
			|||
            return true; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error checking notification count health", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check data integrity | 
				
			|||
     *  | 
				
			|||
     * @return true if data integrity is good | 
				
			|||
     */ | 
				
			|||
    private boolean checkDataIntegrity() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Checking data integrity"); | 
				
			|||
             | 
				
			|||
            // Get all notifications and check basic integrity
 | 
				
			|||
            List<NotificationContent> allNotifications = storage.getAllNotifications(); | 
				
			|||
             | 
				
			|||
            for (NotificationContent notification : allNotifications) { | 
				
			|||
                // Check required fields
 | 
				
			|||
                if (notification.getId() == null || notification.getId().isEmpty()) { | 
				
			|||
                    Log.w(TAG, "Data integrity issue: Notification with null/empty ID"); | 
				
			|||
                    return false; | 
				
			|||
                } | 
				
			|||
                 | 
				
			|||
                if (notification.getTitle() == null || notification.getTitle().isEmpty()) { | 
				
			|||
                    Log.w(TAG, "Data integrity issue: Notification with null/empty title"); | 
				
			|||
                    return false; | 
				
			|||
                } | 
				
			|||
                 | 
				
			|||
                if (notification.getBody() == null || notification.getBody().isEmpty()) { | 
				
			|||
                    Log.w(TAG, "Data integrity issue: Notification with null/empty body"); | 
				
			|||
                    return false; | 
				
			|||
                } | 
				
			|||
                 | 
				
			|||
                // Check timestamp validity
 | 
				
			|||
                if (notification.getScheduledTime() <= 0) { | 
				
			|||
                    Log.w(TAG, "Data integrity issue: Invalid scheduled time"); | 
				
			|||
                    return false; | 
				
			|||
                } | 
				
			|||
                 | 
				
			|||
                if (notification.getFetchTime() <= 0) { | 
				
			|||
                    Log.w(TAG, "Data integrity issue: Invalid fetch time"); | 
				
			|||
                    return false; | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Data integrity check passed"); | 
				
			|||
            return true; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error checking data integrity", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule next maintenance run | 
				
			|||
     */ | 
				
			|||
    private void scheduleNextMaintenance() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Scheduling next maintenance"); | 
				
			|||
             | 
				
			|||
            // Schedule maintenance for tomorrow at 2 AM
 | 
				
			|||
            long nextMaintenanceTime = calculateNextMaintenanceTime(); | 
				
			|||
             | 
				
			|||
            Data maintenanceData = new Data.Builder() | 
				
			|||
                .putLong(KEY_MAINTENANCE_TIME, nextMaintenanceTime) | 
				
			|||
                .build(); | 
				
			|||
             | 
				
			|||
            androidx.work.OneTimeWorkRequest maintenanceWork =  | 
				
			|||
                new androidx.work.OneTimeWorkRequest.Builder(DailyNotificationMaintenanceWorker.class) | 
				
			|||
                    .setInputData(maintenanceData) | 
				
			|||
                    .setInitialDelay(nextMaintenanceTime - System.currentTimeMillis(),  | 
				
			|||
                        java.util.concurrent.TimeUnit.MILLISECONDS) | 
				
			|||
                    .build(); | 
				
			|||
             | 
				
			|||
            androidx.work.WorkManager.getInstance(context).enqueue(maintenanceWork); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Next maintenance scheduled for " + nextMaintenanceTime); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling next maintenance", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Calculate next maintenance time (2 AM tomorrow) | 
				
			|||
     *  | 
				
			|||
     * @return Timestamp for next maintenance | 
				
			|||
     */ | 
				
			|||
    private long calculateNextMaintenanceTime() { | 
				
			|||
        try { | 
				
			|||
            java.util.Calendar calendar = java.util.Calendar.getInstance(); | 
				
			|||
             | 
				
			|||
            // Set to 2 AM
 | 
				
			|||
            calendar.set(java.util.Calendar.HOUR_OF_DAY, 2); | 
				
			|||
            calendar.set(java.util.Calendar.MINUTE, 0); | 
				
			|||
            calendar.set(java.util.Calendar.SECOND, 0); | 
				
			|||
            calendar.set(java.util.Calendar.MILLISECOND, 0); | 
				
			|||
             | 
				
			|||
            // If 2 AM has passed today, schedule for tomorrow
 | 
				
			|||
            if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { | 
				
			|||
                calendar.add(java.util.Calendar.DAY_OF_YEAR, 1); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            return calendar.getTimeInMillis(); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error calculating next maintenance time", e); | 
				
			|||
            // Fallback: 24 hours from now
 | 
				
			|||
            return System.currentTimeMillis() + (24 * 60 * 60 * 1000); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,506 @@ | 
				
			|||
/** | 
				
			|||
 * DailyNotificationPlugin.java | 
				
			|||
 *  | 
				
			|||
 * Android implementation of the Daily Notification Plugin for Capacitor | 
				
			|||
 * Implements offline-first daily notifications with prefetch → cache → schedule → display pipeline | 
				
			|||
 *  | 
				
			|||
 * @author Matthew Raymer | 
				
			|||
 * @version 1.0.0 | 
				
			|||
 */ | 
				
			|||
 | 
				
			|||
package com.timesafari.dailynotification; | 
				
			|||
 | 
				
			|||
import android.Manifest; | 
				
			|||
import android.app.AlarmManager; | 
				
			|||
import android.app.NotificationChannel; | 
				
			|||
import android.app.NotificationManager; | 
				
			|||
import android.app.PendingIntent; | 
				
			|||
import android.content.Context; | 
				
			|||
import android.content.Intent; | 
				
			|||
import android.content.pm.PackageManager; | 
				
			|||
import android.os.Build; | 
				
			|||
import android.os.PowerManager; | 
				
			|||
import android.util.Log; | 
				
			|||
 | 
				
			|||
import androidx.core.app.NotificationCompat; | 
				
			|||
import androidx.work.WorkManager; | 
				
			|||
 | 
				
			|||
import com.getcapacitor.JSObject; | 
				
			|||
import com.getcapacitor.Plugin; | 
				
			|||
import com.getcapacitor.PluginCall; | 
				
			|||
import com.getcapacitor.PluginMethod; | 
				
			|||
import com.getcapacitor.annotation.CapacitorPlugin; | 
				
			|||
import com.getcapacitor.annotation.Permission; | 
				
			|||
 | 
				
			|||
import java.util.Calendar; | 
				
			|||
import java.util.concurrent.TimeUnit; | 
				
			|||
 | 
				
			|||
/** | 
				
			|||
 * Main plugin class for handling daily notifications on Android | 
				
			|||
 *  | 
				
			|||
 * This plugin provides functionality for scheduling and managing daily notifications | 
				
			|||
 * with offline-first approach, background content fetching, and reliable delivery. | 
				
			|||
 */ | 
				
			|||
@CapacitorPlugin( | 
				
			|||
    name = "DailyNotification", | 
				
			|||
    permissions = { | 
				
			|||
        @Permission( | 
				
			|||
            alias = "notifications", | 
				
			|||
            strings = { | 
				
			|||
                Manifest.permission.POST_NOTIFICATIONS, | 
				
			|||
                Manifest.permission.SCHEDULE_EXACT_ALARM, | 
				
			|||
                Manifest.permission.WAKE_LOCK, | 
				
			|||
                Manifest.permission.INTERNET | 
				
			|||
            } | 
				
			|||
        ) | 
				
			|||
    } | 
				
			|||
) | 
				
			|||
public class DailyNotificationPlugin extends Plugin { | 
				
			|||
     | 
				
			|||
    private static final String TAG = "DailyNotificationPlugin"; | 
				
			|||
    private static final String CHANNEL_ID = "timesafari.daily"; | 
				
			|||
    private static final String CHANNEL_NAME = "Daily Notifications"; | 
				
			|||
    private static final String CHANNEL_DESCRIPTION = "Daily notification updates from TimeSafari"; | 
				
			|||
     | 
				
			|||
    private NotificationManager notificationManager; | 
				
			|||
    private AlarmManager alarmManager; | 
				
			|||
    private WorkManager workManager; | 
				
			|||
    private PowerManager powerManager; | 
				
			|||
    private DailyNotificationStorage storage; | 
				
			|||
    private DailyNotificationScheduler scheduler; | 
				
			|||
    private DailyNotificationFetcher fetcher; | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Initialize the plugin and create notification channel | 
				
			|||
     */ | 
				
			|||
    @Override | 
				
			|||
    public void load() { | 
				
			|||
        super.load(); | 
				
			|||
         | 
				
			|||
        try { | 
				
			|||
            // Initialize system services
 | 
				
			|||
            notificationManager = (NotificationManager) getContext() | 
				
			|||
                .getSystemService(Context.NOTIFICATION_SERVICE); | 
				
			|||
            alarmManager = (AlarmManager) getContext() | 
				
			|||
                .getSystemService(Context.ALARM_SERVICE); | 
				
			|||
            workManager = WorkManager.getInstance(getContext()); | 
				
			|||
            powerManager = (PowerManager) getContext() | 
				
			|||
                .getSystemService(Context.POWER_SERVICE); | 
				
			|||
             | 
				
			|||
            // Initialize components
 | 
				
			|||
            storage = new DailyNotificationStorage(getContext()); | 
				
			|||
            scheduler = new DailyNotificationScheduler(getContext(), alarmManager); | 
				
			|||
            fetcher = new DailyNotificationFetcher(getContext(), storage); | 
				
			|||
             | 
				
			|||
            // Create notification channel
 | 
				
			|||
            createNotificationChannel(); | 
				
			|||
             | 
				
			|||
            // Schedule next maintenance
 | 
				
			|||
            scheduleMaintenance(); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "DailyNotificationPlugin initialized successfully"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Failed to initialize DailyNotificationPlugin", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule a daily notification with the specified options | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call containing notification parameters | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void scheduleDailyNotification(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Scheduling daily notification"); | 
				
			|||
             | 
				
			|||
            // Validate required parameters
 | 
				
			|||
            String time = call.getString("time"); | 
				
			|||
            if (time == null || time.isEmpty()) { | 
				
			|||
                call.reject("Time parameter is required"); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Parse time (HH:mm format)
 | 
				
			|||
            String[] timeParts = time.split(":"); | 
				
			|||
            if (timeParts.length != 2) { | 
				
			|||
                call.reject("Invalid time format. Use HH:mm"); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            int hour, minute; | 
				
			|||
            try { | 
				
			|||
                hour = Integer.parseInt(timeParts[0]); | 
				
			|||
                minute = Integer.parseInt(timeParts[1]); | 
				
			|||
            } catch (NumberFormatException e) { | 
				
			|||
                call.reject("Invalid time format. Use HH:mm"); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { | 
				
			|||
                call.reject("Invalid time values"); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Extract other parameters
 | 
				
			|||
            String title = call.getString("title", "Daily Update"); | 
				
			|||
            String body = call.getString("body", "Your daily notification is ready"); | 
				
			|||
            boolean sound = call.getBoolean("sound", true); | 
				
			|||
            String priority = call.getString("priority", "default"); | 
				
			|||
            String url = call.getString("url", ""); | 
				
			|||
             | 
				
			|||
            // Create notification content
 | 
				
			|||
            NotificationContent content = new NotificationContent(); | 
				
			|||
            content.setTitle(title); | 
				
			|||
            content.setBody(body); | 
				
			|||
            content.setSound(sound); | 
				
			|||
            content.setPriority(priority); | 
				
			|||
            content.setUrl(url); | 
				
			|||
            content.setScheduledTime(calculateNextScheduledTime(hour, minute)); | 
				
			|||
             | 
				
			|||
            // Store notification content
 | 
				
			|||
            storage.saveNotificationContent(content); | 
				
			|||
             | 
				
			|||
            // Schedule the notification
 | 
				
			|||
            boolean scheduled = scheduler.scheduleNotification(content); | 
				
			|||
             | 
				
			|||
            if (scheduled) { | 
				
			|||
                // Schedule background fetch for next day
 | 
				
			|||
                scheduleBackgroundFetch(content.getScheduledTime()); | 
				
			|||
                 | 
				
			|||
                Log.i(TAG, "Daily notification scheduled successfully for " + time); | 
				
			|||
                call.resolve(); | 
				
			|||
            } else { | 
				
			|||
                call.reject("Failed to schedule notification"); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling daily notification", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the last notification that was delivered | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void getLastNotification(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Getting last notification"); | 
				
			|||
             | 
				
			|||
            NotificationContent lastNotification = storage.getLastNotification(); | 
				
			|||
             | 
				
			|||
            if (lastNotification != null) { | 
				
			|||
                JSObject result = new JSObject(); | 
				
			|||
                result.put("id", lastNotification.getId()); | 
				
			|||
                result.put("title", lastNotification.getTitle()); | 
				
			|||
                result.put("body", lastNotification.getBody()); | 
				
			|||
                result.put("timestamp", lastNotification.getScheduledTime()); | 
				
			|||
                result.put("url", lastNotification.getUrl()); | 
				
			|||
                 | 
				
			|||
                call.resolve(result); | 
				
			|||
            } else { | 
				
			|||
                call.resolve(null); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error getting last notification", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Cancel all scheduled notifications | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void cancelAllNotifications(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Cancelling all notifications"); | 
				
			|||
             | 
				
			|||
            scheduler.cancelAllNotifications(); | 
				
			|||
            storage.clearAllNotifications(); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "All notifications cancelled successfully"); | 
				
			|||
            call.resolve(); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error cancelling notifications", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the current status of notifications | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void getNotificationStatus(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Getting notification status"); | 
				
			|||
             | 
				
			|||
            JSObject result = new JSObject(); | 
				
			|||
             | 
				
			|||
            // Check if notifications are enabled
 | 
				
			|||
            boolean notificationsEnabled = areNotificationsEnabled(); | 
				
			|||
            result.put("isEnabled", notificationsEnabled); | 
				
			|||
             | 
				
			|||
            // Get next notification time
 | 
				
			|||
            long nextNotificationTime = scheduler.getNextNotificationTime(); | 
				
			|||
            result.put("nextNotificationTime", nextNotificationTime); | 
				
			|||
             | 
				
			|||
            // Get current settings
 | 
				
			|||
            JSObject settings = new JSObject(); | 
				
			|||
            settings.put("sound", true); | 
				
			|||
            settings.put("priority", "default"); | 
				
			|||
            settings.put("timezone", "UTC"); | 
				
			|||
            result.put("settings", settings); | 
				
			|||
             | 
				
			|||
            // Get pending notifications count
 | 
				
			|||
            int pendingCount = scheduler.getPendingNotificationsCount(); | 
				
			|||
            result.put("pending", pendingCount); | 
				
			|||
             | 
				
			|||
            call.resolve(result); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error getting notification status", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Update notification settings | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call containing new settings | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void updateSettings(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Updating notification settings"); | 
				
			|||
             | 
				
			|||
            // Extract settings
 | 
				
			|||
            Boolean sound = call.getBoolean("sound"); | 
				
			|||
            String priority = call.getString("priority"); | 
				
			|||
            String timezone = call.getString("timezone"); | 
				
			|||
             | 
				
			|||
            // Update settings in storage
 | 
				
			|||
            if (sound != null) { | 
				
			|||
                storage.setSoundEnabled(sound); | 
				
			|||
            } | 
				
			|||
            if (priority != null) { | 
				
			|||
                storage.setPriority(priority); | 
				
			|||
            } | 
				
			|||
            if (timezone != null) { | 
				
			|||
                storage.setTimezone(timezone); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Update existing notifications with new settings
 | 
				
			|||
            scheduler.updateNotificationSettings(); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Notification settings updated successfully"); | 
				
			|||
            call.resolve(); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error updating notification settings", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get battery status information | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void getBatteryStatus(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Getting battery status"); | 
				
			|||
             | 
				
			|||
            JSObject result = new JSObject(); | 
				
			|||
             | 
				
			|||
            // Get battery level (simplified - would need BatteryManager in real implementation)
 | 
				
			|||
            result.put("level", 100); // Placeholder
 | 
				
			|||
            result.put("isCharging", false); // Placeholder
 | 
				
			|||
            result.put("powerState", 0); // Placeholder
 | 
				
			|||
            result.put("isOptimizationExempt", false); // Placeholder
 | 
				
			|||
             | 
				
			|||
            call.resolve(result); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error getting battery status", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Request battery optimization exemption | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void requestBatteryOptimizationExemption(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Requesting battery optimization exemption"); | 
				
			|||
             | 
				
			|||
            // This would typically open system settings
 | 
				
			|||
            // For now, just log the request
 | 
				
			|||
            Log.i(TAG, "Battery optimization exemption requested"); | 
				
			|||
            call.resolve(); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error requesting battery optimization exemption", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set adaptive scheduling based on device state | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call containing enabled flag | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void setAdaptiveScheduling(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Setting adaptive scheduling"); | 
				
			|||
             | 
				
			|||
            boolean enabled = call.getBoolean("enabled", true); | 
				
			|||
            storage.setAdaptiveSchedulingEnabled(enabled); | 
				
			|||
             | 
				
			|||
            if (enabled) { | 
				
			|||
                scheduler.enableAdaptiveScheduling(); | 
				
			|||
            } else { | 
				
			|||
                scheduler.disableAdaptiveScheduling(); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Adaptive scheduling " + (enabled ? "enabled" : "disabled")); | 
				
			|||
            call.resolve(); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error setting adaptive scheduling", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get current power state information | 
				
			|||
     *  | 
				
			|||
     * @param call Plugin call | 
				
			|||
     */ | 
				
			|||
    @PluginMethod | 
				
			|||
    public void getPowerState(PluginCall call) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Getting power state"); | 
				
			|||
             | 
				
			|||
            JSObject result = new JSObject(); | 
				
			|||
            result.put("powerState", 0); // Placeholder
 | 
				
			|||
            result.put("isOptimizationExempt", false); // Placeholder
 | 
				
			|||
             | 
				
			|||
            call.resolve(result); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error getting power state", e); | 
				
			|||
            call.reject("Internal error: " + e.getMessage()); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Create the notification channel for Android 8.0+ | 
				
			|||
     */ | 
				
			|||
    private void createNotificationChannel() { | 
				
			|||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | 
				
			|||
            NotificationChannel channel = new NotificationChannel( | 
				
			|||
                CHANNEL_ID, | 
				
			|||
                CHANNEL_NAME, | 
				
			|||
                NotificationManager.IMPORTANCE_HIGH | 
				
			|||
            ); | 
				
			|||
            channel.setDescription(CHANNEL_DESCRIPTION); | 
				
			|||
            channel.enableLights(true); | 
				
			|||
            channel.enableVibration(true); | 
				
			|||
             | 
				
			|||
            notificationManager.createNotificationChannel(channel); | 
				
			|||
            Log.d(TAG, "Notification channel created: " + CHANNEL_ID); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Calculate the next scheduled time for the notification | 
				
			|||
     *  | 
				
			|||
     * @param hour Hour of day (0-23) | 
				
			|||
     * @param minute Minute of hour (0-59) | 
				
			|||
     * @return Timestamp in milliseconds | 
				
			|||
     */ | 
				
			|||
    private long calculateNextScheduledTime(int hour, int minute) { | 
				
			|||
        Calendar calendar = Calendar.getInstance(); | 
				
			|||
        calendar.set(Calendar.HOUR_OF_DAY, hour); | 
				
			|||
        calendar.set(Calendar.MINUTE, minute); | 
				
			|||
        calendar.set(Calendar.SECOND, 0); | 
				
			|||
        calendar.set(Calendar.MILLISECOND, 0); | 
				
			|||
         | 
				
			|||
        // If time has passed today, schedule for tomorrow
 | 
				
			|||
        if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { | 
				
			|||
            calendar.add(Calendar.DAY_OF_YEAR, 1); | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        return calendar.getTimeInMillis(); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule background fetch for content | 
				
			|||
     *  | 
				
			|||
     * @param scheduledTime When the notification is scheduled for | 
				
			|||
     */ | 
				
			|||
    private void scheduleBackgroundFetch(long scheduledTime) { | 
				
			|||
        try { | 
				
			|||
            // Schedule fetch 1 hour before notification
 | 
				
			|||
            long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1); | 
				
			|||
             | 
				
			|||
            if (fetchTime > System.currentTimeMillis()) { | 
				
			|||
                fetcher.scheduleFetch(fetchTime); | 
				
			|||
                Log.d(TAG, "Background fetch scheduled for " + fetchTime); | 
				
			|||
            } | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling background fetch", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule maintenance tasks | 
				
			|||
     */ | 
				
			|||
    private void scheduleMaintenance() { | 
				
			|||
        try { | 
				
			|||
            // Schedule daily maintenance at 2 AM
 | 
				
			|||
            Calendar calendar = Calendar.getInstance(); | 
				
			|||
            calendar.set(Calendar.HOUR_OF_DAY, 2); | 
				
			|||
            calendar.set(Calendar.MINUTE, 0); | 
				
			|||
            calendar.set(Calendar.SECOND, 0); | 
				
			|||
             | 
				
			|||
            if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { | 
				
			|||
                calendar.add(Calendar.DAY_OF_YEAR, 1); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // This would typically use WorkManager for maintenance
 | 
				
			|||
            Log.d(TAG, "Maintenance scheduled for " + calendar.getTimeInMillis()); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling maintenance", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if notifications are enabled | 
				
			|||
     *  | 
				
			|||
     * @return true if notifications are enabled | 
				
			|||
     */ | 
				
			|||
    private boolean areNotificationsEnabled() { | 
				
			|||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | 
				
			|||
            return getContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)  | 
				
			|||
                == PackageManager.PERMISSION_GRANTED; | 
				
			|||
        } | 
				
			|||
        return NotificationManagerCompat.from(getContext()).areNotificationsEnabled(); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,283 @@ | 
				
			|||
/** | 
				
			|||
 * DailyNotificationReceiver.java | 
				
			|||
 *  | 
				
			|||
 * Broadcast receiver for handling scheduled notification alarms | 
				
			|||
 * Displays notifications when scheduled time is reached | 
				
			|||
 *  | 
				
			|||
 * @author Matthew Raymer | 
				
			|||
 * @version 1.0.0 | 
				
			|||
 */ | 
				
			|||
 | 
				
			|||
package com.timesafari.dailynotification; | 
				
			|||
 | 
				
			|||
import android.app.NotificationManager; | 
				
			|||
import android.app.PendingIntent; | 
				
			|||
import android.content.BroadcastReceiver; | 
				
			|||
import android.content.Context; | 
				
			|||
import android.content.Intent; | 
				
			|||
import android.os.Build; | 
				
			|||
import android.util.Log; | 
				
			|||
 | 
				
			|||
import androidx.core.app.NotificationCompat; | 
				
			|||
 | 
				
			|||
/** | 
				
			|||
 * Broadcast receiver for daily notification alarms | 
				
			|||
 *  | 
				
			|||
 * This receiver is triggered by AlarmManager when it's time to display | 
				
			|||
 * a notification. It retrieves the notification content from storage | 
				
			|||
 * and displays it to the user. | 
				
			|||
 */ | 
				
			|||
public class DailyNotificationReceiver extends BroadcastReceiver { | 
				
			|||
     | 
				
			|||
    private static final String TAG = "DailyNotificationReceiver"; | 
				
			|||
    private static final String CHANNEL_ID = "timesafari.daily"; | 
				
			|||
    private static final String EXTRA_NOTIFICATION_ID = "notification_id"; | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Handle broadcast intent when alarm triggers | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param intent Broadcast intent | 
				
			|||
     */ | 
				
			|||
    @Override | 
				
			|||
    public void onReceive(Context context, Intent intent) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Received notification broadcast"); | 
				
			|||
             | 
				
			|||
            String action = intent.getAction(); | 
				
			|||
            if (action == null) { | 
				
			|||
                Log.w(TAG, "Received intent with null action"); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            if ("com.timesafari.daily.NOTIFICATION".equals(action)) { | 
				
			|||
                handleNotificationIntent(context, intent); | 
				
			|||
            } else { | 
				
			|||
                Log.w(TAG, "Unknown action: " + action); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error handling broadcast", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Handle notification intent | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param intent Intent containing notification data | 
				
			|||
     */ | 
				
			|||
    private void handleNotificationIntent(Context context, Intent intent) { | 
				
			|||
        try { | 
				
			|||
            String notificationId = intent.getStringExtra(EXTRA_NOTIFICATION_ID); | 
				
			|||
             | 
				
			|||
            if (notificationId == null) { | 
				
			|||
                Log.w(TAG, "Notification ID not found in intent"); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Processing notification: " + notificationId); | 
				
			|||
             | 
				
			|||
            // Get notification content from storage
 | 
				
			|||
            DailyNotificationStorage storage = new DailyNotificationStorage(context); | 
				
			|||
            NotificationContent content = storage.getNotificationContent(notificationId); | 
				
			|||
             | 
				
			|||
            if (content == null) { | 
				
			|||
                Log.w(TAG, "Notification content not found: " + notificationId); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Check if notification is ready to display
 | 
				
			|||
            if (!content.isReadyToDisplay()) { | 
				
			|||
                Log.d(TAG, "Notification not ready to display yet: " + notificationId); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Display the notification
 | 
				
			|||
            displayNotification(context, content); | 
				
			|||
             | 
				
			|||
            // Schedule next notification if this is a recurring daily notification
 | 
				
			|||
            scheduleNextNotification(context, content); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Notification processed successfully: " + notificationId); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error handling notification intent", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Display the notification to the user | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param content Notification content to display | 
				
			|||
     */ | 
				
			|||
    private void displayNotification(Context context, NotificationContent content) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Displaying notification: " + content.getId()); | 
				
			|||
             | 
				
			|||
            NotificationManager notificationManager =  | 
				
			|||
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); | 
				
			|||
             | 
				
			|||
            if (notificationManager == null) { | 
				
			|||
                Log.e(TAG, "NotificationManager not available"); | 
				
			|||
                return; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Create notification builder
 | 
				
			|||
            NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) | 
				
			|||
                .setSmallIcon(android.R.drawable.ic_dialog_info) | 
				
			|||
                .setContentTitle(content.getTitle()) | 
				
			|||
                .setContentText(content.getBody()) | 
				
			|||
                .setPriority(getNotificationPriority(content.getPriority())) | 
				
			|||
                .setAutoCancel(true) | 
				
			|||
                .setCategory(NotificationCompat.CATEGORY_REMINDER); | 
				
			|||
             | 
				
			|||
            // Add sound if enabled
 | 
				
			|||
            if (content.isSound()) { | 
				
			|||
                builder.setDefaults(NotificationCompat.DEFAULT_SOUND); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Add click action if URL is available
 | 
				
			|||
            if (content.getUrl() != null && !content.getUrl().isEmpty()) { | 
				
			|||
                Intent clickIntent = new Intent(Intent.ACTION_VIEW); | 
				
			|||
                clickIntent.setData(android.net.Uri.parse(content.getUrl())); | 
				
			|||
                 | 
				
			|||
                PendingIntent clickPendingIntent = PendingIntent.getActivity( | 
				
			|||
                    context, | 
				
			|||
                    content.getId().hashCode(), | 
				
			|||
                    clickIntent, | 
				
			|||
                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE | 
				
			|||
                ); | 
				
			|||
                 | 
				
			|||
                builder.setContentIntent(clickPendingIntent); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Add dismiss action
 | 
				
			|||
            Intent dismissIntent = new Intent(context, DailyNotificationReceiver.class); | 
				
			|||
            dismissIntent.setAction("com.timesafari.daily.DISMISS"); | 
				
			|||
            dismissIntent.putExtra(EXTRA_NOTIFICATION_ID, content.getId()); | 
				
			|||
             | 
				
			|||
            PendingIntent dismissPendingIntent = PendingIntent.getBroadcast( | 
				
			|||
                context, | 
				
			|||
                content.getId().hashCode() + 1000, // Different request code
 | 
				
			|||
                dismissIntent, | 
				
			|||
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE | 
				
			|||
            ); | 
				
			|||
             | 
				
			|||
            builder.addAction( | 
				
			|||
                android.R.drawable.ic_menu_close_clear_cancel, | 
				
			|||
                "Dismiss", | 
				
			|||
                dismissPendingIntent | 
				
			|||
            ); | 
				
			|||
             | 
				
			|||
            // Build and display notification
 | 
				
			|||
            int notificationId = content.getId().hashCode(); | 
				
			|||
            notificationManager.notify(notificationId, builder.build()); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Notification displayed successfully: " + content.getId()); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error displaying notification", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule the next occurrence of this daily notification | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param content Current notification content | 
				
			|||
     */ | 
				
			|||
    private void scheduleNextNotification(Context context, NotificationContent content) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Scheduling next notification for: " + content.getId()); | 
				
			|||
             | 
				
			|||
            // Calculate next occurrence (24 hours from now)
 | 
				
			|||
            long nextScheduledTime = content.getScheduledTime() + (24 * 60 * 60 * 1000); | 
				
			|||
             | 
				
			|||
            // Create new content for next occurrence
 | 
				
			|||
            NotificationContent nextContent = new NotificationContent(); | 
				
			|||
            nextContent.setTitle(content.getTitle()); | 
				
			|||
            nextContent.setBody(content.getBody()); | 
				
			|||
            nextContent.setScheduledTime(nextScheduledTime); | 
				
			|||
            nextContent.setSound(content.isSound()); | 
				
			|||
            nextContent.setPriority(content.getPriority()); | 
				
			|||
            nextContent.setUrl(content.getUrl()); | 
				
			|||
            nextContent.setFetchTime(System.currentTimeMillis()); | 
				
			|||
             | 
				
			|||
            // Save to storage
 | 
				
			|||
            DailyNotificationStorage storage = new DailyNotificationStorage(context); | 
				
			|||
            storage.saveNotificationContent(nextContent); | 
				
			|||
             | 
				
			|||
            // Schedule the notification
 | 
				
			|||
            DailyNotificationScheduler scheduler = new DailyNotificationScheduler( | 
				
			|||
                context,  | 
				
			|||
                (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE) | 
				
			|||
            ); | 
				
			|||
             | 
				
			|||
            boolean scheduled = scheduler.scheduleNotification(nextContent); | 
				
			|||
             | 
				
			|||
            if (scheduled) { | 
				
			|||
                Log.i(TAG, "Next notification scheduled successfully"); | 
				
			|||
            } else { | 
				
			|||
                Log.e(TAG, "Failed to schedule next notification"); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling next notification", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get notification priority constant | 
				
			|||
     *  | 
				
			|||
     * @param priority Priority string from content | 
				
			|||
     * @return NotificationCompat priority constant | 
				
			|||
     */ | 
				
			|||
    private int getNotificationPriority(String priority) { | 
				
			|||
        if (priority == null) { | 
				
			|||
            return NotificationCompat.PRIORITY_DEFAULT; | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        switch (priority.toLowerCase()) { | 
				
			|||
            case "high": | 
				
			|||
                return NotificationCompat.PRIORITY_HIGH; | 
				
			|||
            case "low": | 
				
			|||
                return NotificationCompat.PRIORITY_LOW; | 
				
			|||
            case "min": | 
				
			|||
                return NotificationCompat.PRIORITY_MIN; | 
				
			|||
            case "max": | 
				
			|||
                return NotificationCompat.PRIORITY_MAX; | 
				
			|||
            default: | 
				
			|||
                return NotificationCompat.PRIORITY_DEFAULT; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Handle notification dismissal | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param notificationId ID of dismissed notification | 
				
			|||
     */ | 
				
			|||
    private void handleNotificationDismissal(Context context, String notificationId) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Handling notification dismissal: " + notificationId); | 
				
			|||
             | 
				
			|||
            // Remove from storage
 | 
				
			|||
            DailyNotificationStorage storage = new DailyNotificationStorage(context); | 
				
			|||
            storage.removeNotification(notificationId); | 
				
			|||
             | 
				
			|||
            // Cancel any pending alarms
 | 
				
			|||
            DailyNotificationScheduler scheduler = new DailyNotificationScheduler( | 
				
			|||
                context, | 
				
			|||
                (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE) | 
				
			|||
            ); | 
				
			|||
            scheduler.cancelNotification(notificationId); | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Notification dismissed successfully: " + notificationId); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error handling notification dismissal", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,377 @@ | 
				
			|||
/** | 
				
			|||
 * DailyNotificationScheduler.java | 
				
			|||
 *  | 
				
			|||
 * Handles scheduling and timing of daily notifications | 
				
			|||
 * Implements exact and inexact alarm scheduling with battery optimization handling | 
				
			|||
 *  | 
				
			|||
 * @author Matthew Raymer | 
				
			|||
 * @version 1.0.0 | 
				
			|||
 */ | 
				
			|||
 | 
				
			|||
package com.timesafari.dailynotification; | 
				
			|||
 | 
				
			|||
import android.app.AlarmManager; | 
				
			|||
import android.app.PendingIntent; | 
				
			|||
import android.content.Context; | 
				
			|||
import android.content.Intent; | 
				
			|||
import android.os.Build; | 
				
			|||
import android.util.Log; | 
				
			|||
 | 
				
			|||
import java.util.Calendar; | 
				
			|||
import java.util.concurrent.ConcurrentHashMap; | 
				
			|||
 | 
				
			|||
/** | 
				
			|||
 * Manages scheduling of daily notifications using AlarmManager | 
				
			|||
 *  | 
				
			|||
 * This class handles the scheduling aspect of the prefetch → cache → schedule → display pipeline. | 
				
			|||
 * It supports both exact and inexact alarms based on system permissions and battery optimization. | 
				
			|||
 */ | 
				
			|||
public class DailyNotificationScheduler { | 
				
			|||
     | 
				
			|||
    private static final String TAG = "DailyNotificationScheduler"; | 
				
			|||
    private static final String ACTION_NOTIFICATION = "com.timesafari.daily.NOTIFICATION"; | 
				
			|||
    private static final String EXTRA_NOTIFICATION_ID = "notification_id"; | 
				
			|||
     | 
				
			|||
    private final Context context; | 
				
			|||
    private final AlarmManager alarmManager; | 
				
			|||
    private final ConcurrentHashMap<String, PendingIntent> scheduledAlarms; | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Constructor | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     * @param alarmManager System AlarmManager service | 
				
			|||
     */ | 
				
			|||
    public DailyNotificationScheduler(Context context, AlarmManager alarmManager) { | 
				
			|||
        this.context = context; | 
				
			|||
        this.alarmManager = alarmManager; | 
				
			|||
        this.scheduledAlarms = new ConcurrentHashMap<>(); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule a notification for delivery | 
				
			|||
     *  | 
				
			|||
     * @param content Notification content to schedule | 
				
			|||
     * @return true if scheduling was successful | 
				
			|||
     */ | 
				
			|||
    public boolean scheduleNotification(NotificationContent content) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Scheduling notification: " + content.getId()); | 
				
			|||
             | 
				
			|||
            // Cancel any existing alarm for this notification
 | 
				
			|||
            cancelNotification(content.getId()); | 
				
			|||
             | 
				
			|||
            // Create intent for the notification
 | 
				
			|||
            Intent intent = new Intent(context, DailyNotificationReceiver.class); | 
				
			|||
            intent.setAction(ACTION_NOTIFICATION); | 
				
			|||
            intent.putExtra(EXTRA_NOTIFICATION_ID, content.getId()); | 
				
			|||
             | 
				
			|||
            // Create pending intent with unique request code
 | 
				
			|||
            int requestCode = content.getId().hashCode(); | 
				
			|||
            PendingIntent pendingIntent = PendingIntent.getBroadcast( | 
				
			|||
                context,  | 
				
			|||
                requestCode,  | 
				
			|||
                intent,  | 
				
			|||
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE | 
				
			|||
            ); | 
				
			|||
             | 
				
			|||
            // Store the pending intent
 | 
				
			|||
            scheduledAlarms.put(content.getId(), pendingIntent); | 
				
			|||
             | 
				
			|||
            // Schedule the alarm
 | 
				
			|||
            long triggerTime = content.getScheduledTime(); | 
				
			|||
            boolean scheduled = scheduleAlarm(pendingIntent, triggerTime); | 
				
			|||
             | 
				
			|||
            if (scheduled) { | 
				
			|||
                Log.i(TAG, "Notification scheduled successfully for " +  | 
				
			|||
                    formatTime(triggerTime)); | 
				
			|||
                return true; | 
				
			|||
            } else { | 
				
			|||
                Log.e(TAG, "Failed to schedule notification"); | 
				
			|||
                scheduledAlarms.remove(content.getId()); | 
				
			|||
                return false; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling notification", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule an alarm using the best available method | 
				
			|||
     *  | 
				
			|||
     * @param pendingIntent PendingIntent to trigger | 
				
			|||
     * @param triggerTime When to trigger the alarm | 
				
			|||
     * @return true if scheduling was successful | 
				
			|||
     */ | 
				
			|||
    private boolean scheduleAlarm(PendingIntent pendingIntent, long triggerTime) { | 
				
			|||
        try { | 
				
			|||
            // Check if we can use exact alarms
 | 
				
			|||
            if (canUseExactAlarms()) { | 
				
			|||
                return scheduleExactAlarm(pendingIntent, triggerTime); | 
				
			|||
            } else { | 
				
			|||
                return scheduleInexactAlarm(pendingIntent, triggerTime); | 
				
			|||
            } | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling alarm", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule an exact alarm for precise timing | 
				
			|||
     *  | 
				
			|||
     * @param pendingIntent PendingIntent to trigger | 
				
			|||
     * @param triggerTime When to trigger the alarm | 
				
			|||
     * @return true if scheduling was successful | 
				
			|||
     */ | 
				
			|||
    private boolean scheduleExactAlarm(PendingIntent pendingIntent, long triggerTime) { | 
				
			|||
        try { | 
				
			|||
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | 
				
			|||
                alarmManager.setExactAndAllowWhileIdle( | 
				
			|||
                    AlarmManager.RTC_WAKEUP, | 
				
			|||
                    triggerTime, | 
				
			|||
                    pendingIntent | 
				
			|||
                ); | 
				
			|||
            } else { | 
				
			|||
                alarmManager.setExact( | 
				
			|||
                    AlarmManager.RTC_WAKEUP, | 
				
			|||
                    triggerTime, | 
				
			|||
                    pendingIntent | 
				
			|||
                ); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Exact alarm scheduled for " + formatTime(triggerTime)); | 
				
			|||
            return true; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling exact alarm", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Schedule an inexact alarm for battery optimization | 
				
			|||
     *  | 
				
			|||
     * @param pendingIntent PendingIntent to trigger | 
				
			|||
     * @param triggerTime When to trigger the alarm | 
				
			|||
     * @return true if scheduling was successful | 
				
			|||
     */ | 
				
			|||
    private boolean scheduleInexactAlarm(PendingIntent pendingIntent, long triggerTime) { | 
				
			|||
        try { | 
				
			|||
            alarmManager.setRepeating( | 
				
			|||
                AlarmManager.RTC_WAKEUP, | 
				
			|||
                triggerTime, | 
				
			|||
                AlarmManager.INTERVAL_DAY, | 
				
			|||
                pendingIntent | 
				
			|||
            ); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Inexact alarm scheduled for " + formatTime(triggerTime)); | 
				
			|||
            return true; | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error scheduling inexact alarm", e); | 
				
			|||
            return false; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if we can use exact alarms | 
				
			|||
     *  | 
				
			|||
     * @return true if exact alarms are permitted | 
				
			|||
     */ | 
				
			|||
    private boolean canUseExactAlarms() { | 
				
			|||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | 
				
			|||
            return alarmManager.canScheduleExactAlarms(); | 
				
			|||
        } | 
				
			|||
        return true; // Pre-Android 12 always allowed exact alarms
 | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Cancel a specific notification | 
				
			|||
     *  | 
				
			|||
     * @param notificationId ID of notification to cancel | 
				
			|||
     */ | 
				
			|||
    public void cancelNotification(String notificationId) { | 
				
			|||
        try { | 
				
			|||
            PendingIntent pendingIntent = scheduledAlarms.remove(notificationId); | 
				
			|||
            if (pendingIntent != null) { | 
				
			|||
                alarmManager.cancel(pendingIntent); | 
				
			|||
                pendingIntent.cancel(); | 
				
			|||
                Log.d(TAG, "Cancelled notification: " + notificationId); | 
				
			|||
            } | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error cancelling notification: " + notificationId, e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Cancel all scheduled notifications | 
				
			|||
     */ | 
				
			|||
    public void cancelAllNotifications() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Cancelling all notifications"); | 
				
			|||
             | 
				
			|||
            for (String notificationId : scheduledAlarms.keySet()) { | 
				
			|||
                cancelNotification(notificationId); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            scheduledAlarms.clear(); | 
				
			|||
            Log.i(TAG, "All notifications cancelled"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error cancelling all notifications", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the next scheduled notification time | 
				
			|||
     *  | 
				
			|||
     * @return Timestamp of next notification or 0 if none scheduled | 
				
			|||
     */ | 
				
			|||
    public long getNextNotificationTime() { | 
				
			|||
        // This would need to be implemented with actual notification data
 | 
				
			|||
        // For now, return a placeholder
 | 
				
			|||
        return System.currentTimeMillis() + (24 * 60 * 60 * 1000); // 24 hours from now
 | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get count of pending notifications | 
				
			|||
     *  | 
				
			|||
     * @return Number of scheduled notifications | 
				
			|||
     */ | 
				
			|||
    public int getPendingNotificationsCount() { | 
				
			|||
        return scheduledAlarms.size(); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Update notification settings for existing notifications | 
				
			|||
     */ | 
				
			|||
    public void updateNotificationSettings() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Updating notification settings"); | 
				
			|||
             | 
				
			|||
            // This would typically involve rescheduling notifications
 | 
				
			|||
            // with new settings. For now, just log the action.
 | 
				
			|||
            Log.i(TAG, "Notification settings updated"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error updating notification settings", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Enable adaptive scheduling based on device state | 
				
			|||
     */ | 
				
			|||
    public void enableAdaptiveScheduling() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Enabling adaptive scheduling"); | 
				
			|||
             | 
				
			|||
            // This would implement logic to adjust scheduling based on:
 | 
				
			|||
            // - Battery level
 | 
				
			|||
            // - Power save mode
 | 
				
			|||
            // - Doze mode
 | 
				
			|||
            // - User activity patterns
 | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Adaptive scheduling enabled"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error enabling adaptive scheduling", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Disable adaptive scheduling | 
				
			|||
     */ | 
				
			|||
    public void disableAdaptiveScheduling() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Disabling adaptive scheduling"); | 
				
			|||
             | 
				
			|||
            // Reset to default scheduling behavior
 | 
				
			|||
            Log.i(TAG, "Adaptive scheduling disabled"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error disabling adaptive scheduling", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Reschedule notifications after system reboot | 
				
			|||
     */ | 
				
			|||
    public void rescheduleAfterReboot() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Rescheduling notifications after reboot"); | 
				
			|||
             | 
				
			|||
            // This would typically be called from a BOOT_COMPLETED receiver
 | 
				
			|||
            // to restore scheduled notifications after device restart
 | 
				
			|||
             | 
				
			|||
            Log.i(TAG, "Notifications rescheduled after reboot"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error rescheduling after reboot", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if a notification is currently scheduled | 
				
			|||
     *  | 
				
			|||
     * @param notificationId ID of notification to check | 
				
			|||
     * @return true if notification is scheduled | 
				
			|||
     */ | 
				
			|||
    public boolean isNotificationScheduled(String notificationId) { | 
				
			|||
        return scheduledAlarms.containsKey(notificationId); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get scheduling statistics | 
				
			|||
     *  | 
				
			|||
     * @return Scheduling statistics as a string | 
				
			|||
     */ | 
				
			|||
    public String getSchedulingStats() { | 
				
			|||
        return String.format("Scheduled: %d, Exact alarms: %s", | 
				
			|||
            scheduledAlarms.size(), | 
				
			|||
            canUseExactAlarms() ? "enabled" : "disabled"); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Format timestamp for logging | 
				
			|||
     *  | 
				
			|||
     * @param timestamp Timestamp in milliseconds | 
				
			|||
     * @return Formatted time string | 
				
			|||
     */ | 
				
			|||
    private String formatTime(long timestamp) { | 
				
			|||
        Calendar calendar = Calendar.getInstance(); | 
				
			|||
        calendar.setTimeInMillis(timestamp); | 
				
			|||
         | 
				
			|||
        return String.format("%02d:%02d:%02d on %02d/%02d/%04d", | 
				
			|||
            calendar.get(Calendar.HOUR_OF_DAY), | 
				
			|||
            calendar.get(Calendar.MINUTE), | 
				
			|||
            calendar.get(Calendar.SECOND), | 
				
			|||
            calendar.get(Calendar.MONTH) + 1, | 
				
			|||
            calendar.get(Calendar.DAY_OF_MONTH), | 
				
			|||
            calendar.get(Calendar.YEAR)); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Calculate next occurrence of a daily time | 
				
			|||
     *  | 
				
			|||
     * @param hour Hour of day (0-23) | 
				
			|||
     * @param minute Minute of hour (0-59) | 
				
			|||
     * @return Timestamp of next occurrence | 
				
			|||
     */ | 
				
			|||
    public long calculateNextOccurrence(int hour, int minute) { | 
				
			|||
        Calendar calendar = Calendar.getInstance(); | 
				
			|||
        calendar.set(Calendar.HOUR_OF_DAY, hour); | 
				
			|||
        calendar.set(Calendar.MINUTE, minute); | 
				
			|||
        calendar.set(Calendar.SECOND, 0); | 
				
			|||
        calendar.set(Calendar.MILLISECOND, 0); | 
				
			|||
         | 
				
			|||
        // If time has passed today, schedule for tomorrow
 | 
				
			|||
        if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { | 
				
			|||
            calendar.add(Calendar.DAY_OF_YEAR, 1); | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        return calendar.getTimeInMillis(); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,476 @@ | 
				
			|||
/** | 
				
			|||
 * DailyNotificationStorage.java | 
				
			|||
 *  | 
				
			|||
 * Storage management for notification content and settings | 
				
			|||
 * Implements tiered storage: Key-Value (quick) + DB (structured) + Files (large assets) | 
				
			|||
 *  | 
				
			|||
 * @author Matthew Raymer | 
				
			|||
 * @version 1.0.0 | 
				
			|||
 */ | 
				
			|||
 | 
				
			|||
package com.timesafari.dailynotification; | 
				
			|||
 | 
				
			|||
import android.content.Context; | 
				
			|||
import android.content.SharedPreferences; | 
				
			|||
import android.util.Log; | 
				
			|||
 | 
				
			|||
import com.google.gson.Gson; | 
				
			|||
import com.google.gson.reflect.TypeToken; | 
				
			|||
 | 
				
			|||
import java.io.File; | 
				
			|||
import java.lang.reflect.Type; | 
				
			|||
import java.util.ArrayList; | 
				
			|||
import java.util.Collections; | 
				
			|||
import java.util.Comparator; | 
				
			|||
import java.util.List; | 
				
			|||
import java.util.concurrent.ConcurrentHashMap; | 
				
			|||
 | 
				
			|||
/** | 
				
			|||
 * Manages storage for notification content and settings | 
				
			|||
 *  | 
				
			|||
 * This class implements the tiered storage approach: | 
				
			|||
 * - Tier 1: SharedPreferences for quick access to settings and recent data | 
				
			|||
 * - Tier 2: In-memory cache for structured notification content | 
				
			|||
 * - Tier 3: File system for large assets (future use) | 
				
			|||
 */ | 
				
			|||
public class DailyNotificationStorage { | 
				
			|||
     | 
				
			|||
    private static final String TAG = "DailyNotificationStorage"; | 
				
			|||
    private static final String PREFS_NAME = "DailyNotificationPrefs"; | 
				
			|||
    private static final String KEY_NOTIFICATIONS = "notifications"; | 
				
			|||
    private static final String KEY_SETTINGS = "settings"; | 
				
			|||
    private static final String KEY_LAST_FETCH = "last_fetch"; | 
				
			|||
    private static final String KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling"; | 
				
			|||
     | 
				
			|||
    private static final int MAX_CACHE_SIZE = 100; // Maximum notifications to keep in memory
 | 
				
			|||
    private static final long CACHE_CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
 | 
				
			|||
     | 
				
			|||
    private final Context context; | 
				
			|||
    private final SharedPreferences prefs; | 
				
			|||
    private final Gson gson; | 
				
			|||
    private final ConcurrentHashMap<String, NotificationContent> notificationCache; | 
				
			|||
    private final List<NotificationContent> notificationList; | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Constructor | 
				
			|||
     *  | 
				
			|||
     * @param context Application context | 
				
			|||
     */ | 
				
			|||
    public DailyNotificationStorage(Context context) { | 
				
			|||
        this.context = context; | 
				
			|||
        this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); | 
				
			|||
        this.gson = new Gson(); | 
				
			|||
        this.notificationCache = new ConcurrentHashMap<>(); | 
				
			|||
        this.notificationList = Collections.synchronizedList(new ArrayList<>()); | 
				
			|||
         | 
				
			|||
        loadNotificationsFromStorage(); | 
				
			|||
        cleanupOldNotifications(); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Save notification content to storage | 
				
			|||
     *  | 
				
			|||
     * @param content Notification content to save | 
				
			|||
     */ | 
				
			|||
    public void saveNotificationContent(NotificationContent content) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Saving notification: " + content.getId()); | 
				
			|||
             | 
				
			|||
            // Add to cache
 | 
				
			|||
            notificationCache.put(content.getId(), content); | 
				
			|||
             | 
				
			|||
            // Add to list and sort by scheduled time
 | 
				
			|||
            synchronized (notificationList) { | 
				
			|||
                notificationList.removeIf(n -> n.getId().equals(content.getId())); | 
				
			|||
                notificationList.add(content); | 
				
			|||
                Collections.sort(notificationList,  | 
				
			|||
                    Comparator.comparingLong(NotificationContent::getScheduledTime)); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Persist to SharedPreferences
 | 
				
			|||
            saveNotificationsToStorage(); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Notification saved successfully"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error saving notification content", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get notification content by ID | 
				
			|||
     *  | 
				
			|||
     * @param id Notification ID | 
				
			|||
     * @return Notification content or null if not found | 
				
			|||
     */ | 
				
			|||
    public NotificationContent getNotificationContent(String id) { | 
				
			|||
        return notificationCache.get(id); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the last notification that was delivered | 
				
			|||
     *  | 
				
			|||
     * @return Last notification or null if none exists | 
				
			|||
     */ | 
				
			|||
    public NotificationContent getLastNotification() { | 
				
			|||
        synchronized (notificationList) { | 
				
			|||
            if (notificationList.isEmpty()) { | 
				
			|||
                return null; | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Find the most recent delivered notification
 | 
				
			|||
            long currentTime = System.currentTimeMillis(); | 
				
			|||
            for (int i = notificationList.size() - 1; i >= 0; i--) { | 
				
			|||
                NotificationContent notification = notificationList.get(i); | 
				
			|||
                if (notification.getScheduledTime() <= currentTime) { | 
				
			|||
                    return notification; | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            return null; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get all notifications | 
				
			|||
     *  | 
				
			|||
     * @return List of all notifications | 
				
			|||
     */ | 
				
			|||
    public List<NotificationContent> getAllNotifications() { | 
				
			|||
        synchronized (notificationList) { | 
				
			|||
            return new ArrayList<>(notificationList); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get notifications that are ready to be displayed | 
				
			|||
     *  | 
				
			|||
     * @return List of ready notifications | 
				
			|||
     */ | 
				
			|||
    public List<NotificationContent> getReadyNotifications() { | 
				
			|||
        List<NotificationContent> readyNotifications = new ArrayList<>(); | 
				
			|||
        long currentTime = System.currentTimeMillis(); | 
				
			|||
         | 
				
			|||
        synchronized (notificationList) { | 
				
			|||
            for (NotificationContent notification : notificationList) { | 
				
			|||
                if (notification.isReadyToDisplay()) { | 
				
			|||
                    readyNotifications.add(notification); | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
        } | 
				
			|||
         | 
				
			|||
        return readyNotifications; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the next scheduled notification | 
				
			|||
     *  | 
				
			|||
     * @return Next notification or null if none scheduled | 
				
			|||
     */ | 
				
			|||
    public NotificationContent getNextNotification() { | 
				
			|||
        synchronized (notificationList) { | 
				
			|||
            long currentTime = System.currentTimeMillis(); | 
				
			|||
             | 
				
			|||
            for (NotificationContent notification : notificationList) { | 
				
			|||
                if (notification.getScheduledTime() > currentTime) { | 
				
			|||
                    return notification; | 
				
			|||
                } | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            return null; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Remove notification by ID | 
				
			|||
     *  | 
				
			|||
     * @param id Notification ID to remove | 
				
			|||
     */ | 
				
			|||
    public void removeNotification(String id) { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Removing notification: " + id); | 
				
			|||
             | 
				
			|||
            notificationCache.remove(id); | 
				
			|||
             | 
				
			|||
            synchronized (notificationList) { | 
				
			|||
                notificationList.removeIf(n -> n.getId().equals(id)); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            saveNotificationsToStorage(); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Notification removed successfully"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error removing notification", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Clear all notifications | 
				
			|||
     */ | 
				
			|||
    public void clearAllNotifications() { | 
				
			|||
        try { | 
				
			|||
            Log.d(TAG, "Clearing all notifications"); | 
				
			|||
             | 
				
			|||
            notificationCache.clear(); | 
				
			|||
             | 
				
			|||
            synchronized (notificationList) { | 
				
			|||
                notificationList.clear(); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            saveNotificationsToStorage(); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "All notifications cleared successfully"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error clearing notifications", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get notification count | 
				
			|||
     *  | 
				
			|||
     * @return Number of notifications | 
				
			|||
     */ | 
				
			|||
    public int getNotificationCount() { | 
				
			|||
        return notificationCache.size(); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if storage is empty | 
				
			|||
     *  | 
				
			|||
     * @return true if no notifications exist | 
				
			|||
     */ | 
				
			|||
    public boolean isEmpty() { | 
				
			|||
        return notificationCache.isEmpty(); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set sound enabled setting | 
				
			|||
     *  | 
				
			|||
     * @param enabled true to enable sound | 
				
			|||
     */ | 
				
			|||
    public void setSoundEnabled(boolean enabled) { | 
				
			|||
        SharedPreferences.Editor editor = prefs.edit(); | 
				
			|||
        editor.putBoolean("sound_enabled", enabled); | 
				
			|||
        editor.apply(); | 
				
			|||
         | 
				
			|||
        Log.d(TAG, "Sound setting updated: " + enabled); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get sound enabled setting | 
				
			|||
     *  | 
				
			|||
     * @return true if sound is enabled | 
				
			|||
     */ | 
				
			|||
    public boolean isSoundEnabled() { | 
				
			|||
        return prefs.getBoolean("sound_enabled", true); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set notification priority | 
				
			|||
     *  | 
				
			|||
     * @param priority Priority string (high, default, low) | 
				
			|||
     */ | 
				
			|||
    public void setPriority(String priority) { | 
				
			|||
        SharedPreferences.Editor editor = prefs.edit(); | 
				
			|||
        editor.putString("priority", priority); | 
				
			|||
        editor.apply(); | 
				
			|||
         | 
				
			|||
        Log.d(TAG, "Priority setting updated: " + priority); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get notification priority | 
				
			|||
     *  | 
				
			|||
     * @return Priority string | 
				
			|||
     */ | 
				
			|||
    public String getPriority() { | 
				
			|||
        return prefs.getString("priority", "default"); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set timezone setting | 
				
			|||
     *  | 
				
			|||
     * @param timezone Timezone identifier | 
				
			|||
     */ | 
				
			|||
    public void setTimezone(String timezone) { | 
				
			|||
        SharedPreferences.Editor editor = prefs.edit(); | 
				
			|||
        editor.putString("timezone", timezone); | 
				
			|||
        editor.apply(); | 
				
			|||
         | 
				
			|||
        Log.d(TAG, "Timezone setting updated: " + timezone); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get timezone setting | 
				
			|||
     *  | 
				
			|||
     * @return Timezone identifier | 
				
			|||
     */ | 
				
			|||
    public String getTimezone() { | 
				
			|||
        return prefs.getString("timezone", "UTC"); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set adaptive scheduling enabled | 
				
			|||
     *  | 
				
			|||
     * @param enabled true to enable adaptive scheduling | 
				
			|||
     */ | 
				
			|||
    public void setAdaptiveSchedulingEnabled(boolean enabled) { | 
				
			|||
        SharedPreferences.Editor editor = prefs.edit(); | 
				
			|||
        editor.putBoolean(KEY_ADAPTIVE_SCHEDULING, enabled); | 
				
			|||
        editor.apply(); | 
				
			|||
         | 
				
			|||
        Log.d(TAG, "Adaptive scheduling setting updated: " + enabled); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if adaptive scheduling is enabled | 
				
			|||
     *  | 
				
			|||
     * @return true if adaptive scheduling is enabled | 
				
			|||
     */ | 
				
			|||
    public boolean isAdaptiveSchedulingEnabled() { | 
				
			|||
        return prefs.getBoolean(KEY_ADAPTIVE_SCHEDULING, true); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set last fetch timestamp | 
				
			|||
     *  | 
				
			|||
     * @param timestamp Last fetch time in milliseconds | 
				
			|||
     */ | 
				
			|||
    public void setLastFetchTime(long timestamp) { | 
				
			|||
        SharedPreferences.Editor editor = prefs.edit(); | 
				
			|||
        editor.putLong(KEY_LAST_FETCH, timestamp); | 
				
			|||
        editor.apply(); | 
				
			|||
         | 
				
			|||
        Log.d(TAG, "Last fetch time updated: " + timestamp); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get last fetch timestamp | 
				
			|||
     *  | 
				
			|||
     * @return Last fetch time in milliseconds | 
				
			|||
     */ | 
				
			|||
    public long getLastFetchTime() { | 
				
			|||
        return prefs.getLong(KEY_LAST_FETCH, 0); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if it's time to fetch new content | 
				
			|||
     *  | 
				
			|||
     * @return true if fetch is needed | 
				
			|||
     */ | 
				
			|||
    public boolean shouldFetchNewContent() { | 
				
			|||
        long lastFetch = getLastFetchTime(); | 
				
			|||
        long currentTime = System.currentTimeMillis(); | 
				
			|||
        long timeSinceLastFetch = currentTime - lastFetch; | 
				
			|||
         | 
				
			|||
        // Fetch if more than 12 hours have passed
 | 
				
			|||
        return timeSinceLastFetch > 12 * 60 * 60 * 1000; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Load notifications from persistent storage | 
				
			|||
     */ | 
				
			|||
    private void loadNotificationsFromStorage() { | 
				
			|||
        try { | 
				
			|||
            String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]"); | 
				
			|||
            Type type = new TypeToken<ArrayList<NotificationContent>>(){}.getType(); | 
				
			|||
            List<NotificationContent> notifications = gson.fromJson(notificationsJson, type); | 
				
			|||
             | 
				
			|||
            if (notifications != null) { | 
				
			|||
                for (NotificationContent notification : notifications) { | 
				
			|||
                    notificationCache.put(notification.getId(), notification); | 
				
			|||
                    notificationList.add(notification); | 
				
			|||
                } | 
				
			|||
                 | 
				
			|||
                // Sort by scheduled time
 | 
				
			|||
                Collections.sort(notificationList,  | 
				
			|||
                    Comparator.comparingLong(NotificationContent::getScheduledTime)); | 
				
			|||
                 | 
				
			|||
                Log.d(TAG, "Loaded " + notifications.size() + " notifications from storage"); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error loading notifications from storage", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Save notifications to persistent storage | 
				
			|||
     */ | 
				
			|||
    private void saveNotificationsToStorage() { | 
				
			|||
        try { | 
				
			|||
            List<NotificationContent> notifications; | 
				
			|||
            synchronized (notificationList) { | 
				
			|||
                notifications = new ArrayList<>(notificationList); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            String notificationsJson = gson.toJson(notifications); | 
				
			|||
            SharedPreferences.Editor editor = prefs.edit(); | 
				
			|||
            editor.putString(KEY_NOTIFICATIONS, notificationsJson); | 
				
			|||
            editor.apply(); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Saved " + notifications.size() + " notifications to storage"); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error saving notifications to storage", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Clean up old notifications to prevent memory bloat | 
				
			|||
     */ | 
				
			|||
    private void cleanupOldNotifications() { | 
				
			|||
        try { | 
				
			|||
            long currentTime = System.currentTimeMillis(); | 
				
			|||
            long cutoffTime = currentTime - (7 * 24 * 60 * 60 * 1000); // 7 days ago
 | 
				
			|||
             | 
				
			|||
            synchronized (notificationList) { | 
				
			|||
                notificationList.removeIf(notification ->  | 
				
			|||
                    notification.getScheduledTime() < cutoffTime); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Update cache to match
 | 
				
			|||
            notificationCache.clear(); | 
				
			|||
            for (NotificationContent notification : notificationList) { | 
				
			|||
                notificationCache.put(notification.getId(), notification); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            // Limit cache size
 | 
				
			|||
            if (notificationCache.size() > MAX_CACHE_SIZE) { | 
				
			|||
                List<NotificationContent> sortedNotifications = new ArrayList<>(notificationList); | 
				
			|||
                Collections.sort(sortedNotifications,  | 
				
			|||
                    Comparator.comparingLong(NotificationContent::getScheduledTime)); | 
				
			|||
                 | 
				
			|||
                int toRemove = sortedNotifications.size() - MAX_CACHE_SIZE; | 
				
			|||
                for (int i = 0; i < toRemove; i++) { | 
				
			|||
                    NotificationContent notification = sortedNotifications.get(i); | 
				
			|||
                    notificationCache.remove(notification.getId()); | 
				
			|||
                } | 
				
			|||
                 | 
				
			|||
                notificationList.clear(); | 
				
			|||
                notificationList.addAll(sortedNotifications.subList(toRemove, sortedNotifications.size())); | 
				
			|||
            } | 
				
			|||
             | 
				
			|||
            saveNotificationsToStorage(); | 
				
			|||
             | 
				
			|||
            Log.d(TAG, "Cleanup completed. Cache size: " + notificationCache.size()); | 
				
			|||
             | 
				
			|||
        } catch (Exception e) { | 
				
			|||
            Log.e(TAG, "Error during cleanup", e); | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get storage statistics | 
				
			|||
     *  | 
				
			|||
     * @return Storage statistics as a string | 
				
			|||
     */ | 
				
			|||
    public String getStorageStats() { | 
				
			|||
        return String.format("Notifications: %d, Cache size: %d, Last fetch: %d", | 
				
			|||
            notificationList.size(), | 
				
			|||
            notificationCache.size(), | 
				
			|||
            getLastFetchTime()); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -0,0 +1,315 @@ | 
				
			|||
/** | 
				
			|||
 * NotificationContent.java | 
				
			|||
 *  | 
				
			|||
 * Data model for notification content following the project directive schema | 
				
			|||
 * Implements the canonical NotificationContent v1 structure | 
				
			|||
 *  | 
				
			|||
 * @author Matthew Raymer | 
				
			|||
 * @version 1.0.0 | 
				
			|||
 */ | 
				
			|||
 | 
				
			|||
package com.timesafari.dailynotification; | 
				
			|||
 | 
				
			|||
import java.util.UUID; | 
				
			|||
 | 
				
			|||
/** | 
				
			|||
 * Represents notification content with all required fields | 
				
			|||
 *  | 
				
			|||
 * This class follows the canonical schema defined in the project directive: | 
				
			|||
 * - id: string (uuid) | 
				
			|||
 * - title: string | 
				
			|||
 * - body: string (plain text; may include simple emoji) | 
				
			|||
 * - scheduledTime: epoch millis (client-local target) | 
				
			|||
 * - mediaUrl: string? (for future; must be mirrored to local path before use) | 
				
			|||
 * - fetchTime: epoch millis | 
				
			|||
 */ | 
				
			|||
public class NotificationContent { | 
				
			|||
     | 
				
			|||
    private String id; | 
				
			|||
    private String title; | 
				
			|||
    private String body; | 
				
			|||
    private long scheduledTime; | 
				
			|||
    private String mediaUrl; | 
				
			|||
    private long fetchTime; | 
				
			|||
    private boolean sound; | 
				
			|||
    private String priority; | 
				
			|||
    private String url; | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Default constructor with auto-generated UUID | 
				
			|||
     */ | 
				
			|||
    public NotificationContent() { | 
				
			|||
        this.id = UUID.randomUUID().toString(); | 
				
			|||
        this.fetchTime = System.currentTimeMillis(); | 
				
			|||
        this.sound = true; | 
				
			|||
        this.priority = "default"; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Constructor with all required fields | 
				
			|||
     *  | 
				
			|||
     * @param title Notification title | 
				
			|||
     * @param body Notification body text | 
				
			|||
     * @param scheduledTime When to display the notification | 
				
			|||
     */ | 
				
			|||
    public NotificationContent(String title, String body, long scheduledTime) { | 
				
			|||
        this(); | 
				
			|||
        this.title = title; | 
				
			|||
        this.body = body; | 
				
			|||
        this.scheduledTime = scheduledTime; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    // Getters and Setters
 | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the unique identifier for this notification | 
				
			|||
     *  | 
				
			|||
     * @return UUID string | 
				
			|||
     */ | 
				
			|||
    public String getId() { | 
				
			|||
        return id; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set the unique identifier for this notification | 
				
			|||
     *  | 
				
			|||
     * @param id UUID string | 
				
			|||
     */ | 
				
			|||
    public void setId(String id) { | 
				
			|||
        this.id = id; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the notification title | 
				
			|||
     *  | 
				
			|||
     * @return Title string | 
				
			|||
     */ | 
				
			|||
    public String getTitle() { | 
				
			|||
        return title; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set the notification title | 
				
			|||
     *  | 
				
			|||
     * @param title Title string | 
				
			|||
     */ | 
				
			|||
    public void setTitle(String title) { | 
				
			|||
        this.title = title; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the notification body text | 
				
			|||
     *  | 
				
			|||
     * @return Body text string | 
				
			|||
     */ | 
				
			|||
    public String getBody() { | 
				
			|||
        return body; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set the notification body text | 
				
			|||
     *  | 
				
			|||
     * @param body Body text string | 
				
			|||
     */ | 
				
			|||
    public void setBody(String body) { | 
				
			|||
        this.body = body; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the scheduled time for this notification | 
				
			|||
     *  | 
				
			|||
     * @return Timestamp in milliseconds | 
				
			|||
     */ | 
				
			|||
    public long getScheduledTime() { | 
				
			|||
        return scheduledTime; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set the scheduled time for this notification | 
				
			|||
     *  | 
				
			|||
     * @param scheduledTime Timestamp in milliseconds | 
				
			|||
     */ | 
				
			|||
    public void setScheduledTime(long scheduledTime) { | 
				
			|||
        this.scheduledTime = scheduledTime; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the media URL (optional, for future use) | 
				
			|||
     *  | 
				
			|||
     * @return Media URL string or null | 
				
			|||
     */ | 
				
			|||
    public String getMediaUrl() { | 
				
			|||
        return mediaUrl; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set the media URL (optional, for future use) | 
				
			|||
     *  | 
				
			|||
     * @param mediaUrl Media URL string or null | 
				
			|||
     */ | 
				
			|||
    public void setMediaUrl(String mediaUrl) { | 
				
			|||
        this.mediaUrl = mediaUrl; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the fetch time when content was retrieved | 
				
			|||
     *  | 
				
			|||
     * @return Timestamp in milliseconds | 
				
			|||
     */ | 
				
			|||
    public long getFetchTime() { | 
				
			|||
        return fetchTime; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set the fetch time when content was retrieved | 
				
			|||
     *  | 
				
			|||
     * @param fetchTime Timestamp in milliseconds | 
				
			|||
     */ | 
				
			|||
    public void setFetchTime(long fetchTime) { | 
				
			|||
        this.fetchTime = fetchTime; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if sound should be played | 
				
			|||
     *  | 
				
			|||
     * @return true if sound is enabled | 
				
			|||
     */ | 
				
			|||
    public boolean isSound() { | 
				
			|||
        return sound; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set whether sound should be played | 
				
			|||
     *  | 
				
			|||
     * @param sound true to enable sound | 
				
			|||
     */ | 
				
			|||
    public void setSound(boolean sound) { | 
				
			|||
        this.sound = sound; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the notification priority | 
				
			|||
     *  | 
				
			|||
     * @return Priority string (high, default, low) | 
				
			|||
     */ | 
				
			|||
    public String getPriority() { | 
				
			|||
        return priority; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set the notification priority | 
				
			|||
     *  | 
				
			|||
     * @param priority Priority string (high, default, low) | 
				
			|||
     */ | 
				
			|||
    public void setPriority(String priority) { | 
				
			|||
        this.priority = priority; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the associated URL | 
				
			|||
     *  | 
				
			|||
     * @return URL string or null | 
				
			|||
     */ | 
				
			|||
    public String getUrl() { | 
				
			|||
        return url; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Set the associated URL | 
				
			|||
     *  | 
				
			|||
     * @param url URL string or null | 
				
			|||
     */ | 
				
			|||
    public void setUrl(String url) { | 
				
			|||
        this.url = url; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if this notification is stale (older than 24 hours) | 
				
			|||
     *  | 
				
			|||
     * @return true if notification is stale | 
				
			|||
     */ | 
				
			|||
    public boolean isStale() { | 
				
			|||
        long currentTime = System.currentTimeMillis(); | 
				
			|||
        long age = currentTime - fetchTime; | 
				
			|||
        return age > 24 * 60 * 60 * 1000; // 24 hours in milliseconds
 | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the age of this notification in milliseconds | 
				
			|||
     *  | 
				
			|||
     * @return Age in milliseconds | 
				
			|||
     */ | 
				
			|||
    public long getAge() { | 
				
			|||
        return System.currentTimeMillis() - fetchTime; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get the age of this notification in a human-readable format | 
				
			|||
     *  | 
				
			|||
     * @return Human-readable age string | 
				
			|||
     */ | 
				
			|||
    public String getAgeString() { | 
				
			|||
        long age = getAge(); | 
				
			|||
        long seconds = age / 1000; | 
				
			|||
        long minutes = seconds / 60; | 
				
			|||
        long hours = minutes / 60; | 
				
			|||
        long days = hours / 24; | 
				
			|||
         | 
				
			|||
        if (days > 0) { | 
				
			|||
            return days + " day" + (days == 1 ? "" : "s") + " ago"; | 
				
			|||
        } else if (hours > 0) { | 
				
			|||
            return hours + " hour" + (hours == 1 ? "" : "s") + " ago"; | 
				
			|||
        } else if (minutes > 0) { | 
				
			|||
            return minutes + " minute" + (minutes == 1 ? "" : "s") + " ago"; | 
				
			|||
        } else { | 
				
			|||
            return "just now"; | 
				
			|||
        } | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Check if this notification is ready to be displayed | 
				
			|||
     *  | 
				
			|||
     * @return true if notification should be displayed now | 
				
			|||
     */ | 
				
			|||
    public boolean isReadyToDisplay() { | 
				
			|||
        return System.currentTimeMillis() >= scheduledTime; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    /** | 
				
			|||
     * Get time until this notification should be displayed | 
				
			|||
     *  | 
				
			|||
     * @return Time in milliseconds until display | 
				
			|||
     */ | 
				
			|||
    public long getTimeUntilDisplay() { | 
				
			|||
        return Math.max(0, scheduledTime - System.currentTimeMillis()); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    @Override | 
				
			|||
    public String toString() { | 
				
			|||
        return "NotificationContent{" + | 
				
			|||
                "id='" + id + '\'' + | 
				
			|||
                ", title='" + title + '\'' + | 
				
			|||
                ", body='" + body + '\'' + | 
				
			|||
                ", scheduledTime=" + scheduledTime + | 
				
			|||
                ", mediaUrl='" + mediaUrl + '\'' + | 
				
			|||
                ", fetchTime=" + fetchTime + | 
				
			|||
                ", sound=" + sound + | 
				
			|||
                ", priority='" + priority + '\'' + | 
				
			|||
                ", url='" + url + '\'' + | 
				
			|||
                '}'; | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    @Override | 
				
			|||
    public boolean equals(Object o) { | 
				
			|||
        if (this == o) return true; | 
				
			|||
        if (o == null || getClass() != o.getClass()) return false; | 
				
			|||
         | 
				
			|||
        NotificationContent that = (NotificationContent) o; | 
				
			|||
        return id.equals(that.id); | 
				
			|||
    } | 
				
			|||
     | 
				
			|||
    @Override | 
				
			|||
    public int hashCode() { | 
				
			|||
        return id.hashCode(); | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
					Loading…
					
					
				
		Reference in new issue