Browse Source

fix(notifications): resolve TTL violation and timestamp issues

- Separate fetchedAt (immutable) and scheduledAt (mutable) timestamps
- Add custom JsonDeserializer to ensure fetchedAt is set by constructor
- Add transient fetchTime field for Gson compatibility
- Update TTL enforcer to use fetchedAt for freshness checks
- Increase DEFAULT_TTL_SECONDS to 25 hours for daily notifications
- Update storage to use custom Gson deserializer
- Add debug logging for timestamp validation
- Fix timestamp initialization in NotificationContent constructor

This resolves the TTL_VIOLATION error that was preventing
notifications from being scheduled due to stale timestamp data.
master
Matthew Raymer 1 week ago
parent
commit
92c843b07e
  1. 6
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java
  2. 6
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcer.java
  3. 84
      android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java

6
android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java

@ -59,7 +59,10 @@ public class DailyNotificationStorage {
public DailyNotificationStorage(Context context) {
this.context = context;
this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
this.gson = new Gson();
// Create Gson with custom deserializer for NotificationContent
com.google.gson.GsonBuilder gsonBuilder = new com.google.gson.GsonBuilder();
gsonBuilder.registerTypeAdapter(NotificationContent.class, new NotificationContent.NotificationContentDeserializer());
this.gson = gsonBuilder.create();
this.notificationCache = new ConcurrentHashMap<>();
this.notificationList = Collections.synchronizedList(new ArrayList<>());
@ -375,6 +378,7 @@ public class DailyNotificationStorage {
private void loadNotificationsFromStorage() {
try {
String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]");
Log.d(TAG, "Loading notifications from storage: " + notificationsJson);
Type type = new TypeToken<ArrayList<NotificationContent>>(){}.getType();
List<NotificationContent> notifications = gson.fromJson(notificationsJson, type);

6
android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcer.java

@ -32,9 +32,9 @@ public class DailyNotificationTTLEnforcer {
private static final String LOG_CODE_TTL_VIOLATION = "TTL_VIOLATION";
// Default TTL values
private static final long DEFAULT_TTL_SECONDS = 3600; // 1 hour
private static final long DEFAULT_TTL_SECONDS = 90000; // 25 hours (for daily notifications)
private static final long MIN_TTL_SECONDS = 60; // 1 minute
private static final long MAX_TTL_SECONDS = 86400; // 24 hours
private static final long MAX_TTL_SECONDS = 172800; // 48 hours
private final Context context;
private final DailyNotificationDatabase database;
@ -120,7 +120,7 @@ public class DailyNotificationTTLEnforcer {
try {
String slotId = notificationContent.getId();
long scheduledTime = notificationContent.getScheduledTime();
long fetchedAt = notificationContent.getFetchTime();
long fetchedAt = notificationContent.getFetchedAt();
Log.d(TAG, String.format("Validating freshness before arming: slot=%s, scheduled=%d, fetched=%d",
slotId, scheduledTime, fetchedAt));

84
android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java

@ -10,6 +10,7 @@
package com.timesafari.dailynotification;
import android.util.Log;
import java.util.UUID;
/**
@ -30,7 +31,39 @@ public class NotificationContent {
private String body;
private long scheduledTime;
private String mediaUrl;
private long fetchTime;
private final long fetchedAt; // When content was fetched (immutable)
private long scheduledAt; // When this instance was scheduled
// Gson will try to deserialize this field, but we ignore it to keep fetchedAt immutable
@SuppressWarnings("unused")
private transient long fetchTime; // Legacy field for Gson compatibility (ignored)
// Custom deserializer to handle fetchedAt field
public static class NotificationContentDeserializer implements com.google.gson.JsonDeserializer<NotificationContent> {
@Override
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();
// 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();
if (jsonObject.has("mediaUrl")) content.mediaUrl = jsonObject.get("mediaUrl").getAsString();
if (jsonObject.has("scheduledAt")) content.scheduledAt = jsonObject.get("scheduledAt").getAsLong();
if (jsonObject.has("sound")) content.sound = jsonObject.get("sound").getAsBoolean();
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)");
return content;
}
}
private boolean sound;
private String priority;
private String url;
@ -40,9 +73,11 @@ public class NotificationContent {
*/
public NotificationContent() {
this.id = UUID.randomUUID().toString();
this.fetchTime = System.currentTimeMillis();
this.fetchedAt = System.currentTimeMillis();
this.scheduledAt = System.currentTimeMillis();
this.sound = true;
this.priority = "default";
Log.d("NotificationContent", "Constructor: created with fetchedAt=" + this.fetchedAt + ", scheduledAt=" + this.scheduledAt);
}
/**
@ -152,21 +187,30 @@ public class NotificationContent {
}
/**
* Get the fetch time when content was retrieved
* Get the fetch time when content was retrieved (immutable)
*
* @return Timestamp in milliseconds
*/
public long getFetchedAt() {
return fetchedAt;
}
/**
* Get when this notification instance was scheduled
*
* @return Timestamp in milliseconds
*/
public long getFetchTime() {
return fetchTime;
public long getScheduledAt() {
return scheduledAt;
}
/**
* Set the fetch time when content was retrieved
* Set when this notification instance was scheduled
*
* @param fetchTime Timestamp in milliseconds
* @param scheduledAt Timestamp in milliseconds
*/
public void setFetchTime(long fetchTime) {
this.fetchTime = fetchTime;
public void setScheduledAt(long scheduledAt) {
this.scheduledAt = scheduledAt;
}
/**
@ -224,23 +268,32 @@ public class NotificationContent {
}
/**
* Check if this notification is stale (older than 24 hours)
* Check if this notification content is stale (older than 24 hours)
*
* @return true if notification is stale
* @return true if notification content is stale
*/
public boolean isStale() {
long currentTime = System.currentTimeMillis();
long age = currentTime - fetchTime;
long age = currentTime - fetchedAt;
return age > 24 * 60 * 60 * 1000; // 24 hours in milliseconds
}
/**
* Get the age of this notification in milliseconds
* Get the age of this notification content in milliseconds
*
* @return Age in milliseconds
*/
public long getAge() {
return System.currentTimeMillis() - fetchTime;
return System.currentTimeMillis() - fetchedAt;
}
/**
* Get the age since this notification was scheduled
*
* @return Age in milliseconds
*/
public long getScheduledAge() {
return System.currentTimeMillis() - scheduledAt;
}
/**
@ -292,7 +345,8 @@ public class NotificationContent {
", body='" + body + '\'' +
", scheduledTime=" + scheduledTime +
", mediaUrl='" + mediaUrl + '\'' +
", fetchTime=" + fetchTime +
", fetchedAt=" + fetchedAt +
", scheduledAt=" + scheduledAt +
", sound=" + sound +
", priority='" + priority + '\'' +
", url='" + url + '\'' +

Loading…
Cancel
Save