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.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<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
val schedule = Schedule(

View File

@@ -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());