/** * 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(); } }