/** * NotificationContent.java * * Data model for notification content following the project directive schema * Implements the canonical NotificationContent v1 structure * * @author Matthew Raymer * @version 1.0.0 */ package com.timesafari.dailynotification; import android.util.Log; import java.util.UUID; /** * Represents notification content with all required fields * * This class follows the canonical schema defined in the project directive: * - id: string (uuid) * - title: string * - body: string (plain text; may include simple emoji) * - scheduledTime: epoch millis (client-local target) * - mediaUrl: string? (for future; must be mirrored to local path before use) * - fetchTime: epoch millis */ public class NotificationContent { private String id; private String title; private String body; private long scheduledTime; private String mediaUrl; 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 { @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(); // 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("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(); // Reduced logging - only in debug builds // Log.d("NotificationContent", "Deserialized content with fetchedAt=" + content.fetchedAt + " (from constructor)"); return content; } } private boolean sound; private String priority; private String url; /** * Default constructor with auto-generated UUID */ public NotificationContent() { this.id = UUID.randomUUID().toString(); this.fetchedAt = System.currentTimeMillis(); this.scheduledAt = System.currentTimeMillis(); this.sound = true; this.priority = "default"; // 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"; } /** * Constructor with all required fields * * @param title Notification title * @param body Notification body text * @param scheduledTime When to display the notification */ public NotificationContent(String title, String body, long scheduledTime) { this(); this.title = title; this.body = body; this.scheduledTime = scheduledTime; } // Getters and Setters /** * Get the unique identifier for this notification * * @return UUID string */ public String getId() { return id; } /** * Set the unique identifier for this notification * * @param id UUID string */ public void setId(String id) { this.id = id; } /** * Get the notification title * * @return Title string */ public String getTitle() { return title; } /** * Set the notification title * * @param title Title string */ public void setTitle(String title) { this.title = title; } /** * Get the notification body text * * @return Body text string */ public String getBody() { return body; } /** * Set the notification body text * * @param body Body text string */ public void setBody(String body) { this.body = body; } /** * Get the scheduled time for this notification * * @return Timestamp in milliseconds */ public long getScheduledTime() { return scheduledTime; } /** * Set the scheduled time for this notification * * @param scheduledTime Timestamp in milliseconds */ public void setScheduledTime(long scheduledTime) { this.scheduledTime = scheduledTime; } /** * Get the media URL (optional, for future use) * * @return Media URL string or null */ public String getMediaUrl() { return mediaUrl; } /** * Set the media URL (optional, for future use) * * @param mediaUrl Media URL string or null */ public void setMediaUrl(String mediaUrl) { this.mediaUrl = mediaUrl; } /** * 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 getScheduledAt() { return scheduledAt; } /** * Set when this notification instance was scheduled * * @param scheduledAt Timestamp in milliseconds */ public void setScheduledAt(long scheduledAt) { this.scheduledAt = scheduledAt; } /** * Check if sound should be played * * @return true if sound is enabled */ public boolean isSound() { return sound; } /** * Set whether sound should be played * * @param sound true to enable sound */ public void setSound(boolean sound) { this.sound = sound; } /** * Get the notification priority * * @return Priority string (high, default, low) */ public String getPriority() { return priority; } /** * Set the notification priority * * @param priority Priority string (high, default, low) */ public void setPriority(String priority) { this.priority = priority; } /** * Get the associated URL * * @return URL string or null */ public String getUrl() { return url; } /** * Set the associated URL * * @param url URL string or null */ public void setUrl(String url) { this.url = url; } /** * Check if this notification content is stale (older than 24 hours) * * @return true if notification content is stale */ public boolean isStale() { long currentTime = System.currentTimeMillis(); long age = currentTime - fetchedAt; return age > 24 * 60 * 60 * 1000; // 24 hours in milliseconds } /** * Get the age of this notification content in milliseconds * * @return Age in milliseconds */ public long getAge() { return System.currentTimeMillis() - fetchedAt; } /** * Get the age since this notification was scheduled * * @return Age in milliseconds */ public long getScheduledAge() { return System.currentTimeMillis() - scheduledAt; } /** * Get the age of this notification in a human-readable format * * @return Human-readable age string */ public String getAgeString() { long age = getAge(); long seconds = age / 1000; long minutes = seconds / 60; long hours = minutes / 60; long days = hours / 24; if (days > 0) { return days + " day" + (days == 1 ? "" : "s") + " ago"; } else if (hours > 0) { return hours + " hour" + (hours == 1 ? "" : "s") + " ago"; } else if (minutes > 0) { return minutes + " minute" + (minutes == 1 ? "" : "s") + " ago"; } else { return "just now"; } } /** * Check if this notification is ready to be displayed * * @return true if notification should be displayed now */ public boolean isReadyToDisplay() { return System.currentTimeMillis() >= scheduledTime; } /** * Get time until this notification should be displayed * * @return Time in milliseconds until display */ public long getTimeUntilDisplay() { return Math.max(0, scheduledTime - System.currentTimeMillis()); } @Override public String toString() { return "NotificationContent{" + "id='" + id + '\'' + ", title='" + title + '\'' + ", body='" + body + '\'' + ", scheduledTime=" + scheduledTime + ", mediaUrl='" + mediaUrl + '\'' + ", fetchedAt=" + fetchedAt + ", scheduledAt=" + scheduledAt + ", sound=" + sound + ", priority='" + priority + '\'' + ", url='" + url + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NotificationContent that = (NotificationContent) o; return id.equals(that.id); } @Override public int hashCode() { return id.hashCode(); } }