diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt index 6f0846c..49dc010 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt @@ -17,6 +17,10 @@ import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.work.WorkManager +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.Data +import java.util.concurrent.TimeUnit +import com.timesafari.dailynotification.DailyNotificationFetchWorker import com.getcapacitor.JSObject import com.getcapacitor.Plugin import com.getcapacitor.PluginCall @@ -496,33 +500,59 @@ open class DailyNotificationPlugin : Plugin() { Log.i(TAG, "Configuring native fetcher: apiBaseUrl=$apiBaseUrl, activeDid=$activeDid") - // Call the native fetcher's configure method - // Note: This assumes the native fetcher has a configure method - // If the native fetcher interface doesn't have configure, we'll need to handle it differently + // Call the native fetcher's configure method FIRST + // This configures the fetcher instance with API credentials for background operations + var configureSuccess = false + var configureError: Exception? = null + try { - // Store configuration in database for later use - val configId = "native_fetcher_config" - val configValue = JSONObject().apply { + Log.d(TAG, "FETCHER|CONFIGURE_START apiBaseUrl=$apiBaseUrl, activeDid=${activeDid.take(30)}...") + nativeFetcher.configure(apiBaseUrl, activeDid, jwtToken) + configureSuccess = true + Log.i(TAG, "FETCHER|CONFIGURE_COMPLETE success=true") + } catch (e: Exception) { + configureError = e + Log.e(TAG, "FETCHER|CONFIGURE_COMPLETE success=false error=${e.message}", e) + // Continue to store empty config entry - don't fail the entire operation + } + + // Store configuration in database for persistence across app restarts + // If configure() failed, store a valid but empty entry that won't cause errors + val configId = "native_fetcher_config" + val configValue = if (configureSuccess) { + // Store actual configuration values + JSONObject().apply { put("apiBaseUrl", apiBaseUrl) put("activeDid", activeDid) put("jwtToken", jwtToken) }.toString() - - CoroutineScope(Dispatchers.IO).launch { - try { - val config = com.timesafari.dailynotification.entities.NotificationConfigEntity( - configId, null, "native_fetcher", "config", configValue, "json" - ) - getDatabase().notificationConfigDao().insertConfig(config) + } else { + // Store valid but empty entry to prevent errors in code that reads this config + JSONObject().apply { + put("apiBaseUrl", "") + put("activeDid", "") + put("jwtToken", "") + put("configureError", configureError?.message ?: "Unknown error") + }.toString() + } + + CoroutineScope(Dispatchers.IO).launch { + try { + val config = com.timesafari.dailynotification.entities.NotificationConfigEntity( + configId, null, "native_fetcher", "config", configValue, "json" + ) + getDatabase().notificationConfigDao().insertConfig(config) + + if (configureSuccess) { call.resolve() - } catch (e: Exception) { - Log.e(TAG, "Failed to store native fetcher config", e) - call.reject("Failed to store configuration: ${e.message}") + } else { + // Configure failed but we stored a valid entry - reject with error details + call.reject("Native fetcher configure() failed: ${configureError?.message}") } + } catch (e: Exception) { + Log.e(TAG, "Failed to store native fetcher config", e) + call.reject("Failed to store configuration: ${e.message}") } - } catch (e: Exception) { - Log.e(TAG, "Native fetcher configuration failed", e) - call.reject("Native fetcher configuration failed: ${e.message}") } } catch (e: Exception) { Log.e(TAG, "Configure native fetcher error", e) @@ -1257,15 +1287,46 @@ open class DailyNotificationPlugin : Plugin() { ) // Always schedule prefetch 5 minutes before notification - // (URL is optional - generates mock content if not provided) + // (URL is optional - native fetcher will be used if registered) val fetchTime = nextRunTime - (5 * 60 * 1000L) // 5 minutes before - FetchWorker.scheduleDelayedFetch( - context, - fetchTime, - nextRunTime, - url // Can be null - FetchWorker will generate mock content - ) - Log.i(TAG, "Prefetch scheduled: fetchTime=$fetchTime, notificationTime=$nextRunTime, url=${url ?: "none (will generate mock)"}") + val delayMs = fetchTime - System.currentTimeMillis() + + if (delayMs > 0) { + // Schedule delayed prefetch + val inputData = Data.Builder() + .putLong("scheduled_time", nextRunTime) + .putLong("fetch_time", fetchTime) + .putInt("retry_count", 0) + .putBoolean("immediate", false) + .build() + + val workRequest = OneTimeWorkRequestBuilder() + .setInitialDelay(delayMs, TimeUnit.MILLISECONDS) + .setInputData(inputData) + .addTag("prefetch") + .build() + + WorkManager.getInstance(context).enqueue(workRequest) + + Log.i(TAG, "Prefetch scheduled: fetchTime=$fetchTime, notificationTime=$nextRunTime, delayMs=$delayMs, using native fetcher") + } else { + // Fetch time is in the past, schedule immediate fetch + val inputData = Data.Builder() + .putLong("scheduled_time", nextRunTime) + .putLong("fetch_time", System.currentTimeMillis()) + .putInt("retry_count", 0) + .putBoolean("immediate", true) + .build() + + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(inputData) + .addTag("prefetch") + .build() + + WorkManager.getInstance(context).enqueue(workRequest) + + Log.i(TAG, "Immediate prefetch scheduled: notificationTime=$nextRunTime, using native fetcher") + } // Store schedule in database val schedule = Schedule( diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java index 14e4c5f..139855e 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java @@ -179,6 +179,16 @@ public class DailyNotificationWorker extends Worker { try { Log.d(TAG, "DN|DISMISS_START id=" + notificationId); + // Cancel the notification from NotificationManager FIRST + // This ensures the notification disappears immediately when dismissed + NotificationManager notificationManager = + (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + int systemNotificationId = notificationId.hashCode(); + notificationManager.cancel(systemNotificationId); + Log.d(TAG, "DN|DISMISS_CANCEL_NOTIF systemId=" + systemNotificationId); + } + // Remove from Room if present; also remove from legacy storage for compatibility try { DailyNotificationStorageRoom room = new DailyNotificationStorageRoom(getApplicationContext()); diff --git a/package.json b/package.json index c8e2e28..aa73020 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@timesafari/daily-notification-plugin", - "version": "1.0.8", + "version": "1.0.11", "description": "TimeSafari Daily Notification Plugin - Enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Mobile (Capacitor) and Desktop (Electron) platforms", "main": "dist/plugin.js", "module": "dist/esm/index.js",