refactor(android)!: restructure to standard Capacitor plugin layout

Restructure Android project from nested module layout to standard
Capacitor plugin structure following community conventions.

Structure Changes:
- Move plugin code from android/plugin/ to android/src/main/java/
- Move test app from android/app/ to test-apps/android-test-app/app/
- Remove nested android/plugin module structure
- Remove nested android/app test app structure

Build Infrastructure:
- Add Gradle wrapper files (gradlew, gradlew.bat, gradle/wrapper/)
- Transform android/build.gradle from root project to library module
- Update android/settings.gradle for standalone plugin builds
- Add android/gradle.properties with AndroidX configuration
- Add android/consumer-rules.pro for ProGuard rules

Configuration Updates:
- Add prepare script to package.json for automatic builds on npm install
- Update package.json version to 1.0.1
- Update android/build.gradle to properly resolve Capacitor dependencies
- Update test-apps/android-test-app/settings.gradle with correct paths
- Remove android/variables.gradle (hardcode values in build.gradle)

Documentation:
- Update BUILDING.md with new structure and build process
- Update INTEGRATION_GUIDE.md to reflect standard structure
- Update README.md to remove path fix warnings
- Add test-apps/BUILD_PROCESS.md documenting test app build flows

Test App Configuration:
- Fix android-test-app to correctly reference plugin and Capacitor
- Remove capacitor-cordova-android-plugins dependency (not needed)
- Update capacitor.settings.gradle path verification in fix script

BREAKING CHANGE: Plugin now uses standard Capacitor Android structure.
Consuming apps must update their capacitor.settings.gradle to reference
android/ instead of android/plugin/. This is automatically handled by
Capacitor CLI for apps using standard plugin installation.
This commit is contained in:
Matthew Raymer
2025-11-05 08:08:37 +00:00
parent c4b7f6382f
commit d9bdeb6d02
128 changed files with 1654 additions and 1747 deletions

View File

@@ -0,0 +1,388 @@
/**
* 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<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();
// 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();
}
}