feat(android): consolidate databases and add prefetch scheduling
Consolidate Java and Kotlin database implementations into unified schema, add delayed prefetch scheduling, and fix notification delivery issues. Database Consolidation: - Merge Java DailyNotificationDatabase into Kotlin DatabaseSchema - Add migration path from v1 to v2 unified schema - Include all entities: ContentCache, Schedule, Callback, History, NotificationContentEntity, NotificationDeliveryEntity, NotificationConfigEntity - Add @JvmStatic getInstance() for Java interoperability - Update DailyNotificationWorker and DailyNotificationStorageRoom to use unified database Prefetch Functionality: - Add scheduleDelayedFetch() to FetchWorker for 5-minute prefetch before notifications - Support delayed WorkManager scheduling with initialDelay - Update scheduleDailyNotification() to optionally schedule prefetch when URL is provided Notification Delivery Fixes: - Register NotifyReceiver in AndroidManifest.xml (was missing, causing notifications not to fire) - Add safe database initialization with lazy getDatabase() helper - Prevent PluginLoadException on database init failure Build Configuration: - Add kotlin-android and kotlin-kapt plugins - Configure Room annotation processor (kapt) for Kotlin - Add Room KTX dependency for coroutines support - Fix Gradle settings with pluginManagement blocks Plugin Methods Added: - checkPermissionStatus() - detailed permission status - requestNotificationPermissions() - request POST_NOTIFICATIONS - scheduleDailyNotification() - schedule with AlarmManager - configureNativeFetcher() - configure native content fetcher - Various status and configuration methods Code Cleanup: - Remove duplicate BootReceiver.java (keep Kotlin version) - Remove duplicate DailyNotificationPlugin.java (keep Kotlin version) - Remove old Java database implementation - Add native fetcher SPI registry (@JvmStatic methods) The unified database ensures schedule persistence across reboots and provides a single source of truth for all plugin data. Prefetch scheduling enables content caching before notifications fire, improving offline-first reliability.
This commit is contained in:
@@ -90,6 +90,17 @@ export interface PermissionStatus {
|
||||
carPlay?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission status result for checkPermissionStatus()
|
||||
* Returns boolean flags for each permission type
|
||||
*/
|
||||
export interface PermissionStatusResult {
|
||||
notificationsEnabled: boolean;
|
||||
exactAlarmEnabled: boolean;
|
||||
wakeLockEnabled: boolean;
|
||||
allPermissionsGranted: boolean;
|
||||
}
|
||||
|
||||
// Static Daily Reminder Interfaces
|
||||
export interface DailyReminderOptions {
|
||||
id: string;
|
||||
@@ -280,6 +291,188 @@ export interface ContentFetchResult {
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DATABASE TYPE DEFINITIONS
|
||||
// ============================================================================
|
||||
// These types represent the plugin's internal SQLite database schema.
|
||||
// The plugin owns its database, and these types are used for TypeScript
|
||||
// access through Capacitor interfaces.
|
||||
//
|
||||
// See: docs/DATABASE_INTERFACES.md for complete documentation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Recurring schedule pattern stored in database
|
||||
* Used to restore schedules after device reboot
|
||||
*/
|
||||
export interface Schedule {
|
||||
/** Unique schedule identifier */
|
||||
id: string;
|
||||
/** Schedule type: 'fetch' for content fetching, 'notify' for notifications */
|
||||
kind: 'fetch' | 'notify';
|
||||
/** Cron expression (e.g., "0 9 * * *" for daily at 9 AM) */
|
||||
cron?: string;
|
||||
/** Clock time in HH:mm format (e.g., "09:00") */
|
||||
clockTime?: string;
|
||||
/** Whether schedule is enabled */
|
||||
enabled: boolean;
|
||||
/** Timestamp of last execution (milliseconds since epoch) */
|
||||
lastRunAt?: number;
|
||||
/** Timestamp of next scheduled execution (milliseconds since epoch) */
|
||||
nextRunAt?: number;
|
||||
/** Random jitter in milliseconds for timing variation */
|
||||
jitterMs: number;
|
||||
/** Backoff policy ('exp' for exponential, etc.) */
|
||||
backoffPolicy: string;
|
||||
/** Optional JSON state for advanced scheduling */
|
||||
stateJson?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for creating a new schedule
|
||||
*/
|
||||
export interface CreateScheduleInput {
|
||||
kind: 'fetch' | 'notify';
|
||||
cron?: string;
|
||||
clockTime?: string;
|
||||
enabled?: boolean;
|
||||
jitterMs?: number;
|
||||
backoffPolicy?: string;
|
||||
stateJson?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content cache entry with TTL
|
||||
* Stores prefetched content for offline-first display
|
||||
*/
|
||||
export interface ContentCache {
|
||||
/** Unique cache identifier */
|
||||
id: string;
|
||||
/** Timestamp when content was fetched (milliseconds since epoch) */
|
||||
fetchedAt: number;
|
||||
/** Time-to-live in seconds */
|
||||
ttlSeconds: number;
|
||||
/** Content payload (JSON string or base64 encoded) */
|
||||
payload: string;
|
||||
/** Optional metadata */
|
||||
meta?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for creating a content cache entry
|
||||
*/
|
||||
export interface CreateContentCacheInput {
|
||||
id?: string; // Auto-generated if not provided
|
||||
payload: string;
|
||||
ttlSeconds: number;
|
||||
meta?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin configuration entry
|
||||
* Stores user preferences and plugin settings
|
||||
*/
|
||||
export interface Config {
|
||||
/** Unique configuration identifier */
|
||||
id: string;
|
||||
/** Optional TimeSafari DID for user-specific configs */
|
||||
timesafariDid?: string;
|
||||
/** Configuration type (e.g., 'plugin_setting', 'user_preference') */
|
||||
configType: string;
|
||||
/** Configuration key */
|
||||
configKey: string;
|
||||
/** Configuration value (stored as string, parsed based on configDataType) */
|
||||
configValue: string;
|
||||
/** Data type: 'string' | 'boolean' | 'integer' | 'long' | 'float' | 'double' | 'json' */
|
||||
configDataType: string;
|
||||
/** Whether value is encrypted */
|
||||
isEncrypted: boolean;
|
||||
/** Timestamp when config was created (milliseconds since epoch) */
|
||||
createdAt: number;
|
||||
/** Timestamp when config was last updated (milliseconds since epoch) */
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for creating a configuration entry
|
||||
*/
|
||||
export interface CreateConfigInput {
|
||||
id?: string; // Auto-generated if not provided
|
||||
timesafariDid?: string;
|
||||
configType: string;
|
||||
configKey: string;
|
||||
configValue: string;
|
||||
configDataType?: string; // Defaults to 'string' if not provided
|
||||
isEncrypted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback configuration
|
||||
* Stores callback endpoint configurations for execution after events
|
||||
*/
|
||||
export interface Callback {
|
||||
/** Unique callback identifier */
|
||||
id: string;
|
||||
/** Callback type: 'http' for HTTP requests, 'local' for local handlers, 'queue' for queue */
|
||||
kind: 'http' | 'local' | 'queue';
|
||||
/** Target URL or identifier */
|
||||
target: string;
|
||||
/** Optional JSON headers for HTTP callbacks */
|
||||
headersJson?: string;
|
||||
/** Whether callback is enabled */
|
||||
enabled: boolean;
|
||||
/** Timestamp when callback was created (milliseconds since epoch) */
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for creating a callback configuration
|
||||
*/
|
||||
export interface CreateCallbackInput {
|
||||
id: string;
|
||||
kind: 'http' | 'local' | 'queue';
|
||||
target: string;
|
||||
headersJson?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution history entry
|
||||
* Logs fetch/notify/callback execution for debugging and analytics
|
||||
*/
|
||||
export interface History {
|
||||
/** Auto-incrementing history ID */
|
||||
id: number;
|
||||
/** Reference ID (content ID, schedule ID, etc.) */
|
||||
refId: string;
|
||||
/** Execution kind: 'fetch' | 'notify' | 'callback' | 'boot_recovery' */
|
||||
kind: 'fetch' | 'notify' | 'callback' | 'boot_recovery';
|
||||
/** Timestamp when execution occurred (milliseconds since epoch) */
|
||||
occurredAt: number;
|
||||
/** Execution duration in milliseconds */
|
||||
durationMs?: number;
|
||||
/** Outcome: 'success' | 'failure' | 'skipped_ttl' | 'circuit_open' */
|
||||
outcome: string;
|
||||
/** Optional JSON diagnostics */
|
||||
diagJson?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* History statistics
|
||||
*/
|
||||
export interface HistoryStats {
|
||||
/** Total number of history entries */
|
||||
totalCount: number;
|
||||
/** Count by outcome */
|
||||
outcomes: Record<string, number>;
|
||||
/** Count by kind */
|
||||
kinds: Record<string, number>;
|
||||
/** Most recent execution timestamp */
|
||||
mostRecent?: number;
|
||||
/** Oldest execution timestamp */
|
||||
oldest?: number;
|
||||
}
|
||||
|
||||
export interface DualScheduleStatus {
|
||||
contentFetch: {
|
||||
isEnabled: boolean;
|
||||
@@ -422,6 +615,11 @@ export interface DailyNotificationPlugin {
|
||||
getPowerState(): Promise<PowerState>;
|
||||
checkPermissions(): Promise<PermissionStatus>;
|
||||
requestPermissions(): Promise<PermissionStatus>;
|
||||
checkPermissionStatus(): Promise<PermissionStatusResult>;
|
||||
requestNotificationPermissions(): Promise<PermissionStatus>;
|
||||
isChannelEnabled(channelId?: string): Promise<{ enabled: boolean; channelId: string }>;
|
||||
openChannelSettings(channelId?: string): Promise<void>;
|
||||
checkStatus(): Promise<NotificationStatus>;
|
||||
|
||||
// New dual scheduling methods
|
||||
scheduleContentFetch(config: ContentFetchConfig): Promise<void>;
|
||||
@@ -443,6 +641,272 @@ export interface DailyNotificationPlugin {
|
||||
unregisterCallback(name: string): Promise<void>;
|
||||
getRegisteredCallbacks(): Promise<string[]>;
|
||||
|
||||
// ============================================================================
|
||||
// DATABASE ACCESS METHODS
|
||||
// ============================================================================
|
||||
// These methods provide TypeScript/JavaScript access to the plugin's internal
|
||||
// SQLite database. Since the plugin owns its database, the host app/webview
|
||||
// accesses data through these Capacitor interfaces.
|
||||
//
|
||||
// Usage Pattern:
|
||||
// import { DailyNotification } from '@capacitor-community/daily-notification';
|
||||
// const schedules = await DailyNotification.getSchedules({ kind: 'notify' });
|
||||
//
|
||||
// See: docs/DATABASE_INTERFACES.md for complete documentation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all schedules matching optional filters
|
||||
*
|
||||
* @param options Optional filters:
|
||||
* - kind: Filter by schedule type ('fetch' | 'notify')
|
||||
* - enabled: Filter by enabled status (true = only enabled, false = only disabled, undefined = all)
|
||||
* @returns Promise resolving to object with schedules array: { schedules: Schedule[] }
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get all enabled notification schedules
|
||||
* const result = await DailyNotification.getSchedules({
|
||||
* kind: 'notify',
|
||||
* enabled: true
|
||||
* });
|
||||
* const schedules = result.schedules;
|
||||
* ```
|
||||
*/
|
||||
getSchedules(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: Schedule[] }>;
|
||||
|
||||
/**
|
||||
* Get a single schedule by ID
|
||||
*
|
||||
* @param id Schedule ID
|
||||
* @returns Promise resolving to Schedule object or null if not found
|
||||
*/
|
||||
getSchedule(id: string): Promise<Schedule | null>;
|
||||
|
||||
/**
|
||||
* Create a new recurring schedule
|
||||
*
|
||||
* @param schedule Schedule configuration
|
||||
* @returns Promise resolving to created Schedule object
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const schedule = await DailyNotification.createSchedule({
|
||||
* kind: 'notify',
|
||||
* cron: '0 9 * * *', // Daily at 9 AM
|
||||
* enabled: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
createSchedule(schedule: CreateScheduleInput): Promise<Schedule>;
|
||||
|
||||
/**
|
||||
* Update an existing schedule
|
||||
*
|
||||
* @param id Schedule ID
|
||||
* @param updates Partial schedule updates
|
||||
* @returns Promise resolving to updated Schedule object
|
||||
*/
|
||||
updateSchedule(id: string, updates: Partial<Schedule>): Promise<Schedule>;
|
||||
|
||||
/**
|
||||
* Delete a schedule
|
||||
*
|
||||
* @param id Schedule ID
|
||||
* @returns Promise resolving when deletion completes
|
||||
*/
|
||||
deleteSchedule(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Enable or disable a schedule
|
||||
*
|
||||
* @param id Schedule ID
|
||||
* @param enabled Enable state
|
||||
* @returns Promise resolving when update completes
|
||||
*/
|
||||
enableSchedule(id: string, enabled: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Calculate next run time from a cron expression or clockTime
|
||||
*
|
||||
* @param schedule Cron expression (e.g., "0 9 * * *") or clockTime (e.g., "09:00")
|
||||
* @returns Promise resolving to timestamp (milliseconds since epoch)
|
||||
*/
|
||||
calculateNextRunTime(schedule: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* Get content cache by ID or latest cache
|
||||
*
|
||||
* @param options Optional filters:
|
||||
* - id: Specific cache ID (if not provided, returns latest)
|
||||
* @returns Promise resolving to ContentCache object or null
|
||||
*/
|
||||
getContentCacheById(options?: { id?: string }): Promise<ContentCache | null>;
|
||||
|
||||
/**
|
||||
* Get the latest content cache entry
|
||||
*
|
||||
* @returns Promise resolving to latest ContentCache object or null
|
||||
*/
|
||||
getLatestContentCache(): Promise<ContentCache | null>;
|
||||
|
||||
/**
|
||||
* Get content cache history
|
||||
*
|
||||
* @param limit Maximum number of entries to return (default: 10)
|
||||
* @returns Promise resolving to object with history array: { history: ContentCache[] }
|
||||
*/
|
||||
getContentCacheHistory(limit?: number): Promise<{ history: ContentCache[] }>;
|
||||
|
||||
/**
|
||||
* Save content to cache
|
||||
*
|
||||
* @param content Content cache data
|
||||
* @returns Promise resolving to saved ContentCache object
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await DailyNotification.saveContentCache({
|
||||
* id: 'cache_123',
|
||||
* payload: JSON.stringify({ title: 'Hello', body: 'World' }),
|
||||
* ttlSeconds: 3600,
|
||||
* meta: 'fetched_from_api'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
saveContentCache(content: CreateContentCacheInput): Promise<ContentCache>;
|
||||
|
||||
/**
|
||||
* Clear content cache entries
|
||||
*
|
||||
* @param options Optional filters:
|
||||
* - olderThan: Only clear entries older than this timestamp (milliseconds)
|
||||
* @returns Promise resolving when cleanup completes
|
||||
*/
|
||||
clearContentCacheEntries(options?: { olderThan?: number }): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get configuration value
|
||||
*
|
||||
* @param key Configuration key
|
||||
* @param options Optional filters:
|
||||
* - timesafariDid: Filter by TimeSafari DID
|
||||
* @returns Promise resolving to Config object or null
|
||||
*/
|
||||
getConfig(key: string, options?: { timesafariDid?: string }): Promise<Config | null>;
|
||||
|
||||
/**
|
||||
* Get all configurations matching filters
|
||||
*
|
||||
* @param options Optional filters:
|
||||
* - timesafariDid: Filter by TimeSafari DID
|
||||
* - configType: Filter by configuration type
|
||||
* @returns Promise resolving to array of Config objects
|
||||
*/
|
||||
getAllConfigs(options?: { timesafariDid?: string; configType?: string }): Promise<{ configs: Config[] }>;
|
||||
|
||||
/**
|
||||
* Set configuration value
|
||||
*
|
||||
* @param config Configuration data
|
||||
* @returns Promise resolving to saved Config object
|
||||
*/
|
||||
setConfig(config: CreateConfigInput): Promise<Config>;
|
||||
|
||||
/**
|
||||
* Update configuration value
|
||||
*
|
||||
* @param key Configuration key
|
||||
* @param value New value (will be stringified based on dataType)
|
||||
* @param options Optional filters:
|
||||
* - timesafariDid: Filter by TimeSafari DID
|
||||
* @returns Promise resolving to updated Config object
|
||||
*/
|
||||
updateConfig(key: string, value: string, options?: { timesafariDid?: string }): Promise<Config>;
|
||||
|
||||
/**
|
||||
* Delete configuration
|
||||
*
|
||||
* @param key Configuration key
|
||||
* @param options Optional filters:
|
||||
* - timesafariDid: Filter by TimeSafari DID
|
||||
* @returns Promise resolving when deletion completes
|
||||
*/
|
||||
deleteConfig(key: string, options?: { timesafariDid?: string }): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get all callbacks matching filters
|
||||
*
|
||||
* @param options Optional filters:
|
||||
* - enabled: Filter by enabled status
|
||||
* @returns Promise resolving to object with callbacks array: { callbacks: Callback[] }
|
||||
*/
|
||||
getCallbacks(options?: { enabled?: boolean }): Promise<{ callbacks: Callback[] }>;
|
||||
|
||||
/**
|
||||
* Get a single callback by ID
|
||||
*
|
||||
* @param id Callback ID
|
||||
* @returns Promise resolving to Callback object or null
|
||||
*/
|
||||
getCallback(id: string): Promise<Callback | null>;
|
||||
|
||||
/**
|
||||
* Register a new callback
|
||||
*
|
||||
* @param callback Callback configuration
|
||||
* @returns Promise resolving to created Callback object
|
||||
*/
|
||||
registerCallbackConfig(callback: CreateCallbackInput): Promise<Callback>;
|
||||
|
||||
/**
|
||||
* Update an existing callback
|
||||
*
|
||||
* @param id Callback ID
|
||||
* @param updates Partial callback updates
|
||||
* @returns Promise resolving to updated Callback object
|
||||
*/
|
||||
updateCallback(id: string, updates: Partial<Callback>): Promise<Callback>;
|
||||
|
||||
/**
|
||||
* Delete a callback
|
||||
*
|
||||
* @param id Callback ID
|
||||
* @returns Promise resolving when deletion completes
|
||||
*/
|
||||
deleteCallback(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Enable or disable a callback
|
||||
*
|
||||
* @param id Callback ID
|
||||
* @param enabled Enable state
|
||||
* @returns Promise resolving when update completes
|
||||
*/
|
||||
enableCallback(id: string, enabled: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get execution history
|
||||
*
|
||||
* @param options Optional filters:
|
||||
* - since: Only return entries after this timestamp (milliseconds)
|
||||
* - kind: Filter by execution kind ('fetch' | 'notify' | 'callback')
|
||||
* - limit: Maximum number of entries to return (default: 50)
|
||||
* @returns Promise resolving to object with history array: { history: History[] }
|
||||
*/
|
||||
getHistory(options?: {
|
||||
since?: number;
|
||||
kind?: 'fetch' | 'notify' | 'callback';
|
||||
limit?: number;
|
||||
}): Promise<{ history: History[] }>;
|
||||
|
||||
/**
|
||||
* Get history statistics
|
||||
*
|
||||
* @returns Promise resolving to history statistics
|
||||
*/
|
||||
getHistoryStats(): Promise<HistoryStats>;
|
||||
|
||||
// Phase 1: ActiveDid Management Methods (Option A Implementation)
|
||||
setActiveDidFromHost(activeDid: string): Promise<void>;
|
||||
onActiveDidChange(callback: (newActiveDid: string) => Promise<void>): void;
|
||||
|
||||
Reference in New Issue
Block a user