fix(android): configure native fetcher, use DailyNotificationFetchWorker, and cancel notifications on dismiss

Fix three critical issues in the Android notification system:

1. configureNativeFetcher() now actually calls nativeFetcher.configure() method
   - Previously only stored config in database without configuring fetcher instance
   - Added synchronous configure() call with proper error handling
   - Stores valid but empty config entry if configure() fails to prevent downstream errors
   - Adds FETCHER|CONFIGURE_START and FETCHER|CONFIGURE_COMPLETE instrumentation logs

2. Prefetch operations now use DailyNotificationFetchWorker instead of legacy FetchWorker
   - Replaced FetchWorker.scheduleDelayedFetch() with WorkManager scheduling
   - Uses correct input data format (scheduled_time, fetch_time, retry_count, immediate)
   - Enables native fetcher SPI to be used for prefetch operations
   - Handles both delayed and immediate prefetch scenarios

3. Notification dismiss now cancels notification from NotificationManager
   - Added notification cancellation before removing from storage
   - Uses notificationId.hashCode() to match display notification ID
   - Ensures notification disappears immediately when dismiss button is clicked
   - Adds DN|DISMISS_CANCEL_NOTIF instrumentation log

Version bump: 1.0.8 → 1.0.11
This commit is contained in:
Matthew Raymer
2025-11-11 08:06:59 +00:00
parent a5fdf8c5b9
commit 1b34f1f34a
3 changed files with 99 additions and 28 deletions

View File

@@ -17,6 +17,10 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.WorkManager 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.JSObject
import com.getcapacitor.Plugin import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall import com.getcapacitor.PluginCall
@@ -496,17 +500,41 @@ open class DailyNotificationPlugin : Plugin() {
Log.i(TAG, "Configuring native fetcher: apiBaseUrl=$apiBaseUrl, activeDid=$activeDid") Log.i(TAG, "Configuring native fetcher: apiBaseUrl=$apiBaseUrl, activeDid=$activeDid")
// Call the native fetcher's configure method // Call the native fetcher's configure method FIRST
// Note: This assumes the native fetcher has a configure method // This configures the fetcher instance with API credentials for background operations
// If the native fetcher interface doesn't have configure, we'll need to handle it differently var configureSuccess = false
var configureError: Exception? = null
try { try {
// Store configuration in database for later use 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 configId = "native_fetcher_config"
val configValue = JSONObject().apply { val configValue = if (configureSuccess) {
// Store actual configuration values
JSONObject().apply {
put("apiBaseUrl", apiBaseUrl) put("apiBaseUrl", apiBaseUrl)
put("activeDid", activeDid) put("activeDid", activeDid)
put("jwtToken", jwtToken) put("jwtToken", jwtToken)
}.toString() }.toString()
} 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 { CoroutineScope(Dispatchers.IO).launch {
try { try {
@@ -514,16 +542,18 @@ open class DailyNotificationPlugin : Plugin() {
configId, null, "native_fetcher", "config", configValue, "json" configId, null, "native_fetcher", "config", configValue, "json"
) )
getDatabase().notificationConfigDao().insertConfig(config) getDatabase().notificationConfigDao().insertConfig(config)
if (configureSuccess) {
call.resolve() call.resolve()
} else {
// Configure failed but we stored a valid entry - reject with error details
call.reject("Native fetcher configure() failed: ${configureError?.message}")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to store native fetcher config", e) Log.e(TAG, "Failed to store native fetcher config", e)
call.reject("Failed to store configuration: ${e.message}") 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) { } catch (e: Exception) {
Log.e(TAG, "Configure native fetcher error", e) Log.e(TAG, "Configure native fetcher error", e)
call.reject("Configuration error: ${e.message}") call.reject("Configuration error: ${e.message}")
@@ -1257,15 +1287,46 @@ open class DailyNotificationPlugin : Plugin() {
) )
// Always schedule prefetch 5 minutes before notification // 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 val fetchTime = nextRunTime - (5 * 60 * 1000L) // 5 minutes before
FetchWorker.scheduleDelayedFetch( val delayMs = fetchTime - System.currentTimeMillis()
context,
fetchTime, if (delayMs > 0) {
nextRunTime, // Schedule delayed prefetch
url // Can be null - FetchWorker will generate mock content val inputData = Data.Builder()
) .putLong("scheduled_time", nextRunTime)
Log.i(TAG, "Prefetch scheduled: fetchTime=$fetchTime, notificationTime=$nextRunTime, url=${url ?: "none (will generate mock)"}") .putLong("fetch_time", fetchTime)
.putInt("retry_count", 0)
.putBoolean("immediate", false)
.build()
val workRequest = OneTimeWorkRequestBuilder<DailyNotificationFetchWorker>()
.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<DailyNotificationFetchWorker>()
.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 // Store schedule in database
val schedule = Schedule( val schedule = Schedule(

View File

@@ -179,6 +179,16 @@ public class DailyNotificationWorker extends Worker {
try { try {
Log.d(TAG, "DN|DISMISS_START id=" + notificationId); 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 // Remove from Room if present; also remove from legacy storage for compatibility
try { try {
DailyNotificationStorageRoom room = new DailyNotificationStorageRoom(getApplicationContext()); DailyNotificationStorageRoom room = new DailyNotificationStorageRoom(getApplicationContext());

View File

@@ -1,6 +1,6 @@
{ {
"name": "@timesafari/daily-notification-plugin", "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", "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", "main": "dist/plugin.js",
"module": "dist/esm/index.js", "module": "dist/esm/index.js",