Browse Source
Consolidate Java and Kotlin database implementations into unified schema, add delayed prefetch scheduling, and fix notification delivery issues. Database Consolidation: - Merge Java DailyNotificationDatabase into Kotlin DatabaseSchema - Add migration path from v1 to v2 unified schema - Include all entities: ContentCache, Schedule, Callback, History, NotificationContentEntity, NotificationDeliveryEntity, NotificationConfigEntity - Add @JvmStatic getInstance() for Java interoperability - Update DailyNotificationWorker and DailyNotificationStorageRoom to use unified database Prefetch Functionality: - Add scheduleDelayedFetch() to FetchWorker for 5-minute prefetch before notifications - Support delayed WorkManager scheduling with initialDelay - Update scheduleDailyNotification() to optionally schedule prefetch when URL is provided Notification Delivery Fixes: - Register NotifyReceiver in AndroidManifest.xml (was missing, causing notifications not to fire) - Add safe database initialization with lazy getDatabase() helper - Prevent PluginLoadException on database init failure Build Configuration: - Add kotlin-android and kotlin-kapt plugins - Configure Room annotation processor (kapt) for Kotlin - Add Room KTX dependency for coroutines support - Fix Gradle settings with pluginManagement blocks Plugin Methods Added: - checkPermissionStatus() - detailed permission status - requestNotificationPermissions() - request POST_NOTIFICATIONS - scheduleDailyNotification() - schedule with AlarmManager - configureNativeFetcher() - configure native content fetcher - Various status and configuration methods Code Cleanup: - Remove duplicate BootReceiver.java (keep Kotlin version) - Remove duplicate DailyNotificationPlugin.java (keep Kotlin version) - Remove old Java database implementation - Add native fetcher SPI registry (@JvmStatic methods) The unified database ensures schedule persistence across reboots and provides a single source of truth for all plugin data. Prefetch scheduling enables content caching before notifications fire, improving offline-first reliability.master
17 changed files with 3310 additions and 3114 deletions
@ -0,0 +1,2 @@ |
|||||
|
connection.project.dir=../../../../android |
||||
|
eclipse.preferences.version=1 |
||||
@ -0,0 +1,310 @@ |
|||||
|
# Database Consolidation Plan |
||||
|
|
||||
|
## Current State |
||||
|
|
||||
|
### Database 1: Java (`daily_notification_plugin.db`) |
||||
|
- `notification_content` - Specific notification instances |
||||
|
- `notification_delivery` - Delivery tracking/analytics |
||||
|
- `notification_config` - Configuration |
||||
|
|
||||
|
### Database 2: Kotlin (`daily_notification_database`) |
||||
|
- `content_cache` - Fetched content with TTL |
||||
|
- `schedules` - Recurring schedule patterns (CRITICAL for reboot) |
||||
|
- `callbacks` - Callback configurations |
||||
|
- `history` - Execution history |
||||
|
|
||||
|
## Unified Schema Design |
||||
|
|
||||
|
### Required Tables (All Critical) |
||||
|
|
||||
|
1. **`schedules`** - Recurring schedule patterns |
||||
|
- Stores cron/clockTime patterns |
||||
|
- Used to restore schedules after reboot |
||||
|
- Fields: id, kind ('fetch'/'notify'), cron, clockTime, enabled, lastRunAt, nextRunAt, jitterMs, backoffPolicy, stateJson |
||||
|
|
||||
|
2. **`content_cache`** - Fetched content with TTL |
||||
|
- Stores prefetched content for offline-first display |
||||
|
- Fields: id, fetchedAt, ttlSeconds, payload (BLOB), meta |
||||
|
|
||||
|
3. **`notification_config`** - Plugin configuration |
||||
|
- Stores user preferences and plugin settings |
||||
|
- Fields: id, timesafariDid, configType, configKey, configValue, configDataType, isEncrypted, createdAt, updatedAt |
||||
|
|
||||
|
4. **`callbacks`** - Callback configurations |
||||
|
- Stores callback endpoint configurations |
||||
|
- Fields: id, kind ('http'/'local'/'queue'), target, headersJson, enabled, createdAt |
||||
|
|
||||
|
### Optional Tables (Analytics/Debugging) |
||||
|
|
||||
|
5. **`notification_content`** - Specific notification instances |
||||
|
- May still be needed for one-time notifications or TimeSafari integration |
||||
|
- Fields: All existing fields from Java entity |
||||
|
|
||||
|
6. **`notification_delivery`** - Delivery tracking |
||||
|
- Analytics for delivery attempts and user interactions |
||||
|
- Fields: All existing fields from Java entity |
||||
|
|
||||
|
7. **`history`** - Execution history |
||||
|
- Logs fetch/notify/callback execution |
||||
|
- Fields: id, refId, kind, occurredAt, durationMs, outcome, diagJson |
||||
|
|
||||
|
## Consolidation Strategy |
||||
|
|
||||
|
- [x] Keep Kotlin schema as base - It already has critical tables |
||||
|
- [x] Add Java tables to Kotlin schema - Merge missing entities |
||||
|
- [x] Update all Java code - Use unified database instance |
||||
|
- [x] Update all Kotlin code - Use unified database instance |
||||
|
- [x] Single database file: `daily_notification_plugin.db` |
||||
|
|
||||
|
## Migration Path |
||||
|
|
||||
|
- [x] Create unified `DailyNotificationDatabase` with all entities |
||||
|
- [x] Update Java code to use unified database |
||||
|
- [x] Update Kotlin code to use unified database |
||||
|
- [x] Remove old `DailyNotificationDatabase` files |
||||
|
- [ ] Test reboot recovery |
||||
|
|
||||
|
## Key Decisions |
||||
|
|
||||
|
- **Primary language**: Kotlin (more modern, better coroutine support) |
||||
|
- **Database name**: `daily_notification_plugin.db` (Java naming convention) |
||||
|
- **All entities**: Both Java and Kotlin compatible |
||||
|
- **DAOs**: Mix of Java and Kotlin DAOs as needed |
||||
|
|
||||
|
## TypeScript Interface Requirements |
||||
|
|
||||
|
Since the plugin owns the database, the host app/webview needs TypeScript interfaces to read/write data. |
||||
|
|
||||
|
### Required TypeScript Methods |
||||
|
|
||||
|
#### Schedules Management |
||||
|
```typescript |
||||
|
// Read schedules |
||||
|
getSchedules(options?: { kind?: 'fetch' | 'notify', enabled?: boolean }): Promise<Schedule[]> |
||||
|
getSchedule(id: string): Promise<Schedule | null> |
||||
|
|
||||
|
// Write schedules |
||||
|
createSchedule(schedule: CreateScheduleInput): Promise<Schedule> |
||||
|
updateSchedule(id: string, updates: Partial<Schedule>): Promise<Schedule> |
||||
|
deleteSchedule(id: string): Promise<void> |
||||
|
enableSchedule(id: string, enabled: boolean): Promise<void> |
||||
|
|
||||
|
// Utility |
||||
|
calculateNextRunTime(schedule: string): Promise<number> |
||||
|
``` |
||||
|
|
||||
|
#### Content Cache Management |
||||
|
```typescript |
||||
|
// Read content cache |
||||
|
getContentCache(options?: { id?: string }): Promise<ContentCache | null> |
||||
|
getLatestContentCache(): Promise<ContentCache | null> |
||||
|
getContentCacheHistory(limit?: number): Promise<ContentCache[]> |
||||
|
|
||||
|
// Write content cache |
||||
|
saveContentCache(content: CreateContentCacheInput): Promise<ContentCache> |
||||
|
clearContentCache(options?: { olderThan?: number }): Promise<void> |
||||
|
``` |
||||
|
|
||||
|
#### Configuration Management |
||||
|
```typescript |
||||
|
// Read config |
||||
|
getConfig(key: string, options?: { timesafariDid?: string }): Promise<Config | null> |
||||
|
getAllConfigs(options?: { timesafariDid?: string, configType?: string }): Promise<Config[]> |
||||
|
|
||||
|
// Write config |
||||
|
setConfig(config: CreateConfigInput): Promise<Config> |
||||
|
updateConfig(key: string, value: string, options?: { timesafariDid?: string }): Promise<Config> |
||||
|
deleteConfig(key: string, options?: { timesafariDid?: string }): Promise<void> |
||||
|
``` |
||||
|
|
||||
|
#### Callbacks Management |
||||
|
```typescript |
||||
|
// Read callbacks |
||||
|
getCallbacks(options?: { enabled?: boolean }): Promise<Callback[]> |
||||
|
getCallback(id: string): Promise<Callback | null> |
||||
|
|
||||
|
// Write callbacks |
||||
|
registerCallback(callback: CreateCallbackInput): Promise<Callback> |
||||
|
updateCallback(id: string, updates: Partial<Callback>): Promise<Callback> |
||||
|
deleteCallback(id: string): Promise<void> |
||||
|
enableCallback(id: string, enabled: boolean): Promise<void> |
||||
|
``` |
||||
|
|
||||
|
#### History/Analytics (Optional) |
||||
|
```typescript |
||||
|
// Read history |
||||
|
getHistory(options?: { |
||||
|
since?: number, |
||||
|
kind?: 'fetch' | 'notify' | 'callback', |
||||
|
limit?: number |
||||
|
}): Promise<History[]> |
||||
|
getHistoryStats(): Promise<HistoryStats> |
||||
|
``` |
||||
|
|
||||
|
### Type Definitions |
||||
|
|
||||
|
```typescript |
||||
|
interface Schedule { |
||||
|
id: string |
||||
|
kind: 'fetch' | 'notify' |
||||
|
cron?: string |
||||
|
clockTime?: string // HH:mm format |
||||
|
enabled: boolean |
||||
|
lastRunAt?: number |
||||
|
nextRunAt?: number |
||||
|
jitterMs: number |
||||
|
backoffPolicy: string |
||||
|
stateJson?: string |
||||
|
} |
||||
|
|
||||
|
interface ContentCache { |
||||
|
id: string |
||||
|
fetchedAt: number |
||||
|
ttlSeconds: number |
||||
|
payload: string // Base64 or JSON string |
||||
|
meta?: string |
||||
|
} |
||||
|
|
||||
|
interface Config { |
||||
|
id: string |
||||
|
timesafariDid?: string |
||||
|
configType: string |
||||
|
configKey: string |
||||
|
configValue: string |
||||
|
configDataType: string |
||||
|
isEncrypted: boolean |
||||
|
createdAt: number |
||||
|
updatedAt: number |
||||
|
} |
||||
|
|
||||
|
interface Callback { |
||||
|
id: string |
||||
|
kind: 'http' | 'local' | 'queue' |
||||
|
target: string |
||||
|
headersJson?: string |
||||
|
enabled: boolean |
||||
|
createdAt: number |
||||
|
} |
||||
|
|
||||
|
interface History { |
||||
|
id: number |
||||
|
refId: string |
||||
|
kind: 'fetch' | 'notify' | 'callback' | 'boot_recovery' |
||||
|
occurredAt: number |
||||
|
durationMs?: number |
||||
|
outcome: string |
||||
|
diagJson?: string |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Database Consolidation Plan |
||||
|
|
||||
|
## Status: ✅ **CONSOLIDATION COMPLETE** |
||||
|
|
||||
|
The unified database has been successfully created and all code has been migrated to use it. |
||||
|
|
||||
|
## Current State |
||||
|
|
||||
|
### Unified Database (`daily_notification_plugin.db`) |
||||
|
Located in: `android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt` |
||||
|
|
||||
|
**All Tables Consolidated:** |
||||
|
- ✅ `content_cache` - Fetched content with TTL (Kotlin) |
||||
|
- ✅ `schedules` - Recurring schedule patterns (Kotlin, CRITICAL for reboot) |
||||
|
- ✅ `callbacks` - Callback configurations (Kotlin) |
||||
|
- ✅ `history` - Execution history (Kotlin) |
||||
|
- ✅ `notification_content` - Specific notification instances (Java) |
||||
|
- ✅ `notification_delivery` - Delivery tracking/analytics (Java) |
||||
|
- ✅ `notification_config` - Configuration management (Java) |
||||
|
|
||||
|
### Old Database Files (DEPRECATED - REMOVED) |
||||
|
- ✅ `android/src/main/java/com/timesafari/dailynotification/database/DailyNotificationDatabase.java` - **REMOVED** - All functionality merged into unified database |
||||
|
|
||||
|
## Migration Status |
||||
|
|
||||
|
### ✅ Completed Tasks |
||||
|
- [x] Analyzed both database schemas and identified all required tables |
||||
|
- [x] Designed unified database schema with all required entities |
||||
|
- [x] Created unified DailyNotificationDatabase class (Kotlin) |
||||
|
- [x] Added migration from version 1 (Kotlin-only) to version 2 (unified) |
||||
|
- [x] Updated all Java code to use unified database |
||||
|
- [x] `DailyNotificationStorageRoom.java` - Uses unified database |
||||
|
- [x] `DailyNotificationWorker.java` - Uses unified database |
||||
|
- [x] Updated all Kotlin code to use unified database |
||||
|
- [x] `DailyNotificationPlugin.kt` - Uses unified database |
||||
|
- [x] `FetchWorker.kt` - Uses unified database |
||||
|
- [x] `NotifyReceiver.kt` - Uses unified database |
||||
|
- [x] `BootReceiver.kt` - Uses unified database |
||||
|
- [x] Implemented all Config methods in PluginMethods |
||||
|
- [x] TypeScript interfaces updated for database CRUD operations |
||||
|
- [x] Documentation created for AI assistants |
||||
|
|
||||
|
### ⏳ Pending Tasks |
||||
|
- [x] Remove old database files (`DailyNotificationDatabase.java`) |
||||
|
- [ ] Test reboot recovery with unified database |
||||
|
- [ ] Verify migration path works correctly |
||||
|
|
||||
|
## Unified Schema Design (IMPLEMENTED) |
||||
|
|
||||
|
### Required Tables (All Critical) |
||||
|
|
||||
|
1. **`schedules`** - Recurring schedule patterns |
||||
|
- Stores cron/clockTime patterns |
||||
|
- Used to restore schedules after reboot |
||||
|
- Fields: id, kind ('fetch'/'notify'), cron, clockTime, enabled, lastRunAt, nextRunAt, jitterMs, backoffPolicy, stateJson |
||||
|
|
||||
|
2. **`content_cache`** - Fetched content with TTL |
||||
|
- Stores prefetched content for offline-first display |
||||
|
- Fields: id, fetchedAt, ttlSeconds, payload (BLOB), meta |
||||
|
|
||||
|
3. **`notification_config`** - Plugin configuration |
||||
|
- Stores user preferences and plugin settings |
||||
|
- Fields: id, timesafariDid, configType, configKey, configValue, configDataType, isEncrypted, createdAt, updatedAt, ttlSeconds, isActive, metadata |
||||
|
|
||||
|
4. **`callbacks`** - Callback configurations |
||||
|
- Stores callback endpoint configurations |
||||
|
- Fields: id, kind ('http'/'local'/'queue'), target, headersJson, enabled, createdAt |
||||
|
|
||||
|
5. **`notification_content`** - Specific notification instances |
||||
|
- Stores notification content with plugin-specific fields |
||||
|
- Fields: All existing fields from Java entity |
||||
|
|
||||
|
6. **`notification_delivery`** - Delivery tracking |
||||
|
- Analytics for delivery attempts and user interactions |
||||
|
- Fields: All existing fields from Java entity |
||||
|
|
||||
|
7. **`history`** - Execution history |
||||
|
- Logs fetch/notify/callback execution |
||||
|
- Fields: id, refId, kind, occurredAt, durationMs, outcome, diagJson |
||||
|
|
||||
|
## Implementation Details |
||||
|
|
||||
|
### Database Access |
||||
|
- **Kotlin**: `DailyNotificationDatabase.getDatabase(context)` |
||||
|
- **Java**: `DailyNotificationDatabase.getInstance(context)` (Java-compatible wrapper) |
||||
|
|
||||
|
### Migration Path |
||||
|
- Version 1 → Version 2: Automatically creates Java entity tables when upgrading from Kotlin-only schema |
||||
|
- Migration runs automatically on first access after upgrade |
||||
|
|
||||
|
### Thread Safety |
||||
|
- All database operations use Kotlin coroutines (`Dispatchers.IO`) |
||||
|
- Room handles thread safety internally |
||||
|
- Singleton pattern ensures single database instance |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
1. **Remove Old Database File** ✅ COMPLETE |
||||
|
- [x] Delete `android/src/main/java/com/timesafari/dailynotification/database/DailyNotificationDatabase.java` |
||||
|
- [x] Verify no remaining references |
||||
|
|
||||
|
2. **Testing** |
||||
|
- [ ] Test reboot recovery with unified database |
||||
|
- [ ] Verify schedule restoration works correctly |
||||
|
- [ ] Verify all Config methods work correctly |
||||
|
- [ ] Test migration from v1 to v2 |
||||
|
|
||||
|
3. **Documentation** |
||||
|
- [ ] Update any remaining documentation references |
||||
|
- [ ] Verify AI documentation is complete |
||||
|
|
||||
@ -1,206 +0,0 @@ |
|||||
/** |
|
||||
* BootReceiver.java |
|
||||
* |
|
||||
* Android Boot Receiver for DailyNotification plugin |
|
||||
* Handles system boot events to restore scheduled notifications |
|
||||
* |
|
||||
* @author Matthew Raymer |
|
||||
* @version 1.0.0 |
|
||||
*/ |
|
||||
|
|
||||
package com.timesafari.dailynotification; |
|
||||
|
|
||||
import android.content.BroadcastReceiver; |
|
||||
import android.content.Context; |
|
||||
import android.content.Intent; |
|
||||
import android.util.Log; |
|
||||
|
|
||||
/** |
|
||||
* Broadcast receiver for system boot events |
|
||||
* |
|
||||
* This receiver is triggered when: |
|
||||
* - Device boots up (BOOT_COMPLETED) |
|
||||
* - App is updated (MY_PACKAGE_REPLACED) |
|
||||
* - Any package is updated (PACKAGE_REPLACED) |
|
||||
* |
|
||||
* It ensures that scheduled notifications are restored after system events |
|
||||
* that might have cleared the alarm manager. |
|
||||
*/ |
|
||||
public class BootReceiver extends BroadcastReceiver { |
|
||||
|
|
||||
private static final String TAG = "BootReceiver"; |
|
||||
|
|
||||
// Broadcast actions we handle
|
|
||||
private static final String ACTION_LOCKED_BOOT_COMPLETED = "android.intent.action.LOCKED_BOOT_COMPLETED"; |
|
||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED"; |
|
||||
private static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED"; |
|
||||
|
|
||||
@Override |
|
||||
public void onReceive(Context context, Intent intent) { |
|
||||
if (intent == null || intent.getAction() == null) { |
|
||||
Log.w(TAG, "Received null intent or action"); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
String action = intent.getAction(); |
|
||||
Log.d(TAG, "Received broadcast: " + action); |
|
||||
|
|
||||
try { |
|
||||
switch (action) { |
|
||||
case ACTION_LOCKED_BOOT_COMPLETED: |
|
||||
handleLockedBootCompleted(context); |
|
||||
break; |
|
||||
|
|
||||
case ACTION_BOOT_COMPLETED: |
|
||||
handleBootCompleted(context); |
|
||||
break; |
|
||||
|
|
||||
case ACTION_MY_PACKAGE_REPLACED: |
|
||||
handlePackageReplaced(context, intent); |
|
||||
break; |
|
||||
|
|
||||
default: |
|
||||
Log.w(TAG, "Unknown action: " + action); |
|
||||
break; |
|
||||
} |
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "Error handling broadcast: " + action, e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Handle locked boot completion (before user unlock) |
|
||||
* |
|
||||
* @param context Application context |
|
||||
*/ |
|
||||
private void handleLockedBootCompleted(Context context) { |
|
||||
Log.i(TAG, "Locked boot completed - preparing for recovery"); |
|
||||
|
|
||||
try { |
|
||||
// Use device protected storage context for Direct Boot
|
|
||||
Context deviceProtectedContext = context; |
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { |
|
||||
deviceProtectedContext = context.createDeviceProtectedStorageContext(); |
|
||||
} |
|
||||
|
|
||||
// Minimal work here - just log that we're ready
|
|
||||
// Full recovery will happen on BOOT_COMPLETED when storage is available
|
|
||||
Log.i(TAG, "Locked boot completed - ready for full recovery on unlock"); |
|
||||
|
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "Error during locked boot completion", e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Handle device boot completion (after user unlock) |
|
||||
* |
|
||||
* @param context Application context |
|
||||
*/ |
|
||||
private void handleBootCompleted(Context context) { |
|
||||
Log.i(TAG, "Device boot completed - restoring notifications"); |
|
||||
|
|
||||
try { |
|
||||
// Initialize components for recovery
|
|
||||
DailyNotificationStorage storage = new DailyNotificationStorage(context); |
|
||||
android.app.AlarmManager alarmManager = (android.app.AlarmManager) |
|
||||
context.getSystemService(android.content.Context.ALARM_SERVICE); |
|
||||
DailyNotificationScheduler scheduler = new DailyNotificationScheduler(context, alarmManager); |
|
||||
|
|
||||
// Perform boot recovery
|
|
||||
boolean recoveryPerformed = performBootRecovery(context, storage, scheduler); |
|
||||
|
|
||||
if (recoveryPerformed) { |
|
||||
Log.i(TAG, "Boot recovery completed successfully"); |
|
||||
} else { |
|
||||
Log.d(TAG, "Boot recovery skipped (not needed or already performed)"); |
|
||||
} |
|
||||
|
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "Error during boot recovery", e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Handle package replacement (app update) |
|
||||
* |
|
||||
* @param context Application context |
|
||||
* @param intent Broadcast intent |
|
||||
*/ |
|
||||
private void handlePackageReplaced(Context context, Intent intent) { |
|
||||
Log.i(TAG, "Package replaced - restoring notifications"); |
|
||||
|
|
||||
try { |
|
||||
// Initialize components for recovery
|
|
||||
DailyNotificationStorage storage = new DailyNotificationStorage(context); |
|
||||
android.app.AlarmManager alarmManager = (android.app.AlarmManager) |
|
||||
context.getSystemService(android.content.Context.ALARM_SERVICE); |
|
||||
DailyNotificationScheduler scheduler = new DailyNotificationScheduler(context, alarmManager); |
|
||||
|
|
||||
// Perform package replacement recovery
|
|
||||
boolean recoveryPerformed = performBootRecovery(context, storage, scheduler); |
|
||||
|
|
||||
if (recoveryPerformed) { |
|
||||
Log.i(TAG, "Package replacement recovery completed successfully"); |
|
||||
} else { |
|
||||
Log.d(TAG, "Package replacement recovery skipped (not needed or already performed)"); |
|
||||
} |
|
||||
|
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "Error during package replacement recovery", e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Perform boot recovery by rescheduling notifications |
|
||||
* |
|
||||
* @param context Application context |
|
||||
* @param storage Notification storage |
|
||||
* @param scheduler Notification scheduler |
|
||||
* @return true if recovery was performed, false otherwise |
|
||||
*/ |
|
||||
private boolean performBootRecovery(Context context, DailyNotificationStorage storage, |
|
||||
DailyNotificationScheduler scheduler) { |
|
||||
try { |
|
||||
Log.d(TAG, "DN|BOOT_RECOVERY_START"); |
|
||||
|
|
||||
// Get all notifications from storage
|
|
||||
java.util.List<NotificationContent> notifications = storage.getAllNotifications(); |
|
||||
|
|
||||
if (notifications.isEmpty()) { |
|
||||
Log.d(TAG, "DN|BOOT_RECOVERY_SKIP no_notifications"); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
Log.d(TAG, "DN|BOOT_RECOVERY_FOUND count=" + notifications.size()); |
|
||||
|
|
||||
int recoveredCount = 0; |
|
||||
long currentTime = System.currentTimeMillis(); |
|
||||
|
|
||||
for (NotificationContent notification : notifications) { |
|
||||
try { |
|
||||
if (notification.getScheduledTime() > currentTime) { |
|
||||
boolean scheduled = scheduler.scheduleNotification(notification); |
|
||||
if (scheduled) { |
|
||||
recoveredCount++; |
|
||||
Log.d(TAG, "DN|BOOT_RECOVERY_OK id=" + notification.getId()); |
|
||||
} else { |
|
||||
Log.w(TAG, "DN|BOOT_RECOVERY_FAIL id=" + notification.getId()); |
|
||||
} |
|
||||
} else { |
|
||||
Log.d(TAG, "DN|BOOT_RECOVERY_SKIP_PAST id=" + notification.getId()); |
|
||||
} |
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "DN|BOOT_RECOVERY_ERR id=" + notification.getId() + " err=" + e.getMessage(), e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Log.i(TAG, "DN|BOOT_RECOVERY_COMPLETE recovered=" + recoveredCount + "/" + notifications.size()); |
|
||||
return recoveredCount > 0; |
|
||||
|
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "DN|BOOT_RECOVERY_ERR exception=" + e.getMessage(), e); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,300 +0,0 @@ |
|||||
/** |
|
||||
* DailyNotificationDatabase.java |
|
||||
* |
|
||||
* Room database for the DailyNotification plugin |
|
||||
* Provides centralized data management with encryption, retention policies, and migration support |
|
||||
* |
|
||||
* @author Matthew Raymer |
|
||||
* @version 1.0.0 |
|
||||
* @since 2025-10-20 |
|
||||
*/ |
|
||||
|
|
||||
package com.timesafari.dailynotification.database; |
|
||||
|
|
||||
import android.content.Context; |
|
||||
import androidx.room.*; |
|
||||
import androidx.room.migration.Migration; |
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase; |
|
||||
|
|
||||
import com.timesafari.dailynotification.dao.NotificationContentDao; |
|
||||
import com.timesafari.dailynotification.dao.NotificationDeliveryDao; |
|
||||
import com.timesafari.dailynotification.dao.NotificationConfigDao; |
|
||||
import com.timesafari.dailynotification.entities.NotificationContentEntity; |
|
||||
import com.timesafari.dailynotification.entities.NotificationDeliveryEntity; |
|
||||
import com.timesafari.dailynotification.entities.NotificationConfigEntity; |
|
||||
|
|
||||
import java.util.concurrent.ExecutorService; |
|
||||
import java.util.concurrent.Executors; |
|
||||
|
|
||||
/** |
|
||||
* Room database for the DailyNotification plugin |
|
||||
* |
|
||||
* This database provides: |
|
||||
* - Centralized data management for all plugin data |
|
||||
* - Encryption support for sensitive information |
|
||||
* - Automatic retention policy enforcement |
|
||||
* - Migration support for schema changes |
|
||||
* - Performance optimization with proper indexing |
|
||||
* - Background thread execution for database operations |
|
||||
*/ |
|
||||
@Database( |
|
||||
entities = { |
|
||||
NotificationContentEntity.class, |
|
||||
NotificationDeliveryEntity.class, |
|
||||
NotificationConfigEntity.class |
|
||||
}, |
|
||||
version = 1, |
|
||||
exportSchema = false |
|
||||
) |
|
||||
public abstract class DailyNotificationDatabase extends RoomDatabase { |
|
||||
|
|
||||
private static final String TAG = "DailyNotificationDatabase"; |
|
||||
private static final String DATABASE_NAME = "daily_notification_plugin.db"; |
|
||||
|
|
||||
// Singleton instance
|
|
||||
private static volatile DailyNotificationDatabase INSTANCE; |
|
||||
|
|
||||
// Thread pool for database operations
|
|
||||
private static final int NUMBER_OF_THREADS = 4; |
|
||||
public static final ExecutorService databaseWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS); |
|
||||
|
|
||||
// DAO accessors
|
|
||||
public abstract NotificationContentDao notificationContentDao(); |
|
||||
public abstract NotificationDeliveryDao notificationDeliveryDao(); |
|
||||
public abstract NotificationConfigDao notificationConfigDao(); |
|
||||
|
|
||||
/** |
|
||||
* Get singleton instance of the database |
|
||||
* |
|
||||
* @param context Application context |
|
||||
* @return Database instance |
|
||||
*/ |
|
||||
public static DailyNotificationDatabase getInstance(Context context) { |
|
||||
if (INSTANCE == null) { |
|
||||
synchronized (DailyNotificationDatabase.class) { |
|
||||
if (INSTANCE == null) { |
|
||||
INSTANCE = Room.databaseBuilder( |
|
||||
context.getApplicationContext(), |
|
||||
DailyNotificationDatabase.class, |
|
||||
DATABASE_NAME |
|
||||
) |
|
||||
.addCallback(roomCallback) |
|
||||
.addMigrations(MIGRATION_1_2) // Add future migrations here
|
|
||||
.build(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
return INSTANCE; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Room database callback for initialization and cleanup |
|
||||
*/ |
|
||||
private static RoomDatabase.Callback roomCallback = new RoomDatabase.Callback() { |
|
||||
@Override |
|
||||
public void onCreate(SupportSQLiteDatabase db) { |
|
||||
super.onCreate(db); |
|
||||
// Initialize database with default data if needed
|
|
||||
databaseWriteExecutor.execute(() -> { |
|
||||
// Populate with default configurations
|
|
||||
populateDefaultConfigurations(); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onOpen(SupportSQLiteDatabase db) { |
|
||||
super.onOpen(db); |
|
||||
// Perform any necessary setup when database is opened
|
|
||||
databaseWriteExecutor.execute(() -> { |
|
||||
// Clean up expired data
|
|
||||
cleanupExpiredData(); |
|
||||
}); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
/** |
|
||||
* Populate database with default configurations |
|
||||
*/ |
|
||||
private static void populateDefaultConfigurations() { |
|
||||
if (INSTANCE == null) return; |
|
||||
|
|
||||
NotificationConfigDao configDao = INSTANCE.notificationConfigDao(); |
|
||||
|
|
||||
// Default plugin settings
|
|
||||
NotificationConfigEntity defaultSettings = new NotificationConfigEntity( |
|
||||
"default_plugin_settings", |
|
||||
null, // Global settings
|
|
||||
"plugin_setting", |
|
||||
"default_settings", |
|
||||
"{}", |
|
||||
"json" |
|
||||
); |
|
||||
defaultSettings.setTypedValue("{\"version\":\"1.0.0\",\"retention_days\":7,\"max_notifications\":100}"); |
|
||||
configDao.insertConfig(defaultSettings); |
|
||||
|
|
||||
// Default performance settings
|
|
||||
NotificationConfigEntity performanceSettings = new NotificationConfigEntity( |
|
||||
"default_performance_settings", |
|
||||
null, // Global settings
|
|
||||
"performance_setting", |
|
||||
"performance_config", |
|
||||
"{}", |
|
||||
"json" |
|
||||
); |
|
||||
performanceSettings.setTypedValue("{\"max_concurrent_deliveries\":5,\"delivery_timeout_ms\":30000,\"retry_attempts\":3}"); |
|
||||
configDao.insertConfig(performanceSettings); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Clean up expired data from all tables |
|
||||
*/ |
|
||||
private static void cleanupExpiredData() { |
|
||||
if (INSTANCE == null) return; |
|
||||
|
|
||||
long currentTime = System.currentTimeMillis(); |
|
||||
|
|
||||
// Clean up expired notifications
|
|
||||
NotificationContentDao contentDao = INSTANCE.notificationContentDao(); |
|
||||
int deletedNotifications = contentDao.deleteExpiredNotifications(currentTime); |
|
||||
|
|
||||
// Clean up old delivery tracking data (keep for 30 days)
|
|
||||
NotificationDeliveryDao deliveryDao = INSTANCE.notificationDeliveryDao(); |
|
||||
long deliveryCutoff = currentTime - (30L * 24 * 60 * 60 * 1000); // 30 days ago
|
|
||||
int deletedDeliveries = deliveryDao.deleteOldDeliveries(deliveryCutoff); |
|
||||
|
|
||||
// Clean up expired configurations
|
|
||||
NotificationConfigDao configDao = INSTANCE.notificationConfigDao(); |
|
||||
int deletedConfigs = configDao.deleteExpiredConfigs(currentTime); |
|
||||
|
|
||||
android.util.Log.d(TAG, "Cleanup completed: " + deletedNotifications + " notifications, " + |
|
||||
deletedDeliveries + " deliveries, " + deletedConfigs + " configs"); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Migration from version 1 to 2 |
|
||||
* Add new columns for enhanced functionality |
|
||||
*/ |
|
||||
static final Migration MIGRATION_1_2 = new Migration(1, 2) { |
|
||||
@Override |
|
||||
public void migrate(SupportSQLiteDatabase database) { |
|
||||
// Add new columns to notification_content table
|
|
||||
database.execSQL("ALTER TABLE notification_content ADD COLUMN analytics_data TEXT"); |
|
||||
database.execSQL("ALTER TABLE notification_content ADD COLUMN priority_level INTEGER DEFAULT 0"); |
|
||||
|
|
||||
// Add new columns to notification_delivery table
|
|
||||
database.execSQL("ALTER TABLE notification_delivery ADD COLUMN delivery_metadata TEXT"); |
|
||||
database.execSQL("ALTER TABLE notification_delivery ADD COLUMN performance_metrics TEXT"); |
|
||||
|
|
||||
// Add new columns to notification_config table
|
|
||||
database.execSQL("ALTER TABLE notification_config ADD COLUMN config_category TEXT DEFAULT 'general'"); |
|
||||
database.execSQL("ALTER TABLE notification_config ADD COLUMN config_priority INTEGER DEFAULT 0"); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
/** |
|
||||
* Close the database connection |
|
||||
* Should be called when the plugin is being destroyed |
|
||||
*/ |
|
||||
public static void closeDatabase() { |
|
||||
if (INSTANCE != null) { |
|
||||
INSTANCE.close(); |
|
||||
INSTANCE = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Clear all data from the database |
|
||||
* Use with caution - this will delete all plugin data |
|
||||
*/ |
|
||||
public static void clearAllData() { |
|
||||
if (INSTANCE == null) return; |
|
||||
|
|
||||
databaseWriteExecutor.execute(() -> { |
|
||||
NotificationContentDao contentDao = INSTANCE.notificationContentDao(); |
|
||||
NotificationDeliveryDao deliveryDao = INSTANCE.notificationDeliveryDao(); |
|
||||
NotificationConfigDao configDao = INSTANCE.notificationConfigDao(); |
|
||||
|
|
||||
// Clear all tables
|
|
||||
contentDao.deleteNotificationsByPluginVersion("0"); // Delete all
|
|
||||
deliveryDao.deleteDeliveriesByTimeSafariDid("all"); // Delete all
|
|
||||
configDao.deleteConfigsByType("all"); // Delete all
|
|
||||
|
|
||||
android.util.Log.d(TAG, "All plugin data cleared"); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Get database statistics |
|
||||
* |
|
||||
* @return Database statistics as a formatted string |
|
||||
*/ |
|
||||
public static String getDatabaseStats() { |
|
||||
if (INSTANCE == null) return "Database not initialized"; |
|
||||
|
|
||||
NotificationContentDao contentDao = INSTANCE.notificationContentDao(); |
|
||||
NotificationDeliveryDao deliveryDao = INSTANCE.notificationDeliveryDao(); |
|
||||
NotificationConfigDao configDao = INSTANCE.notificationConfigDao(); |
|
||||
|
|
||||
int notificationCount = contentDao.getTotalNotificationCount(); |
|
||||
int deliveryCount = deliveryDao.getTotalDeliveryCount(); |
|
||||
int configCount = configDao.getTotalConfigCount(); |
|
||||
|
|
||||
return String.format("Database Stats:\n" + |
|
||||
" Notifications: %d\n" + |
|
||||
" Deliveries: %d\n" + |
|
||||
" Configurations: %d\n" + |
|
||||
" Total Records: %d", |
|
||||
notificationCount, deliveryCount, configCount, |
|
||||
notificationCount + deliveryCount + configCount); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Perform database maintenance |
|
||||
* Includes cleanup, optimization, and integrity checks |
|
||||
*/ |
|
||||
public static void performMaintenance() { |
|
||||
if (INSTANCE == null) return; |
|
||||
|
|
||||
databaseWriteExecutor.execute(() -> { |
|
||||
long startTime = System.currentTimeMillis(); |
|
||||
|
|
||||
// Clean up expired data
|
|
||||
cleanupExpiredData(); |
|
||||
|
|
||||
// Additional maintenance tasks can be added here
|
|
||||
// - Vacuum database
|
|
||||
// - Analyze tables for query optimization
|
|
||||
// - Check database integrity
|
|
||||
|
|
||||
long duration = System.currentTimeMillis() - startTime; |
|
||||
android.util.Log.d(TAG, "Database maintenance completed in " + duration + "ms"); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Export database data for backup or migration |
|
||||
* |
|
||||
* @return Database export as JSON string |
|
||||
*/ |
|
||||
public static String exportDatabaseData() { |
|
||||
if (INSTANCE == null) return "{}"; |
|
||||
|
|
||||
// This would typically serialize all data to JSON
|
|
||||
// Implementation depends on specific export requirements
|
|
||||
return "{\"export\":\"not_implemented_yet\"}"; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Import database data from backup |
|
||||
* |
|
||||
* @param jsonData JSON data to import |
|
||||
* @return Success status |
|
||||
*/ |
|
||||
public static boolean importDatabaseData(String jsonData) { |
|
||||
if (INSTANCE == null || jsonData == null) return false; |
|
||||
|
|
||||
// This would typically deserialize JSON data and insert into database
|
|
||||
// Implementation depends on specific import requirements
|
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,619 @@ |
|||||
|
# Database Interfaces Documentation |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Version**: 1.0.0 |
||||
|
**Last Updated**: 2025-01-21 |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
The Daily Notification Plugin owns its own SQLite database for storing schedules, cached content, configuration, and execution history. Since the plugin's database is isolated from the host app, the webview accesses this data through TypeScript/Capacitor interfaces. |
||||
|
|
||||
|
This document explains how to use these interfaces from TypeScript/JavaScript code in your Capacitor app. |
||||
|
|
||||
|
## Architecture |
||||
|
|
||||
|
``` |
||||
|
┌─────────────────────────────────────────────────────────────┐ |
||||
|
│ Host App (TypeScript) │ |
||||
|
│ import { DailyNotification } from '@capacitor-community/...'│ |
||||
|
│ │ |
||||
|
│ const schedules = await DailyNotification.getSchedules() │ |
||||
|
└──────────────────────┬──────────────────────────────────────┘ |
||||
|
│ Capacitor Bridge |
||||
|
▼ |
||||
|
┌─────────────────────────────────────────────────────────────┐ |
||||
|
│ Plugin (Native Android/Kotlin) │ |
||||
|
│ │ |
||||
|
│ @PluginMethod │ |
||||
|
│ getSchedules() → Room Database → SQLite │ |
||||
|
└─────────────────────────────────────────────────────────────┘ |
||||
|
``` |
||||
|
|
||||
|
## Quick Start |
||||
|
|
||||
|
```typescript |
||||
|
import { DailyNotification } from '@capacitor-community/daily-notification'; |
||||
|
|
||||
|
// Get all enabled notification schedules |
||||
|
const schedules = await DailyNotification.getSchedules({ |
||||
|
kind: 'notify', |
||||
|
enabled: true |
||||
|
}); |
||||
|
|
||||
|
// Get latest cached content |
||||
|
const content = await DailyNotification.getLatestContentCache(); |
||||
|
|
||||
|
// Create a new schedule |
||||
|
const newSchedule = await DailyNotification.createSchedule({ |
||||
|
kind: 'notify', |
||||
|
cron: '0 9 * * *', // Daily at 9 AM |
||||
|
enabled: true |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
## Interface Categories |
||||
|
|
||||
|
### 1. Schedules Management |
||||
|
|
||||
|
Schedules represent recurring patterns for fetching content or displaying notifications. These are critical for reboot recovery - Android doesn't persist AlarmManager/WorkManager schedules, so they must be restored from the database. |
||||
|
|
||||
|
#### Get All Schedules |
||||
|
|
||||
|
```typescript |
||||
|
// Get all schedules |
||||
|
const result = await DailyNotification.getSchedules(); |
||||
|
const allSchedules = result.schedules; |
||||
|
|
||||
|
// Get only enabled notification schedules |
||||
|
const notifyResult = await DailyNotification.getSchedules({ |
||||
|
kind: 'notify', |
||||
|
enabled: true |
||||
|
}); |
||||
|
const enabledNotify = notifyResult.schedules; |
||||
|
|
||||
|
// Get only fetch schedules |
||||
|
const fetchResult = await DailyNotification.getSchedules({ |
||||
|
kind: 'fetch' |
||||
|
}); |
||||
|
const fetchSchedules = fetchResult.schedules; |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<{ schedules: Schedule[] }>` - Note: Array is wrapped in object due to Capacitor serialization |
||||
|
|
||||
|
#### Get Single Schedule |
||||
|
|
||||
|
```typescript |
||||
|
const schedule = await DailyNotification.getSchedule('notify_1234567890'); |
||||
|
if (schedule) { |
||||
|
console.log(`Next run: ${new Date(schedule.nextRunAt)}`); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Schedule | null>` |
||||
|
|
||||
|
#### Create Schedule |
||||
|
|
||||
|
```typescript |
||||
|
const schedule = await DailyNotification.createSchedule({ |
||||
|
kind: 'notify', |
||||
|
cron: '0 9 * * *', // Daily at 9 AM (cron format) |
||||
|
// OR |
||||
|
clockTime: '09:00', // Simple HH:mm format |
||||
|
enabled: true, |
||||
|
jitterMs: 60000, // 1 minute jitter |
||||
|
backoffPolicy: 'exp' |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Schedule>` |
||||
|
|
||||
|
#### Update Schedule |
||||
|
|
||||
|
```typescript |
||||
|
// Update schedule enable state |
||||
|
await DailyNotification.updateSchedule('notify_1234567890', { |
||||
|
enabled: false |
||||
|
}); |
||||
|
|
||||
|
// Update next run time |
||||
|
await DailyNotification.updateSchedule('notify_1234567890', { |
||||
|
nextRunAt: Date.now() + 86400000 // Tomorrow |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Schedule>` |
||||
|
|
||||
|
#### Delete Schedule |
||||
|
|
||||
|
```typescript |
||||
|
await DailyNotification.deleteSchedule('notify_1234567890'); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<void>` |
||||
|
|
||||
|
#### Enable/Disable Schedule |
||||
|
|
||||
|
```typescript |
||||
|
// Disable schedule |
||||
|
await DailyNotification.enableSchedule('notify_1234567890', false); |
||||
|
|
||||
|
// Enable schedule |
||||
|
await DailyNotification.enableSchedule('notify_1234567890', true); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<void>` |
||||
|
|
||||
|
#### Calculate Next Run Time |
||||
|
|
||||
|
```typescript |
||||
|
// Calculate next run from cron expression |
||||
|
const nextRun = await DailyNotification.calculateNextRunTime('0 9 * * *'); |
||||
|
|
||||
|
// Calculate next run from clockTime |
||||
|
const nextRun2 = await DailyNotification.calculateNextRunTime('09:00'); |
||||
|
|
||||
|
console.log(`Next run: ${new Date(nextRun)}`); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<number>` (timestamp in milliseconds) |
||||
|
|
||||
|
### 2. Content Cache Management |
||||
|
|
||||
|
Content cache stores prefetched content for offline-first display. Each entry has a TTL (time-to-live) for freshness validation. |
||||
|
|
||||
|
#### Get Latest Content Cache |
||||
|
|
||||
|
```typescript |
||||
|
const latest = await DailyNotification.getLatestContentCache(); |
||||
|
if (latest) { |
||||
|
const content = JSON.parse(latest.payload); |
||||
|
const age = Date.now() - latest.fetchedAt; |
||||
|
const isFresh = age < (latest.ttlSeconds * 1000); |
||||
|
|
||||
|
console.log(`Content age: ${age}ms, Fresh: ${isFresh}`); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<ContentCache | null>` |
||||
|
|
||||
|
#### Get Content Cache by ID |
||||
|
|
||||
|
```typescript |
||||
|
const cache = await DailyNotification.getContentCacheById({ |
||||
|
id: 'cache_1234567890' |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<ContentCache | null>` |
||||
|
|
||||
|
#### Get Content Cache History |
||||
|
|
||||
|
```typescript |
||||
|
// Get last 10 cache entries |
||||
|
const result = await DailyNotification.getContentCacheHistory(10); |
||||
|
const history = result.history; |
||||
|
|
||||
|
history.forEach(cache => { |
||||
|
console.log(`Cache ${cache.id}: ${new Date(cache.fetchedAt)}`); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<{ history: ContentCache[] }>` |
||||
|
|
||||
|
#### Save Content Cache |
||||
|
|
||||
|
```typescript |
||||
|
const cached = await DailyNotification.saveContentCache({ |
||||
|
payload: JSON.stringify({ |
||||
|
title: 'Daily Update', |
||||
|
body: 'Your daily content is ready!', |
||||
|
data: { /* ... */ } |
||||
|
}), |
||||
|
ttlSeconds: 3600, // 1 hour TTL |
||||
|
meta: 'fetched_from_api' |
||||
|
}); |
||||
|
|
||||
|
console.log(`Cached content with ID: ${cached.id}`); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<ContentCache>` |
||||
|
|
||||
|
#### Clear Content Cache |
||||
|
|
||||
|
```typescript |
||||
|
// Clear all cache entries |
||||
|
await DailyNotification.clearContentCacheEntries(); |
||||
|
|
||||
|
// Clear entries older than 24 hours |
||||
|
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000); |
||||
|
await DailyNotification.clearContentCacheEntries({ |
||||
|
olderThan: oneDayAgo |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<void>` |
||||
|
|
||||
|
### 3. Configuration Management |
||||
|
|
||||
|
**Note**: Configuration management methods (`getConfig`, `setConfig`, etc.) are currently not implemented in the Kotlin database schema. These will be available once the database consolidation is complete (see `android/DATABASE_CONSOLIDATION_PLAN.md`). For now, use the Java-based `DailyNotificationStorageRoom` for configuration storage if needed. |
||||
|
|
||||
|
When implemented, these methods will store plugin settings and user preferences with optional TimeSafari DID scoping. |
||||
|
|
||||
|
#### Get Configuration |
||||
|
|
||||
|
```typescript |
||||
|
// Get config by key |
||||
|
const config = await DailyNotification.getConfig('notification_sound_enabled'); |
||||
|
|
||||
|
if (config) { |
||||
|
const value = config.configDataType === 'boolean' |
||||
|
? config.configValue === 'true' |
||||
|
: config.configValue; |
||||
|
console.log(`Sound enabled: ${value}`); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Config | null>` |
||||
|
|
||||
|
#### Get All Configurations |
||||
|
|
||||
|
```typescript |
||||
|
// Get all configs |
||||
|
const allConfigs = await DailyNotification.getAllConfigs(); |
||||
|
|
||||
|
// Get configs for specific user |
||||
|
const userConfigs = await DailyNotification.getAllConfigs({ |
||||
|
timesafariDid: 'did:ethr:0x...' |
||||
|
}); |
||||
|
|
||||
|
// Get configs by type |
||||
|
const pluginConfigs = await DailyNotification.getAllConfigs({ |
||||
|
configType: 'plugin_setting' |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Config[]>` |
||||
|
|
||||
|
#### Set Configuration |
||||
|
|
||||
|
```typescript |
||||
|
await DailyNotification.setConfig({ |
||||
|
configType: 'user_preference', |
||||
|
configKey: 'notification_sound_enabled', |
||||
|
configValue: 'true', |
||||
|
configDataType: 'boolean', |
||||
|
timesafariDid: 'did:ethr:0x...' // Optional: user-specific |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Config>` |
||||
|
|
||||
|
#### Update Configuration |
||||
|
|
||||
|
```typescript |
||||
|
await DailyNotification.updateConfig( |
||||
|
'notification_sound_enabled', |
||||
|
'false', |
||||
|
{ timesafariDid: 'did:ethr:0x...' } |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Config>` |
||||
|
|
||||
|
#### Delete Configuration |
||||
|
|
||||
|
```typescript |
||||
|
await DailyNotification.deleteConfig('notification_sound_enabled', { |
||||
|
timesafariDid: 'did:ethr:0x...' |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<void>` |
||||
|
|
||||
|
### 4. Callbacks Management |
||||
|
|
||||
|
Callbacks are executed after fetch/notify events. They can be HTTP endpoints, local handlers, or queue destinations. |
||||
|
|
||||
|
#### Get All Callbacks |
||||
|
|
||||
|
```typescript |
||||
|
// Get all callbacks |
||||
|
const result = await DailyNotification.getCallbacks(); |
||||
|
const allCallbacks = result.callbacks; |
||||
|
|
||||
|
// Get only enabled callbacks |
||||
|
const enabledResult = await DailyNotification.getCallbacks({ |
||||
|
enabled: true |
||||
|
}); |
||||
|
const enabledCallbacks = enabledResult.callbacks; |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<{ callbacks: Callback[] }>` |
||||
|
|
||||
|
#### Get Single Callback |
||||
|
|
||||
|
```typescript |
||||
|
const callback = await DailyNotification.getCallback('on_notify_delivered'); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Callback | null>` |
||||
|
|
||||
|
#### Register Callback |
||||
|
|
||||
|
```typescript |
||||
|
await DailyNotification.registerCallbackConfig({ |
||||
|
id: 'on_notify_delivered', |
||||
|
kind: 'http', |
||||
|
target: 'https://api.example.com/webhooks/notify', |
||||
|
headersJson: JSON.stringify({ |
||||
|
'Authorization': 'Bearer token123', |
||||
|
'Content-Type': 'application/json' |
||||
|
}), |
||||
|
enabled: true |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Callback>` |
||||
|
|
||||
|
#### Update Callback |
||||
|
|
||||
|
```typescript |
||||
|
await DailyNotification.updateCallback('on_notify_delivered', { |
||||
|
enabled: false, |
||||
|
headersJson: JSON.stringify({ 'Authorization': 'Bearer newtoken' }) |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<Callback>` |
||||
|
|
||||
|
#### Delete Callback |
||||
|
|
||||
|
```typescript |
||||
|
await DailyNotification.deleteCallback('on_notify_delivered'); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<void>` |
||||
|
|
||||
|
#### Enable/Disable Callback |
||||
|
|
||||
|
```typescript |
||||
|
await DailyNotification.enableCallback('on_notify_delivered', false); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<void>` |
||||
|
|
||||
|
### 5. History/Analytics |
||||
|
|
||||
|
History provides execution logs for debugging and analytics. |
||||
|
|
||||
|
#### Get History |
||||
|
|
||||
|
```typescript |
||||
|
// Get last 50 entries |
||||
|
const result = await DailyNotification.getHistory(); |
||||
|
const history = result.history; |
||||
|
|
||||
|
// Get entries since yesterday |
||||
|
const yesterday = Date.now() - (24 * 60 * 60 * 1000); |
||||
|
const recentResult = await DailyNotification.getHistory({ |
||||
|
since: yesterday, |
||||
|
limit: 100 |
||||
|
}); |
||||
|
const recentHistory = recentResult.history; |
||||
|
|
||||
|
// Get only fetch executions |
||||
|
const fetchResult = await DailyNotification.getHistory({ |
||||
|
kind: 'fetch', |
||||
|
limit: 20 |
||||
|
}); |
||||
|
const fetchHistory = fetchResult.history; |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<{ history: History[] }>` |
||||
|
|
||||
|
#### Get History Statistics |
||||
|
|
||||
|
```typescript |
||||
|
const stats = await DailyNotification.getHistoryStats(); |
||||
|
|
||||
|
console.log(`Total executions: ${stats.totalCount}`); |
||||
|
console.log(`Success rate: ${stats.outcomes.success / stats.totalCount * 100}%`); |
||||
|
console.log(`Fetch executions: ${stats.kinds.fetch}`); |
||||
|
console.log(`Most recent: ${new Date(stats.mostRecent)}`); |
||||
|
``` |
||||
|
|
||||
|
**Returns**: `Promise<HistoryStats>` |
||||
|
|
||||
|
## Type Definitions |
||||
|
|
||||
|
### Schedule |
||||
|
|
||||
|
```typescript |
||||
|
interface Schedule { |
||||
|
id: string; |
||||
|
kind: 'fetch' | 'notify'; |
||||
|
cron?: string; // Cron expression (e.g., "0 9 * * *") |
||||
|
clockTime?: string; // HH:mm format (e.g., "09:00") |
||||
|
enabled: boolean; |
||||
|
lastRunAt?: number; // Timestamp (ms) |
||||
|
nextRunAt?: number; // Timestamp (ms) |
||||
|
jitterMs: number; |
||||
|
backoffPolicy: string; // 'exp', etc. |
||||
|
stateJson?: string; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### ContentCache |
||||
|
|
||||
|
```typescript |
||||
|
interface ContentCache { |
||||
|
id: string; |
||||
|
fetchedAt: number; // Timestamp (ms) |
||||
|
ttlSeconds: number; |
||||
|
payload: string; // JSON string or base64 |
||||
|
meta?: string; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Config |
||||
|
|
||||
|
```typescript |
||||
|
interface Config { |
||||
|
id: string; |
||||
|
timesafariDid?: string; |
||||
|
configType: string; |
||||
|
configKey: string; |
||||
|
configValue: string; |
||||
|
configDataType: string; // 'string' | 'boolean' | 'integer' | etc. |
||||
|
isEncrypted: boolean; |
||||
|
createdAt: number; // Timestamp (ms) |
||||
|
updatedAt: number; // Timestamp (ms) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Callback |
||||
|
|
||||
|
```typescript |
||||
|
interface Callback { |
||||
|
id: string; |
||||
|
kind: 'http' | 'local' | 'queue'; |
||||
|
target: string; |
||||
|
headersJson?: string; |
||||
|
enabled: boolean; |
||||
|
createdAt: number; // Timestamp (ms) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### History |
||||
|
|
||||
|
```typescript |
||||
|
interface History { |
||||
|
id: number; |
||||
|
refId: string; |
||||
|
kind: 'fetch' | 'notify' | 'callback' | 'boot_recovery'; |
||||
|
occurredAt: number; // Timestamp (ms) |
||||
|
durationMs?: number; |
||||
|
outcome: string; // 'success' | 'failure' | etc. |
||||
|
diagJson?: string; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Common Patterns |
||||
|
|
||||
|
### Pattern 1: Check Schedule Status |
||||
|
|
||||
|
```typescript |
||||
|
async function checkScheduleStatus() { |
||||
|
const result = await DailyNotification.getSchedules({ enabled: true }); |
||||
|
const schedules = result.schedules; |
||||
|
|
||||
|
for (const schedule of schedules) { |
||||
|
if (schedule.nextRunAt) { |
||||
|
const nextRun = new Date(schedule.nextRunAt); |
||||
|
const now = new Date(); |
||||
|
const timeUntil = nextRun.getTime() - now.getTime(); |
||||
|
|
||||
|
console.log(`${schedule.kind} schedule ${schedule.id}:`); |
||||
|
console.log(` Next run: ${nextRun}`); |
||||
|
console.log(` Time until: ${Math.round(timeUntil / 1000 / 60)} minutes`); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Pattern 2: Verify Content Freshness |
||||
|
|
||||
|
```typescript |
||||
|
async function isContentFresh(): Promise<boolean> { |
||||
|
const cache = await DailyNotification.getLatestContentCache(); |
||||
|
|
||||
|
if (!cache) { |
||||
|
return false; // No content available |
||||
|
} |
||||
|
|
||||
|
const age = Date.now() - cache.fetchedAt; |
||||
|
const ttlMs = cache.ttlSeconds * 1000; |
||||
|
|
||||
|
return age < ttlMs; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Pattern 3: Update User Preferences |
||||
|
|
||||
|
```typescript |
||||
|
async function updateUserPreferences(did: string, preferences: Record<string, any>) { |
||||
|
for (const [key, value] of Object.entries(preferences)) { |
||||
|
await DailyNotification.setConfig({ |
||||
|
timesafariDid: did, |
||||
|
configType: 'user_preference', |
||||
|
configKey: key, |
||||
|
configValue: String(value), |
||||
|
configDataType: typeof value === 'boolean' ? 'boolean' : 'string' |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Pattern 4: Monitor Execution Health |
||||
|
|
||||
|
```typescript |
||||
|
async function checkExecutionHealth() { |
||||
|
const stats = await DailyNotification.getHistoryStats(); |
||||
|
const recentResult = await DailyNotification.getHistory({ |
||||
|
since: Date.now() - (24 * 60 * 60 * 1000) // Last 24 hours |
||||
|
}); |
||||
|
const recent = recentResult.history; |
||||
|
|
||||
|
const successCount = recent.filter(h => h.outcome === 'success').length; |
||||
|
const failureCount = recent.filter(h => h.outcome === 'failure').length; |
||||
|
const successRate = successCount / recent.length; |
||||
|
|
||||
|
console.log(`24h Success Rate: ${(successRate * 100).toFixed(1)}%`); |
||||
|
console.log(`Successes: ${successCount}, Failures: ${failureCount}`); |
||||
|
|
||||
|
return successRate > 0.9; // Healthy if > 90% success rate |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Error Handling |
||||
|
|
||||
|
All methods return Promises and can reject with errors: |
||||
|
|
||||
|
```typescript |
||||
|
try { |
||||
|
const schedule = await DailyNotification.getSchedule('invalid_id'); |
||||
|
if (!schedule) { |
||||
|
console.log('Schedule not found'); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('Error accessing database:', error); |
||||
|
// Handle error - database might be unavailable, etc. |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Thread Safety |
||||
|
|
||||
|
All database operations are executed on background threads (Kotlin `Dispatchers.IO`). Methods are safe to call from any thread in your TypeScript code. |
||||
|
|
||||
|
## Implementation Status |
||||
|
|
||||
|
### ✅ Implemented |
||||
|
- Schedule management (CRUD operations) |
||||
|
- Content cache management (CRUD operations) |
||||
|
- Callback management (CRUD operations) |
||||
|
- History/analytics (read operations) |
||||
|
|
||||
|
### ⚠️ Pending Database Consolidation |
||||
|
- Configuration management (Config table exists in Java DB, needs to be added to Kotlin schema) |
||||
|
- See `android/DATABASE_CONSOLIDATION_PLAN.md` for full consolidation plan |
||||
|
|
||||
|
## Return Format Notes |
||||
|
|
||||
|
**Important**: Capacitor serializes arrays wrapped in JSObject. Methods that return arrays will return them in this format: |
||||
|
- `getSchedules()` → `{ schedules: Schedule[] }` |
||||
|
- `getCallbacks()` → `{ callbacks: Callback[] }` |
||||
|
- `getHistory()` → `{ history: History[] }` |
||||
|
- `getContentCacheHistory()` → `{ history: ContentCache[] }` |
||||
|
|
||||
|
This is due to Capacitor's serialization mechanism. Always access the array property from the returned object. |
||||
|
|
||||
@ -0,0 +1,157 @@ |
|||||
|
# Database Interfaces Implementation Summary |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: 2025-01-21 |
||||
|
**Status**: ✅ **COMPLETE** - TypeScript interfaces and Android implementations ready |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
The Daily Notification Plugin now exposes comprehensive TypeScript interfaces for accessing its internal SQLite database. Since the plugin owns its database (isolated from host apps), the webview accesses data through Capacitor bridge methods. |
||||
|
|
||||
|
## What Was Implemented |
||||
|
|
||||
|
### ✅ TypeScript Interface Definitions (`src/definitions.ts`) |
||||
|
|
||||
|
Added 30+ database access methods with full type definitions: |
||||
|
|
||||
|
- **Schedule Management**: `getSchedules()`, `createSchedule()`, `updateSchedule()`, `deleteSchedule()`, `enableSchedule()`, `calculateNextRunTime()` |
||||
|
- **Content Cache Management**: `getContentCacheById()`, `getLatestContentCache()`, `getContentCacheHistory()`, `saveContentCache()`, `clearContentCacheEntries()` |
||||
|
- **Callback Management**: `getCallbacks()`, `getCallback()`, `registerCallbackConfig()`, `updateCallback()`, `deleteCallback()`, `enableCallback()` |
||||
|
- **History/Analytics**: `getHistory()`, `getHistoryStats()` |
||||
|
- **Configuration Management**: Stubs for `getConfig()`, `setConfig()`, `updateConfig()`, `deleteConfig()`, `getAllConfigs()` (pending database consolidation) |
||||
|
|
||||
|
### ✅ Android PluginMethods (`DailyNotificationPlugin.kt`) |
||||
|
|
||||
|
Implemented all database access methods: |
||||
|
- All operations run on background threads (`Dispatchers.IO`) for thread safety |
||||
|
- Proper error handling with descriptive error messages |
||||
|
- JSON serialization helpers for entity-to-JSObject conversion |
||||
|
- Filter support (by kind, enabled status, time ranges, etc.) |
||||
|
|
||||
|
### ✅ Database Schema Extensions (`DatabaseSchema.kt`) |
||||
|
|
||||
|
Extended DAOs with additional queries: |
||||
|
- `ScheduleDao`: Added `getAll()`, `getByKind()`, `getByKindAndEnabled()`, `deleteById()`, `update()` |
||||
|
- `ContentCacheDao`: Added `getHistory()`, `deleteAll()` |
||||
|
- `CallbackDao`: Added `getAll()`, `getByEnabled()`, `getById()`, `update()` |
||||
|
- `HistoryDao`: Added `getSinceByKind()`, `getRecent()` |
||||
|
|
||||
|
### ✅ Comprehensive Documentation (`docs/DATABASE_INTERFACES.md`) |
||||
|
|
||||
|
Created 600+ line documentation guide: |
||||
|
- Complete API reference with examples |
||||
|
- Common usage patterns |
||||
|
- Type definitions |
||||
|
- Error handling guidance |
||||
|
- Return format notes (Capacitor serialization) |
||||
|
- Implementation status |
||||
|
|
||||
|
## Key Features |
||||
|
|
||||
|
### For Developers |
||||
|
|
||||
|
- **Type-Safe**: Full TypeScript type definitions |
||||
|
- **Well-Documented**: Comprehensive JSDoc comments and examples |
||||
|
- **Error Handling**: Clear error messages for debugging |
||||
|
- **Thread-Safe**: All operations on background threads |
||||
|
|
||||
|
### For AI Assistants |
||||
|
|
||||
|
- **Clear Structure**: Methods organized by category |
||||
|
- **Comprehensive Examples**: Real-world usage patterns |
||||
|
- **Type Information**: Complete type definitions with JSDoc |
||||
|
- **Architecture Documentation**: Clear explanation of plugin database ownership |
||||
|
|
||||
|
## Usage Example |
||||
|
|
||||
|
```typescript |
||||
|
import { DailyNotification } from '@capacitor-community/daily-notification'; |
||||
|
|
||||
|
// Get all enabled notification schedules |
||||
|
const result = await DailyNotification.getSchedules({ |
||||
|
kind: 'notify', |
||||
|
enabled: true |
||||
|
}); |
||||
|
const schedules = result.schedules; |
||||
|
|
||||
|
// Get latest cached content |
||||
|
const cache = await DailyNotification.getLatestContentCache(); |
||||
|
if (cache) { |
||||
|
const content = JSON.parse(cache.payload); |
||||
|
console.log('Content:', content); |
||||
|
} |
||||
|
|
||||
|
// Create a new schedule |
||||
|
const newSchedule = await DailyNotification.createSchedule({ |
||||
|
kind: 'notify', |
||||
|
cron: '0 9 * * *', // Daily at 9 AM |
||||
|
enabled: true |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
## Implementation Status |
||||
|
|
||||
|
### ✅ Fully Implemented |
||||
|
- Schedule management (CRUD) |
||||
|
- Content cache management (CRUD) |
||||
|
- Callback management (CRUD) |
||||
|
- History/analytics (read operations) |
||||
|
|
||||
|
### ⚠️ Pending Database Consolidation |
||||
|
- Configuration management (Config table exists in Java DB, needs to be added to Kotlin schema) |
||||
|
- See `android/DATABASE_CONSOLIDATION_PLAN.md` for details |
||||
|
|
||||
|
## Architecture Notes |
||||
|
|
||||
|
### Why Plugin Owns Database |
||||
|
|
||||
|
1. **Isolation**: Plugin data is separate from host app data |
||||
|
2. **Reboot Recovery**: Schedules must persist across reboots (Android doesn't persist AlarmManager schedules) |
||||
|
3. **Offline-First**: Cached content available without network |
||||
|
4. **Self-Contained**: Plugin manages its own lifecycle |
||||
|
|
||||
|
### How Webview Accesses Database |
||||
|
|
||||
|
``` |
||||
|
TypeScript/Webview |
||||
|
↓ Capacitor Bridge |
||||
|
Android PluginMethod (@PluginMethod) |
||||
|
↓ Kotlin Coroutines (Dispatchers.IO) |
||||
|
Room Database (Kotlin) |
||||
|
↓ SQLite |
||||
|
daily_notification_plugin.db |
||||
|
``` |
||||
|
|
||||
|
## Files Modified/Created |
||||
|
|
||||
|
1. **`src/definitions.ts`** - Added database interface methods and types |
||||
|
2. **`android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`** - Implemented PluginMethods |
||||
|
3. **`android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt`** - Extended DAOs |
||||
|
4. **`docs/DATABASE_INTERFACES.md`** - Complete documentation |
||||
|
5. **`android/DATABASE_CONSOLIDATION_PLAN.md`** - Updated with interface requirements |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
1. **Complete Database Consolidation**: Merge Java and Kotlin databases into single unified schema |
||||
|
2. **Add Config Table**: Implement Config management methods once consolidated |
||||
|
3. **Testing**: Test all database methods end-to-end |
||||
|
4. **iOS Implementation**: Adapt to iOS when ready |
||||
|
|
||||
|
## Documentation References |
||||
|
|
||||
|
- **Complete API Reference**: `docs/DATABASE_INTERFACES.md` |
||||
|
- **Consolidation Plan**: `android/DATABASE_CONSOLIDATION_PLAN.md` |
||||
|
- **TypeScript Definitions**: `src/definitions.ts` |
||||
|
- **Database Schema**: `android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt` |
||||
|
|
||||
|
## For AI Assistants |
||||
|
|
||||
|
This implementation provides: |
||||
|
- **Clear Interface Contracts**: TypeScript interfaces define exact method signatures |
||||
|
- **Comprehensive Examples**: Every method has usage examples |
||||
|
- **Architecture Context**: Clear explanation of why database is plugin-owned |
||||
|
- **Implementation Details**: Android code shows how methods work internally |
||||
|
- **Error Patterns**: Consistent error handling across all methods |
||||
|
|
||||
|
All interfaces are type-safe, well-documented, and ready for use in projects that integrate this plugin. |
||||
|
|
||||
Loading…
Reference in new issue