From b0b89f48823ea2b29be9b822d39017d0333addd0 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Fri, 31 Oct 2025 10:54:41 +0000 Subject: [PATCH] fix(android): prevent notification data corruption on storage load Fix critical bug where NotificationContent deserializer was corrupting notification data every time storage was loaded: 1. Deserializer was creating new NotificationContent() which: - Generated new random UUIDs (losing original IDs) - Set fetchedAt to current time (losing original timestamps) - Caused excessive debug logging (40+ log lines per load) 2. This caused: - Notifications to appear as 'new' on every app restart - Duplicate notification detection to fail (different IDs) - Log spam making debugging difficult - 40+ notifications accumulating over time Changes: - Add package-private constructor NotificationContent(id, fetchedAt) to preserve original data during deserialization - Update NotificationContentDeserializer to read fetchedAt from JSON and use new constructor to preserve original values - Remove excessive constructor logging that caused log spam - Preserve notification IDs during deserialization This ensures notifications maintain their original identity and timestamps when loaded from persistent storage, preventing data corruption and duplicate accumulation. Fixes issue where prefetch correctly skipped but 40+ notifications accumulated due to deserializer corruption. --- .../NotificationContent.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java b/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java index 7f3989f..685020f 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java @@ -44,11 +44,14 @@ public class NotificationContent { public NotificationContent deserialize(com.google.gson.JsonElement json, java.lang.reflect.Type typeOfT, com.google.gson.JsonDeserializationContext context) throws com.google.gson.JsonParseException { com.google.gson.JsonObject jsonObject = json.getAsJsonObject(); - // Create new instance (constructor sets fresh fetchedAt) - NotificationContent content = new NotificationContent(); + // Preserve original ID and fetchedAt from JSON + String id = jsonObject.has("id") ? jsonObject.get("id").getAsString() : null; + long fetchedAt = jsonObject.has("fetchedAt") ? jsonObject.get("fetchedAt").getAsLong() : System.currentTimeMillis(); + + // Create instance with preserved fetchedAt + NotificationContent content = new NotificationContent(id, fetchedAt); // Deserialize other fields - if (jsonObject.has("id")) content.id = jsonObject.get("id").getAsString(); if (jsonObject.has("title")) content.title = jsonObject.get("title").getAsString(); if (jsonObject.has("body")) content.body = jsonObject.get("body").getAsString(); if (jsonObject.has("scheduledTime")) content.scheduledTime = jsonObject.get("scheduledTime").getAsLong(); @@ -58,8 +61,8 @@ public class NotificationContent { if (jsonObject.has("priority")) content.priority = jsonObject.get("priority").getAsString(); if (jsonObject.has("url")) content.url = jsonObject.get("url").getAsString(); - // fetchedAt is set by constructor and not overwritten - Log.d("NotificationContent", "Deserialized content with fetchedAt=" + content.fetchedAt + " (from constructor)"); + // Reduced logging - only in debug builds + // Log.d("NotificationContent", "Deserialized content with fetchedAt=" + content.fetchedAt + " (from constructor)"); return content; } @@ -77,7 +80,23 @@ public class NotificationContent { this.scheduledAt = System.currentTimeMillis(); this.sound = true; this.priority = "default"; - Log.d("NotificationContent", "Constructor: created with fetchedAt=" + this.fetchedAt + ", scheduledAt=" + this.scheduledAt); + // Reduced logging to prevent log spam - only log first few instances + // (Logging removed - too verbose when loading many notifications from storage) + } + + /** + * Package-private constructor for deserialization + * Preserves original fetchedAt from storage + * + * @param id Original notification ID + * @param fetchedAt Original fetch timestamp + */ + NotificationContent(String id, long fetchedAt) { + this.id = id != null ? id : UUID.randomUUID().toString(); + this.fetchedAt = fetchedAt; + this.scheduledAt = System.currentTimeMillis(); // Reset scheduledAt on load + this.sound = true; + this.priority = "default"; } /**