Browse Source

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.
master
Matthew Raymer 1 day ago
parent
commit
b0b89f4882
  1. 31
      android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java

31
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";
}
/**

Loading…
Cancel
Save