You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
506 lines
17 KiB
506 lines
17 KiB
/**
|
|
* DailyNotificationPlugin.java
|
|
*
|
|
* Android implementation of the Daily Notification Plugin for Capacitor
|
|
* Implements offline-first daily notifications with prefetch → cache → schedule → display pipeline
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
package com.timesafari.dailynotification;
|
|
|
|
import android.Manifest;
|
|
import android.app.AlarmManager;
|
|
import android.app.NotificationChannel;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.PackageManager;
|
|
import android.os.Build;
|
|
import android.os.PowerManager;
|
|
import android.util.Log;
|
|
|
|
import androidx.core.app.NotificationCompat;
|
|
import androidx.work.WorkManager;
|
|
|
|
import com.getcapacitor.JSObject;
|
|
import com.getcapacitor.Plugin;
|
|
import com.getcapacitor.PluginCall;
|
|
import com.getcapacitor.PluginMethod;
|
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
import com.getcapacitor.annotation.Permission;
|
|
|
|
import java.util.Calendar;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/**
|
|
* Main plugin class for handling daily notifications on Android
|
|
*
|
|
* This plugin provides functionality for scheduling and managing daily notifications
|
|
* with offline-first approach, background content fetching, and reliable delivery.
|
|
*/
|
|
@CapacitorPlugin(
|
|
name = "DailyNotification",
|
|
permissions = {
|
|
@Permission(
|
|
alias = "notifications",
|
|
strings = {
|
|
Manifest.permission.POST_NOTIFICATIONS,
|
|
Manifest.permission.SCHEDULE_EXACT_ALARM,
|
|
Manifest.permission.WAKE_LOCK,
|
|
Manifest.permission.INTERNET
|
|
}
|
|
)
|
|
}
|
|
)
|
|
public class DailyNotificationPlugin extends Plugin {
|
|
|
|
private static final String TAG = "DailyNotificationPlugin";
|
|
private static final String CHANNEL_ID = "timesafari.daily";
|
|
private static final String CHANNEL_NAME = "Daily Notifications";
|
|
private static final String CHANNEL_DESCRIPTION = "Daily notification updates from TimeSafari";
|
|
|
|
private NotificationManager notificationManager;
|
|
private AlarmManager alarmManager;
|
|
private WorkManager workManager;
|
|
private PowerManager powerManager;
|
|
private DailyNotificationStorage storage;
|
|
private DailyNotificationScheduler scheduler;
|
|
private DailyNotificationFetcher fetcher;
|
|
|
|
/**
|
|
* Initialize the plugin and create notification channel
|
|
*/
|
|
@Override
|
|
public void load() {
|
|
super.load();
|
|
|
|
try {
|
|
// Initialize system services
|
|
notificationManager = (NotificationManager) getContext()
|
|
.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
alarmManager = (AlarmManager) getContext()
|
|
.getSystemService(Context.ALARM_SERVICE);
|
|
workManager = WorkManager.getInstance(getContext());
|
|
powerManager = (PowerManager) getContext()
|
|
.getSystemService(Context.POWER_SERVICE);
|
|
|
|
// Initialize components
|
|
storage = new DailyNotificationStorage(getContext());
|
|
scheduler = new DailyNotificationScheduler(getContext(), alarmManager);
|
|
fetcher = new DailyNotificationFetcher(getContext(), storage);
|
|
|
|
// Create notification channel
|
|
createNotificationChannel();
|
|
|
|
// Schedule next maintenance
|
|
scheduleMaintenance();
|
|
|
|
Log.i(TAG, "DailyNotificationPlugin initialized successfully");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Failed to initialize DailyNotificationPlugin", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule a daily notification with the specified options
|
|
*
|
|
* @param call Plugin call containing notification parameters
|
|
*/
|
|
@PluginMethod
|
|
public void scheduleDailyNotification(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Scheduling daily notification");
|
|
|
|
// Validate required parameters
|
|
String time = call.getString("time");
|
|
if (time == null || time.isEmpty()) {
|
|
call.reject("Time parameter is required");
|
|
return;
|
|
}
|
|
|
|
// Parse time (HH:mm format)
|
|
String[] timeParts = time.split(":");
|
|
if (timeParts.length != 2) {
|
|
call.reject("Invalid time format. Use HH:mm");
|
|
return;
|
|
}
|
|
|
|
int hour, minute;
|
|
try {
|
|
hour = Integer.parseInt(timeParts[0]);
|
|
minute = Integer.parseInt(timeParts[1]);
|
|
} catch (NumberFormatException e) {
|
|
call.reject("Invalid time format. Use HH:mm");
|
|
return;
|
|
}
|
|
|
|
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
call.reject("Invalid time values");
|
|
return;
|
|
}
|
|
|
|
// Extract other parameters
|
|
String title = call.getString("title", "Daily Update");
|
|
String body = call.getString("body", "Your daily notification is ready");
|
|
boolean sound = call.getBoolean("sound", true);
|
|
String priority = call.getString("priority", "default");
|
|
String url = call.getString("url", "");
|
|
|
|
// Create notification content
|
|
NotificationContent content = new NotificationContent();
|
|
content.setTitle(title);
|
|
content.setBody(body);
|
|
content.setSound(sound);
|
|
content.setPriority(priority);
|
|
content.setUrl(url);
|
|
content.setScheduledTime(calculateNextScheduledTime(hour, minute));
|
|
|
|
// Store notification content
|
|
storage.saveNotificationContent(content);
|
|
|
|
// Schedule the notification
|
|
boolean scheduled = scheduler.scheduleNotification(content);
|
|
|
|
if (scheduled) {
|
|
// Schedule background fetch for next day
|
|
scheduleBackgroundFetch(content.getScheduledTime());
|
|
|
|
Log.i(TAG, "Daily notification scheduled successfully for " + time);
|
|
call.resolve();
|
|
} else {
|
|
call.reject("Failed to schedule notification");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error scheduling daily notification", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the last notification that was delivered
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void getLastNotification(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Getting last notification");
|
|
|
|
NotificationContent lastNotification = storage.getLastNotification();
|
|
|
|
if (lastNotification != null) {
|
|
JSObject result = new JSObject();
|
|
result.put("id", lastNotification.getId());
|
|
result.put("title", lastNotification.getTitle());
|
|
result.put("body", lastNotification.getBody());
|
|
result.put("timestamp", lastNotification.getScheduledTime());
|
|
result.put("url", lastNotification.getUrl());
|
|
|
|
call.resolve(result);
|
|
} else {
|
|
call.resolve(null);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting last notification", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel all scheduled notifications
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void cancelAllNotifications(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Cancelling all notifications");
|
|
|
|
scheduler.cancelAllNotifications();
|
|
storage.clearAllNotifications();
|
|
|
|
Log.i(TAG, "All notifications cancelled successfully");
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error cancelling notifications", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current status of notifications
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void getNotificationStatus(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Getting notification status");
|
|
|
|
JSObject result = new JSObject();
|
|
|
|
// Check if notifications are enabled
|
|
boolean notificationsEnabled = areNotificationsEnabled();
|
|
result.put("isEnabled", notificationsEnabled);
|
|
|
|
// Get next notification time
|
|
long nextNotificationTime = scheduler.getNextNotificationTime();
|
|
result.put("nextNotificationTime", nextNotificationTime);
|
|
|
|
// Get current settings
|
|
JSObject settings = new JSObject();
|
|
settings.put("sound", true);
|
|
settings.put("priority", "default");
|
|
settings.put("timezone", "UTC");
|
|
result.put("settings", settings);
|
|
|
|
// Get pending notifications count
|
|
int pendingCount = scheduler.getPendingNotificationsCount();
|
|
result.put("pending", pendingCount);
|
|
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting notification status", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update notification settings
|
|
*
|
|
* @param call Plugin call containing new settings
|
|
*/
|
|
@PluginMethod
|
|
public void updateSettings(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Updating notification settings");
|
|
|
|
// Extract settings
|
|
Boolean sound = call.getBoolean("sound");
|
|
String priority = call.getString("priority");
|
|
String timezone = call.getString("timezone");
|
|
|
|
// Update settings in storage
|
|
if (sound != null) {
|
|
storage.setSoundEnabled(sound);
|
|
}
|
|
if (priority != null) {
|
|
storage.setPriority(priority);
|
|
}
|
|
if (timezone != null) {
|
|
storage.setTimezone(timezone);
|
|
}
|
|
|
|
// Update existing notifications with new settings
|
|
scheduler.updateNotificationSettings();
|
|
|
|
Log.i(TAG, "Notification settings updated successfully");
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error updating notification settings", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get battery status information
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void getBatteryStatus(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Getting battery status");
|
|
|
|
JSObject result = new JSObject();
|
|
|
|
// Get battery level (simplified - would need BatteryManager in real implementation)
|
|
result.put("level", 100); // Placeholder
|
|
result.put("isCharging", false); // Placeholder
|
|
result.put("powerState", 0); // Placeholder
|
|
result.put("isOptimizationExempt", false); // Placeholder
|
|
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting battery status", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request battery optimization exemption
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void requestBatteryOptimizationExemption(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Requesting battery optimization exemption");
|
|
|
|
// This would typically open system settings
|
|
// For now, just log the request
|
|
Log.i(TAG, "Battery optimization exemption requested");
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error requesting battery optimization exemption", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set adaptive scheduling based on device state
|
|
*
|
|
* @param call Plugin call containing enabled flag
|
|
*/
|
|
@PluginMethod
|
|
public void setAdaptiveScheduling(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Setting adaptive scheduling");
|
|
|
|
boolean enabled = call.getBoolean("enabled", true);
|
|
storage.setAdaptiveSchedulingEnabled(enabled);
|
|
|
|
if (enabled) {
|
|
scheduler.enableAdaptiveScheduling();
|
|
} else {
|
|
scheduler.disableAdaptiveScheduling();
|
|
}
|
|
|
|
Log.i(TAG, "Adaptive scheduling " + (enabled ? "enabled" : "disabled"));
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error setting adaptive scheduling", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current power state information
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void getPowerState(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Getting power state");
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("powerState", 0); // Placeholder
|
|
result.put("isOptimizationExempt", false); // Placeholder
|
|
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting power state", e);
|
|
call.reject("Internal error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the notification channel for Android 8.0+
|
|
*/
|
|
private void createNotificationChannel() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
NotificationChannel channel = new NotificationChannel(
|
|
CHANNEL_ID,
|
|
CHANNEL_NAME,
|
|
NotificationManager.IMPORTANCE_HIGH
|
|
);
|
|
channel.setDescription(CHANNEL_DESCRIPTION);
|
|
channel.enableLights(true);
|
|
channel.enableVibration(true);
|
|
|
|
notificationManager.createNotificationChannel(channel);
|
|
Log.d(TAG, "Notification channel created: " + CHANNEL_ID);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the next scheduled time for the notification
|
|
*
|
|
* @param hour Hour of day (0-23)
|
|
* @param minute Minute of hour (0-59)
|
|
* @return Timestamp in milliseconds
|
|
*/
|
|
private long calculateNextScheduledTime(int hour, int minute) {
|
|
Calendar calendar = Calendar.getInstance();
|
|
calendar.set(Calendar.HOUR_OF_DAY, hour);
|
|
calendar.set(Calendar.MINUTE, minute);
|
|
calendar.set(Calendar.SECOND, 0);
|
|
calendar.set(Calendar.MILLISECOND, 0);
|
|
|
|
// If time has passed today, schedule for tomorrow
|
|
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
|
|
calendar.add(Calendar.DAY_OF_YEAR, 1);
|
|
}
|
|
|
|
return calendar.getTimeInMillis();
|
|
}
|
|
|
|
/**
|
|
* Schedule background fetch for content
|
|
*
|
|
* @param scheduledTime When the notification is scheduled for
|
|
*/
|
|
private void scheduleBackgroundFetch(long scheduledTime) {
|
|
try {
|
|
// Schedule fetch 1 hour before notification
|
|
long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1);
|
|
|
|
if (fetchTime > System.currentTimeMillis()) {
|
|
fetcher.scheduleFetch(fetchTime);
|
|
Log.d(TAG, "Background fetch scheduled for " + fetchTime);
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error scheduling background fetch", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule maintenance tasks
|
|
*/
|
|
private void scheduleMaintenance() {
|
|
try {
|
|
// Schedule daily maintenance at 2 AM
|
|
Calendar calendar = Calendar.getInstance();
|
|
calendar.set(Calendar.HOUR_OF_DAY, 2);
|
|
calendar.set(Calendar.MINUTE, 0);
|
|
calendar.set(Calendar.SECOND, 0);
|
|
|
|
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
|
|
calendar.add(Calendar.DAY_OF_YEAR, 1);
|
|
}
|
|
|
|
// This would typically use WorkManager for maintenance
|
|
Log.d(TAG, "Maintenance scheduled for " + calendar.getTimeInMillis());
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error scheduling maintenance", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if notifications are enabled
|
|
*
|
|
* @return true if notifications are enabled
|
|
*/
|
|
private boolean areNotificationsEnabled() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
return getContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
|
== PackageManager.PERMISSION_GRANTED;
|
|
}
|
|
return NotificationManagerCompat.from(getContext()).areNotificationsEnabled();
|
|
}
|
|
}
|
|
|