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.
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 getFetchTime() {
|
||||
return fetchTime;
|
||||
public long getFetchedAt() {
|
||||
return fetchedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fetch time when content was retrieved
|
||||
* Get when this notification instance was scheduled
|
||||
*
|
||||
* @param fetchTime Timestamp in milliseconds
|
||||
* @return Timestamp in milliseconds
|
||||
*/
|
||||
public void setFetchTime(long fetchTime) {
|
||||
this.fetchTime = fetchTime;
|
||||
public long getScheduledAt() {
|
||||
return scheduledAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set when this notification instance was scheduled
|
||||
*
|
||||
* @param scheduledAt Timestamp in milliseconds
|
||||
*/
|
||||
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 + '\'' +
|
||||
|
||||
Reference in New Issue
Block a user