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.
1935 lines
73 KiB
1935 lines
73 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.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.pm.PackageManager;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
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;
|
|
|
|
// SQLite database components
|
|
private DailyNotificationDatabase database;
|
|
private DailyNotificationMigration migration;
|
|
private String databasePath;
|
|
private boolean useSharedStorage = false;
|
|
|
|
// Rolling window management
|
|
private DailyNotificationRollingWindow rollingWindow;
|
|
|
|
// Exact alarm management
|
|
private DailyNotificationExactAlarmManager exactAlarmManager;
|
|
|
|
// Reboot recovery management
|
|
private DailyNotificationRebootRecoveryManager rebootRecoveryManager;
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// Phase 1: Initialize TimeSafari Integration Components
|
|
eTagManager = new DailyNotificationETagManager(storage);
|
|
jwtManager = new DailyNotificationJWTManager(storage, eTagManager);
|
|
enhancedFetcher = new EnhancedDailyNotificationFetcher(getContext(), storage, eTagManager, jwtManager);
|
|
|
|
// Initialize TTL enforcer and connect to scheduler
|
|
initializeTTLEnforcer();
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure the plugin with database and storage options
|
|
*
|
|
* @param call Plugin call containing configuration parameters
|
|
*/
|
|
@PluginMethod
|
|
public void configure(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Configuring plugin with new options");
|
|
|
|
// Get configuration options
|
|
String dbPath = call.getString("dbPath");
|
|
String storageMode = call.getString("storage", "tiered");
|
|
Integer ttlSeconds = call.getInt("ttlSeconds");
|
|
Integer prefetchLeadMinutes = call.getInt("prefetchLeadMinutes");
|
|
Integer maxNotificationsPerDay = call.getInt("maxNotificationsPerDay");
|
|
Integer retentionDays = call.getInt("retentionDays");
|
|
|
|
// Phase 1: Process activeDidIntegration configuration
|
|
JSObject activeDidConfig = call.getObject("activeDidIntegration");
|
|
if (activeDidConfig != null) {
|
|
configureActiveDidIntegration(activeDidConfig);
|
|
}
|
|
|
|
// Update storage mode
|
|
useSharedStorage = "shared".equals(storageMode);
|
|
|
|
// Set database path
|
|
if (dbPath != null && !dbPath.isEmpty()) {
|
|
databasePath = dbPath;
|
|
Log.d(TAG, "Database path set to: " + databasePath);
|
|
} else {
|
|
// Use default database path
|
|
databasePath = getContext().getDatabasePath("daily_notifications.db").getAbsolutePath();
|
|
Log.d(TAG, "Using default database path: " + databasePath);
|
|
}
|
|
|
|
// Initialize SQLite database if using shared storage
|
|
if (useSharedStorage) {
|
|
initializeSQLiteDatabase();
|
|
}
|
|
|
|
// Store configuration in database or SharedPreferences
|
|
storeConfiguration(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays);
|
|
|
|
Log.i(TAG, "Plugin configuration completed successfully");
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error configuring plugin", e);
|
|
call.reject("Configuration failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize SQLite database with migration
|
|
*/
|
|
private void initializeSQLiteDatabase() {
|
|
try {
|
|
Log.d(TAG, "Initializing SQLite database");
|
|
|
|
// Create database instance
|
|
database = new DailyNotificationDatabase(getContext(), databasePath);
|
|
|
|
// Initialize migration utility
|
|
migration = new DailyNotificationMigration(getContext(), database);
|
|
|
|
// Perform migration if needed
|
|
if (migration.migrateToSQLite()) {
|
|
Log.i(TAG, "Migration completed successfully");
|
|
|
|
// Validate migration
|
|
if (migration.validateMigration()) {
|
|
Log.i(TAG, "Migration validation successful");
|
|
Log.i(TAG, migration.getMigrationStats());
|
|
} else {
|
|
Log.w(TAG, "Migration validation failed");
|
|
}
|
|
} else {
|
|
Log.w(TAG, "Migration failed or not needed");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error initializing SQLite database", e);
|
|
throw new RuntimeException("SQLite initialization failed", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store configuration values
|
|
*/
|
|
private void storeConfiguration(Integer ttlSeconds, Integer prefetchLeadMinutes,
|
|
Integer maxNotificationsPerDay, Integer retentionDays) {
|
|
try {
|
|
if (useSharedStorage && database != null) {
|
|
// Store in SQLite
|
|
storeConfigurationInSQLite(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays);
|
|
} else {
|
|
// Store in SharedPreferences
|
|
storeConfigurationInSharedPreferences(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays);
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error storing configuration", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store configuration in SQLite database
|
|
*/
|
|
private void storeConfigurationInSQLite(Integer ttlSeconds, Integer prefetchLeadMinutes,
|
|
Integer maxNotificationsPerDay, Integer retentionDays) {
|
|
try {
|
|
SQLiteDatabase db = database.getWritableDatabase();
|
|
|
|
// Store each configuration value
|
|
if (ttlSeconds != null) {
|
|
storeConfigValue(db, "ttlSeconds", String.valueOf(ttlSeconds));
|
|
}
|
|
if (prefetchLeadMinutes != null) {
|
|
storeConfigValue(db, "prefetchLeadMinutes", String.valueOf(prefetchLeadMinutes));
|
|
}
|
|
if (maxNotificationsPerDay != null) {
|
|
storeConfigValue(db, "maxNotificationsPerDay", String.valueOf(maxNotificationsPerDay));
|
|
}
|
|
if (retentionDays != null) {
|
|
storeConfigValue(db, "retentionDays", String.valueOf(retentionDays));
|
|
}
|
|
|
|
Log.d(TAG, "Configuration stored in SQLite");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error storing configuration in SQLite", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store a single configuration value in SQLite
|
|
*/
|
|
private void storeConfigValue(SQLiteDatabase db, String key, String value) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(DailyNotificationDatabase.COL_CONFIG_K, key);
|
|
values.put(DailyNotificationDatabase.COL_CONFIG_V, value);
|
|
|
|
// Use INSERT OR REPLACE to handle updates
|
|
db.replace(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values);
|
|
}
|
|
|
|
/**
|
|
* Store configuration in SharedPreferences
|
|
*/
|
|
private void storeConfigurationInSharedPreferences(Integer ttlSeconds, Integer prefetchLeadMinutes,
|
|
Integer maxNotificationsPerDay, Integer retentionDays) {
|
|
try {
|
|
SharedPreferences prefs = getContext().getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = prefs.edit();
|
|
|
|
if (ttlSeconds != null) {
|
|
editor.putInt("ttlSeconds", ttlSeconds);
|
|
}
|
|
if (prefetchLeadMinutes != null) {
|
|
editor.putInt("prefetchLeadMinutes", prefetchLeadMinutes);
|
|
}
|
|
if (maxNotificationsPerDay != null) {
|
|
editor.putInt("maxNotificationsPerDay", maxNotificationsPerDay);
|
|
}
|
|
if (retentionDays != null) {
|
|
editor.putInt("retentionDays", retentionDays);
|
|
}
|
|
|
|
editor.apply();
|
|
Log.d(TAG, "Configuration stored in SharedPreferences");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error storing configuration in SharedPreferences", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize TTL enforcer and connect to scheduler
|
|
*/
|
|
private void initializeTTLEnforcer() {
|
|
try {
|
|
Log.d(TAG, "Initializing TTL enforcer");
|
|
|
|
// Create TTL enforcer with current storage mode
|
|
DailyNotificationTTLEnforcer ttlEnforcer = new DailyNotificationTTLEnforcer(
|
|
getContext(),
|
|
database,
|
|
useSharedStorage
|
|
);
|
|
|
|
// Connect to scheduler
|
|
scheduler.setTTLEnforcer(ttlEnforcer);
|
|
|
|
// Initialize rolling window
|
|
initializeRollingWindow(ttlEnforcer);
|
|
|
|
Log.i(TAG, "TTL enforcer initialized and connected to scheduler");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error initializing TTL enforcer", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize rolling window manager
|
|
*/
|
|
private void initializeRollingWindow(DailyNotificationTTLEnforcer ttlEnforcer) {
|
|
try {
|
|
Log.d(TAG, "Initializing rolling window manager");
|
|
|
|
// Detect platform (Android vs iOS)
|
|
boolean isIOSPlatform = false; // TODO: Implement platform detection
|
|
|
|
// Create rolling window manager
|
|
rollingWindow = new DailyNotificationRollingWindow(
|
|
getContext(),
|
|
scheduler,
|
|
ttlEnforcer,
|
|
storage,
|
|
isIOSPlatform
|
|
);
|
|
|
|
// Initialize exact alarm manager
|
|
initializeExactAlarmManager();
|
|
|
|
// Initialize reboot recovery manager
|
|
initializeRebootRecoveryManager();
|
|
|
|
// Start initial window maintenance
|
|
rollingWindow.maintainRollingWindow();
|
|
|
|
Log.i(TAG, "Rolling window manager initialized");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error initializing rolling window manager", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize exact alarm manager
|
|
*/
|
|
private void initializeExactAlarmManager() {
|
|
try {
|
|
Log.d(TAG, "Initializing exact alarm manager");
|
|
|
|
// Create exact alarm manager
|
|
exactAlarmManager = new DailyNotificationExactAlarmManager(
|
|
getContext(),
|
|
alarmManager,
|
|
scheduler
|
|
);
|
|
|
|
// Connect to scheduler
|
|
scheduler.setExactAlarmManager(exactAlarmManager);
|
|
|
|
Log.i(TAG, "Exact alarm manager initialized");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error initializing exact alarm manager", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize reboot recovery manager
|
|
*/
|
|
private void initializeRebootRecoveryManager() {
|
|
try {
|
|
Log.d(TAG, "Initializing reboot recovery manager");
|
|
|
|
// Create reboot recovery manager
|
|
rebootRecoveryManager = new DailyNotificationRebootRecoveryManager(
|
|
getContext(),
|
|
scheduler,
|
|
exactAlarmManager,
|
|
rollingWindow
|
|
);
|
|
|
|
// Register broadcast receivers
|
|
rebootRecoveryManager.registerReceivers();
|
|
|
|
Log.i(TAG, "Reboot recovery manager initialized");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error initializing reboot recovery manager", 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();
|
|
}
|
|
|
|
/**
|
|
* Maintain rolling window (for testing or manual triggers)
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void maintainRollingWindow(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Manual rolling window maintenance requested");
|
|
|
|
if (rollingWindow != null) {
|
|
rollingWindow.forceMaintenance();
|
|
call.resolve();
|
|
} else {
|
|
call.reject("Rolling window not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error during manual rolling window maintenance", e);
|
|
call.reject("Error maintaining rolling window: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get rolling window statistics
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void getRollingWindowStats(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Rolling window stats requested");
|
|
|
|
if (rollingWindow != null) {
|
|
String stats = rollingWindow.getRollingWindowStats();
|
|
JSObject result = new JSObject();
|
|
result.put("stats", stats);
|
|
result.put("maintenanceNeeded", rollingWindow.isMaintenanceNeeded());
|
|
result.put("timeUntilNextMaintenance", rollingWindow.getTimeUntilNextMaintenance());
|
|
call.resolve(result);
|
|
} else {
|
|
call.reject("Rolling window not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting rolling window stats", e);
|
|
call.reject("Error getting rolling window stats: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get exact alarm status
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void getExactAlarmStatus(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Exact alarm status requested");
|
|
|
|
if (exactAlarmManager != null) {
|
|
DailyNotificationExactAlarmManager.ExactAlarmStatus status = exactAlarmManager.getExactAlarmStatus();
|
|
JSObject result = new JSObject();
|
|
result.put("supported", status.supported);
|
|
result.put("enabled", status.enabled);
|
|
result.put("canSchedule", status.canSchedule);
|
|
result.put("fallbackWindow", status.fallbackWindow.description);
|
|
call.resolve(result);
|
|
} else {
|
|
call.reject("Exact alarm manager not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting exact alarm status", e);
|
|
call.reject("Error getting exact alarm status: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request exact alarm permission
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void requestExactAlarmPermission(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Exact alarm permission request");
|
|
|
|
if (exactAlarmManager != null) {
|
|
boolean success = exactAlarmManager.requestExactAlarmPermission();
|
|
if (success) {
|
|
call.resolve();
|
|
} else {
|
|
call.reject("Failed to request exact alarm permission");
|
|
}
|
|
} else {
|
|
call.reject("Exact alarm manager not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error requesting exact alarm permission", e);
|
|
call.reject("Error requesting exact alarm permission: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open exact alarm settings
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void openExactAlarmSettings(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Opening exact alarm settings");
|
|
|
|
if (exactAlarmManager != null) {
|
|
boolean success = exactAlarmManager.openExactAlarmSettings();
|
|
if (success) {
|
|
call.resolve();
|
|
} else {
|
|
call.reject("Failed to open exact alarm settings");
|
|
}
|
|
} else {
|
|
call.reject("Exact alarm manager not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error opening exact alarm settings", e);
|
|
call.reject("Error opening exact alarm settings: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get reboot recovery status
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
@PluginMethod
|
|
public void getRebootRecoveryStatus(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Reboot recovery status requested");
|
|
|
|
if (rebootRecoveryManager != null) {
|
|
DailyNotificationRebootRecoveryManager.RecoveryStatus status = rebootRecoveryManager.getRecoveryStatus();
|
|
JSObject result = new JSObject();
|
|
result.put("inProgress", status.inProgress);
|
|
result.put("lastRecoveryTime", status.lastRecoveryTime);
|
|
result.put("timeSinceLastRecovery", status.timeSinceLastRecovery);
|
|
result.put("recoveryNeeded", rebootRecoveryManager.isRecoveryNeeded());
|
|
call.resolve(result);
|
|
} else {
|
|
call.reject("Reboot recovery manager not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting reboot recovery status", e);
|
|
call.reject("Error getting reboot recovery status: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// MARK: - Phase 1: TimeSafari Integration Methods
|
|
|
|
/**
|
|
* Configure activeDid integration options
|
|
*
|
|
* @param config Configuration object with platform and storage type
|
|
*/
|
|
private void configureActiveDidIntegration(JSObject config) {
|
|
try {
|
|
Log.d(TAG, "Configuring Phase 2 activeDid integration");
|
|
|
|
String platform = config.getString("platform", "android");
|
|
String storageType = config.getString("storageType", "plugin-managed");
|
|
Integer jwtExpirationSeconds = config.getInteger("jwtExpirationSeconds", 60);
|
|
String apiServer = config.getString("apiServer");
|
|
|
|
// Phase 2: Host-provided activeDid initial configuration
|
|
String initialActiveDid = config.getString("activeDid");
|
|
boolean autoSync = config.getBoolean("autoSync", false);
|
|
Integer identityChangeGraceSeconds = config.getInteger("identityChangeGraceSeconds", 30);
|
|
|
|
Log.d(TAG, "Phase 2 ActiveDid config - Platform: " + platform +
|
|
", Storage: " + storageType + ", JWT Expiry: " + jwtExpirationSeconds + "s" +
|
|
", API Server: " + apiServer + ", Initial ActiveDid: " +
|
|
(initialActiveDid != null ? initialActiveDid.substring(0, Math.min(20, initialActiveDid.length())) + "..." : "null") +
|
|
", AutoSync: " + autoSync + ", Grace Period: " + identityChangeGraceSeconds + "s");
|
|
|
|
// Phase 2: Configure JWT manager with auto-sync capabilities
|
|
if (jwtManager != null) {
|
|
if (initialActiveDid != null && !initialActiveDid.isEmpty()) {
|
|
jwtManager.setActiveDid(initialActiveDid, jwtExpirationSeconds);
|
|
Log.d(TAG, "Phase 2: Initial ActiveDid set in JWT manager");
|
|
}
|
|
Log.d(TAG, "Phase 2: JWT manager configured with auto-sync: " + autoSync);
|
|
}
|
|
|
|
// Phase 2: Configure enhanced fetcher with TimeSafari API support
|
|
if (enhancedFetcher != null && apiServer != null && !apiServer.isEmpty()) {
|
|
enhancedFetcher.setApiServerUrl(apiServer);
|
|
Log.d(TAG, "Phase 2: Enhanced fetcher configured with API server: " + apiServer);
|
|
|
|
// Phase 2: Set up TimeSafari-specific configuration
|
|
if (initialActiveDid != null && !initialActiveDid.isEmpty()) {
|
|
EnhancedDailyNotificationFetcher.TimeSafariUserConfig userConfig =
|
|
new EnhancedDailyNotificationFetcher.TimeSafariUserConfig();
|
|
userConfig.activeDid = initialActiveDid;
|
|
userConfig.fetchOffersToPerson = true;
|
|
userConfig.fetchOffersToProjects = true;
|
|
userConfig.fetchProjectUpdates = true;
|
|
|
|
Log.d(TAG, "Phase 2: TimeSafari user configuration prepared");
|
|
}
|
|
}
|
|
|
|
// Phase 2: Store auto-sync configuration for future use
|
|
storeAutoSyncConfiguration(autoSync, identityChangeGraceSeconds);
|
|
|
|
Log.i(TAG, "Phase 2 ActiveDid integration configured successfully");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error configuring Phase 2 activeDid integration", e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store auto-sync configuration for background tasks
|
|
*/
|
|
private void storeAutoSyncConfiguration(boolean autoSync, int gracePeriodSeconds) {
|
|
try {
|
|
if (storage != null) {
|
|
// Store auto-sync settings in plugin storage
|
|
Map<String, Object> syncConfig = new HashMap<>();
|
|
syncConfig.put("autoSync", autoSync);
|
|
syncConfig.put("gracePeriodSeconds", gracePeriodSeconds);
|
|
syncConfig.put("configuredAt", System.currentTimeMillis());
|
|
|
|
// Store in SharedPreferences for persistence
|
|
android.content.SharedPreferences preferences = getContext()
|
|
.getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);
|
|
preferences.edit()
|
|
.putBoolean("autoSync", autoSync)
|
|
.putInt("gracePeriodSeconds", gracePeriodSeconds)
|
|
.putLong("configuredAt", System.currentTimeMillis())
|
|
.apply();
|
|
|
|
Log.d(TAG, "Phase 2: Auto-sync configuration stored");
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error storing auto-sync configuration", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set active DID from host application
|
|
*
|
|
* This implements the Option A pattern where the host provides activeDid
|
|
*/
|
|
@PluginMethod
|
|
public void setActiveDidFromHost(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Setting activeDid from host");
|
|
|
|
String activeDid = call.getString("activeDid");
|
|
if (activeDid == null || activeDid.isEmpty()) {
|
|
call.reject("activeDid cannot be null or empty");
|
|
return;
|
|
}
|
|
|
|
// Set activeDid in JWT manager
|
|
if (jwtManager != null) {
|
|
jwtManager.setActiveDid(activeDid);
|
|
Log.d(TAG, "ActiveDid set in JWT manager: " + activeDid);
|
|
}
|
|
|
|
Log.i(TAG, "ActiveDid set successfully from host");
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error setting activeDid from host", e);
|
|
call.reject("Error setting activeDid: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh authentication for new identity
|
|
*/
|
|
@PluginMethod
|
|
public void refreshAuthenticationForNewIdentity(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Refreshing authentication for new identity");
|
|
|
|
String activeDid = call.getString("activeDid");
|
|
if (activeDid == null || activeDid.isEmpty()) {
|
|
call.reject("activeDid cannot be null or empty");
|
|
return;
|
|
}
|
|
|
|
// Refresh JWT with new activeDid
|
|
if (jwtManager != null) {
|
|
jwtManager.setActiveDid(activeDid);
|
|
Log.d(TAG, "Authentication refreshed for activeDid: " + activeDid);
|
|
}
|
|
|
|
Log.i(TAG, "Authentication refreshed successfully");
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error refreshing authentication", e);
|
|
call.reject("Error refreshing authentication: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear cached content for new identity
|
|
*/
|
|
@PluginMethod
|
|
public void clearCacheForNewIdentity(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Clearing cache for new identity");
|
|
|
|
// Clear content cache
|
|
if (storage != null) {
|
|
storage.clearAllContent();
|
|
Log.d(TAG, "Content cache cleared");
|
|
}
|
|
|
|
// Clear ETag cache
|
|
if (eTagManager != null) {
|
|
eTagManager.clearETags();
|
|
Log.d(TAG, "ETag cache cleared");
|
|
}
|
|
|
|
// Clear authentication state in JWT manager
|
|
if (jwtManager != null) {
|
|
jwtManager.clearAuthentication();
|
|
Log.d(TAG, "Authentication state cleared");
|
|
}
|
|
|
|
Log.i(TAG, "Cache cleared successfully for new identity");
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error clearing cache for new identity", e);
|
|
call.reject("Error clearing cache: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update background tasks with new identity
|
|
*/
|
|
@PluginMethod
|
|
public void updateBackgroundTaskIdentity(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Updating background tasks with new identity");
|
|
|
|
String activeDid = call.getString("activeDid");
|
|
if (activeDid == null || activeDid.isEmpty()) {
|
|
call.reject("activeDid cannot be null or empty");
|
|
return;
|
|
}
|
|
|
|
// For Phase 1, this mainly updates the JWT manager
|
|
// Future phases will restart background WorkManager tasks
|
|
if (jwtManager != null) {
|
|
jwtManager.setActiveDid(activeDid);
|
|
Log.d(TAG, "Background task identity updated to: " + activeDid);
|
|
}
|
|
|
|
Log.i(TAG, "Background tasks updated successfully");
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error updating background tasks", e);
|
|
call.reject("Error updating background tasks: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test JWT generation for debugging
|
|
*/
|
|
@PluginMethod
|
|
public void testJWTGeneration(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Testing JWT generation");
|
|
|
|
String activeDid = call.getString("activeDid", "did:example:test");
|
|
|
|
if (jwtManager != null) {
|
|
jwtManager.setActiveDid(activeDid);
|
|
String token = jwtManager.getCurrentJWTToken();
|
|
String debugInfo = jwtManager.getTokenDebugInfo();
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("activeDid", activeDid);
|
|
result.put("tokenLength", token != null ? token.length() : 0);
|
|
result.put("debugInfo", debugInfo);
|
|
result.put("authenticated", jwtManager.isAuthenticated());
|
|
|
|
Log.d(TAG, "JWT test completed successfully");
|
|
call.resolve(result);
|
|
} else {
|
|
call.reject("JWT manager not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error testing JWT generation", e);
|
|
call.reject("JWT test failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test Endorser.ch API calls
|
|
*/
|
|
@PluginMethod
|
|
public void testEndorserAPI(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Testing Endorser.ch API calls");
|
|
|
|
String activeDid = call.getString("activeDid", "did:example:test");
|
|
String apiServer = call.getString("apiServer", "https://api.endorser.ch");
|
|
|
|
if (enhancedFetcher != null) {
|
|
// Set up test configuration
|
|
enhancedFetcher.setApiServerUrl(apiServer);
|
|
|
|
EnhancedDailyNotificationFetcher.TimeSafariUserConfig userConfig =
|
|
new EnhancedDailyNotificationFetcher.TimeSafariUserConfig();
|
|
userConfig.activeDid = activeDid;
|
|
userConfig.fetchOffersToPerson = true;
|
|
userConfig.fetchOffersToProjects = true;
|
|
userConfig.fetchProjectUpdates = true;
|
|
|
|
// Execute test fetch
|
|
CompletableFuture<EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle> future =
|
|
enhancedFetcher.fetchAllTimeSafariData(userConfig);
|
|
|
|
// For immediate testing, we'll create a simple response
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("activeDid", activeDid);
|
|
result.put("apiServer", apiServer);
|
|
result.put("testCompleted", true);
|
|
result.put("message", "Endorser.ch API test initiated successfully");
|
|
|
|
Log.d(TAG, "Endorser.ch API test completed successfully");
|
|
call.resolve(result);
|
|
} else {
|
|
call.reject("Enhanced fetcher not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error testing Endorser.ch API", e);
|
|
call.reject("Endorser.ch API test failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// MARK: - Phase 3: TimeSafari Background Coordination Methods
|
|
|
|
/**
|
|
* Phase 3: Coordinate background tasks with TimeSafari PlatformServiceMixin
|
|
*/
|
|
@PluginMethod
|
|
public void coordinateBackgroundTasks(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Phase 3: Coordinating background tasks with PlatformServiceMixin");
|
|
|
|
if (scheduler != null) {
|
|
scheduler.coordinateWithPlatformServiceMixin();
|
|
|
|
// Schedule enhanced WorkManager jobs with coordination
|
|
scheduleCoordinatedBackgroundJobs();
|
|
|
|
Log.i(TAG, "Phase 3: Background task coordination completed");
|
|
call.resolve();
|
|
} else {
|
|
call.reject("Scheduler not initialized");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error coordinating background tasks", e);
|
|
call.reject("Background task coordination failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Schedule coordinated background jobs
|
|
*/
|
|
private void scheduleCoordinatedBackgroundJobs() {
|
|
try {
|
|
Log.d(TAG, "Phase 3: Scheduling coordinated background jobs");
|
|
|
|
// Create coordinated WorkManager job with TimeSafari awareness
|
|
androidx.work.Data inputData = new androidx.work.Data.Builder()
|
|
.putBoolean("timesafari_coordination", true)
|
|
.putLong("coordination_timestamp", System.currentTimeMillis())
|
|
.putString("active_did_tracking", "enabled")
|
|
.build();
|
|
|
|
androidx.work.OneTimeWorkRequest coordinatedWork =
|
|
new androidx.work.OneTimeWorkRequest.Builder(DailyNotificationFetchWorker.class)
|
|
.setInputData(inputData)
|
|
.setConstraints(androidx.work.Constraints.Builder()
|
|
.setRequiresCharging(false)
|
|
.setRequiresBatteryNotLow(false)
|
|
.setRequiredNetworkType(androidx.work.NetworkType.CONNECTED)
|
|
.build())
|
|
.addTag("timesafari_coordinated")
|
|
.addTag("phase3_background")
|
|
.build();
|
|
|
|
// Schedule with coordination awareness
|
|
workManager.enqueueUniqueWork(
|
|
"tsaf_coordinated_fetch",
|
|
androidx.work.ExistingWorkPolicy.REPLACE,
|
|
coordinatedWork
|
|
);
|
|
|
|
Log.d(TAG, "Phase 3: Coordinated background job scheduled");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error scheduling coordinated background jobs", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Handle app lifecycle events for TimeSafari coordination
|
|
*/
|
|
@PluginMethod
|
|
public void handleAppLifecycleEvent(PluginCall call) {
|
|
try {
|
|
String lifecycleEvent = call.getString("lifecycleEvent");
|
|
Log.d(TAG, "Phase 3: Handling app lifecycle event: " + lifecycleEvent);
|
|
|
|
if (lifecycleEvent == null) {
|
|
call.reject("lifecycleEvent parameter required");
|
|
return;
|
|
}
|
|
|
|
switch (lifecycleEvent) {
|
|
case "app_background":
|
|
handleAppBackgrounded();
|
|
break;
|
|
case "app_foreground":
|
|
handleAppForegrounded();
|
|
break;
|
|
case "app_resumed":
|
|
handleAppResumed();
|
|
break;
|
|
case "app_paused":
|
|
handleAppPaused();
|
|
break;
|
|
default:
|
|
Log.w(TAG, "Phase 3: Unknown lifecycle event: " + lifecycleEvent);
|
|
break;
|
|
}
|
|
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error handling app lifecycle event", e);
|
|
call.reject("App lifecycle event handling failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Handle app backgrounded event
|
|
*/
|
|
private void handleAppBackgrounded() {
|
|
try {
|
|
Log.d(TAG, "Phase 3: App backgrounded - activating TimeSafari coordination");
|
|
|
|
// Activate enhanced background execution
|
|
if (scheduler != null) {
|
|
scheduler.coordinateWithPlatformServiceMixin();
|
|
}
|
|
|
|
// Store app state for coordination
|
|
android.content.SharedPreferences prefs = getContext()
|
|
.getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);
|
|
prefs.edit()
|
|
.putLong("lastAppBackgrounded", System.currentTimeMillis())
|
|
.putBoolean("isAppBackgrounded", true)
|
|
.apply();
|
|
|
|
Log.d(TAG, "Phase 3: App backgrounded coordination completed");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error handling app backgrounded", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Handle app foregrounded event
|
|
*/
|
|
private void handleAppForegrounded() {
|
|
try {
|
|
Log.d(TAG, "Phase 3: App foregrounded - updating TimeSafari coordination");
|
|
|
|
// Update coordination state
|
|
android.content.SharedPreferences prefs = getContext()
|
|
.getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);
|
|
prefs.edit()
|
|
.putLong("lastAppForegrounded", System.currentTimeMillis())
|
|
.putBoolean("isAppBackgrounded", false)
|
|
.apply();
|
|
|
|
// Check if activeDid coordination is needed
|
|
checkActiveDidCoordination();
|
|
|
|
Log.d(TAG, "Phase 3: App foregrounded coordination completed");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error handling app foregrounded", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Handle app resumed event
|
|
*/
|
|
private void handleAppResumed() {
|
|
try {
|
|
Log.d(TAG, "Phase 3: App resumed - syncing TimeSafari state");
|
|
|
|
// Sync state with resumed app
|
|
syncTimeSafariState();
|
|
|
|
Log.d(TAG, "Phase 3: App resumed coordination completed");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error handling app resumed", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Handle app paused event
|
|
*/
|
|
private void handleAppPaused() {
|
|
try {
|
|
Log.d(TAG, "Phase 3: App paused - pausing TimeSafari coordination");
|
|
|
|
// Pause non-critical coordination
|
|
pauseTimeSafariCoordination();
|
|
|
|
Log.d(TAG, "Phase 3: App paused coordination completed");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error handling app paused");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Check if activeDid coordination is needed
|
|
*/
|
|
private void checkActiveDidCoordination() {
|
|
try {
|
|
android.content.SharedPreferences prefs = getContext()
|
|
.getSharedPreferences(
|
|
"daily_notification_timesafari", Context.MODE_PRIVATE);
|
|
|
|
long lastActiveDidChange = prefs.getLong("lastActiveDidChange", 0);
|
|
long lastAppForegrounded = prefs.getLong("lastAppForegrounded", 0);
|
|
|
|
// If activeDid changed while app was backgrounded, update background tasks
|
|
if (lastActiveDidChange > lastAppForegrounded) {
|
|
Log.d(TAG, "Phase 3: ActiveDid changed while backgrounded - updating background tasks");
|
|
|
|
// Update background tasks with new activeDid
|
|
if (jwtManager != null) {
|
|
String currentActiveDid = jwtManager.getCurrentActiveDid();
|
|
if (currentActiveDid != null && !currentActiveDid.isEmpty()) {
|
|
Log.d(TAG, "Phase 3: Updating background tasks for activeDid: " + currentActiveDid);
|
|
// Background task update would happen here
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error checking activeDid coordination", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Sync TimeSafari state after app resume
|
|
*/
|
|
private void syncTimeSafariState() {
|
|
try {
|
|
Log.d(TAG, "Phase 3: Syncing TimeSafari state");
|
|
|
|
// Sync authentication state
|
|
if (jwtManager != null) {
|
|
jwtManager.refreshJWTIfNeeded();
|
|
}
|
|
|
|
// Sync notification delivery tracking
|
|
if (scheduler != null) {
|
|
// Update notification delivery metrics
|
|
android.content.SharedPreferences prefs = getContext()
|
|
.getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);
|
|
|
|
long lastBackgroundDelivery = prefs.getLong("lastBackgroundDelivered", 0);
|
|
if (lastBackgroundDelivery > 0) {
|
|
String lastDeliveredId = prefs.getString("lastBackgroundDeliveredId", "");
|
|
scheduler.recordNotificationDelivery(lastDeliveredId);
|
|
Log.d(TAG, "Phase 3: Synced background delivery: " + lastDeliveredId);
|
|
}
|
|
}
|
|
|
|
Log.d(TAG, "Phase 3: TimeSafari state sync completed");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error syncing TimeSafari state", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Pause TimeSafari coordination when app paused
|
|
*/
|
|
private void pauseTimeSafariCoordination() {
|
|
try {
|
|
Log.d(TAG, "Phase 3: Pausing TimeSafari coordination");
|
|
|
|
// Mark coordination as paused
|
|
android.content.SharedPreferences prefs = getContext()
|
|
.getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);
|
|
|
|
prefs.edit()
|
|
.putLong("lastCoordinationPaused", System.currentTimeMillis())
|
|
.putBoolean("coordinationPaused", true)
|
|
.apply();
|
|
|
|
Log.d(TAG, "Phase 3: TimeSafari coordination paused");
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error pausing TimeSafari coordination", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Get coordination status for debugging
|
|
*/
|
|
@PluginMethod
|
|
public void getCoordinationStatus(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Phase 3: Getting coordination status");
|
|
|
|
android.content.SharedPreferences prefs = getContext()
|
|
.getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);
|
|
|
|
com.getcapacitor.JSObject status = new com.getcapacitor.JSObject();
|
|
status.put("autoSync", prefs.getBoolean("autoSync", false));
|
|
status.put("coordinationPaused", prefs.getBoolean("coordinationPaused", false));
|
|
status.put("lastBackgroundFetchCoordinated", prefs.getLong("lastBackgroundFetchCoordinated", 0));
|
|
status.put("lastActiveDidChange", prefs.getLong("lastActiveDidChange", 0));
|
|
status.put("lastAppBackgrounded", prefs.getLong("lastAppBackgrounded", 0));
|
|
status.put("lastAppForegrounded", prefs.getLong("lastAppForegrounded", 0));
|
|
|
|
call.resolve(status);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Phase 3: Error getting coordination status", e);
|
|
call.reject("Coordination status retrieval failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// Static Daily Reminder Methods
|
|
@PluginMethod
|
|
public void scheduleDailyReminder(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Scheduling daily reminder");
|
|
|
|
// Extract reminder options
|
|
String id = call.getString("id");
|
|
String title = call.getString("title");
|
|
String body = call.getString("body");
|
|
String time = call.getString("time");
|
|
boolean sound = call.getBoolean("sound", true);
|
|
boolean vibration = call.getBoolean("vibration", true);
|
|
String priority = call.getString("priority", "normal");
|
|
boolean repeatDaily = call.getBoolean("repeatDaily", true);
|
|
String timezone = call.getString("timezone");
|
|
|
|
// Validate required parameters
|
|
if (id == null || title == null || body == null || time == null) {
|
|
call.reject("Missing required parameters: id, title, body, time");
|
|
return;
|
|
}
|
|
|
|
// Parse time (HH:mm format)
|
|
String[] timeParts = time.split(":");
|
|
if (timeParts.length != 2) {
|
|
call.reject("Invalid time format. Use HH:mm (e.g., 09:00)");
|
|
return;
|
|
}
|
|
|
|
int hour = Integer.parseInt(timeParts[0]);
|
|
int minute = Integer.parseInt(timeParts[1]);
|
|
|
|
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
call.reject("Invalid time values. Hour must be 0-23, minute must be 0-59");
|
|
return;
|
|
}
|
|
|
|
// Create reminder content
|
|
NotificationContent reminderContent = new NotificationContent();
|
|
reminderContent.setId("reminder_" + id); // Prefix to identify as reminder
|
|
reminderContent.setTitle(title);
|
|
reminderContent.setBody(body);
|
|
reminderContent.setSound(sound);
|
|
reminderContent.setPriority(priority);
|
|
reminderContent.setFetchTime(System.currentTimeMillis());
|
|
|
|
// Calculate next trigger time
|
|
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_MONTH, 1);
|
|
}
|
|
|
|
reminderContent.setScheduledTime(calendar.getTimeInMillis());
|
|
|
|
// Store reminder in database
|
|
storeReminderInDatabase(id, title, body, time, sound, vibration, priority, repeatDaily, timezone);
|
|
|
|
// Schedule the notification
|
|
boolean scheduled = scheduler.scheduleNotification(reminderContent);
|
|
|
|
if (scheduled) {
|
|
Log.i(TAG, "Daily reminder scheduled successfully: " + id);
|
|
call.resolve();
|
|
} else {
|
|
call.reject("Failed to schedule daily reminder");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error scheduling daily reminder", e);
|
|
call.reject("Daily reminder scheduling failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
public void cancelDailyReminder(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Cancelling daily reminder");
|
|
|
|
String reminderId = call.getString("reminderId");
|
|
if (reminderId == null) {
|
|
call.reject("Missing reminderId parameter");
|
|
return;
|
|
}
|
|
|
|
// Cancel the scheduled notification (use prefixed ID)
|
|
scheduler.cancelNotification("reminder_" + reminderId);
|
|
|
|
// Remove from database
|
|
removeReminderFromDatabase(reminderId);
|
|
|
|
Log.i(TAG, "Daily reminder cancelled: " + reminderId);
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error cancelling daily reminder", e);
|
|
call.reject("Daily reminder cancellation failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
public void getScheduledReminders(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Getting scheduled reminders");
|
|
|
|
// Get reminders from database
|
|
java.util.List<DailyReminderInfo> reminders = getRemindersFromDatabase();
|
|
|
|
// Convert to JSObject array
|
|
JSObject result = new JSObject();
|
|
result.put("reminders", reminders);
|
|
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting scheduled reminders", e);
|
|
call.reject("Failed to get scheduled reminders: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
public void updateDailyReminder(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Updating daily reminder");
|
|
|
|
String reminderId = call.getString("reminderId");
|
|
if (reminderId == null) {
|
|
call.reject("Missing reminderId parameter");
|
|
return;
|
|
}
|
|
|
|
// Extract updated options
|
|
String title = call.getString("title");
|
|
String body = call.getString("body");
|
|
String time = call.getString("time");
|
|
Boolean sound = call.getBoolean("sound");
|
|
Boolean vibration = call.getBoolean("vibration");
|
|
String priority = call.getString("priority");
|
|
Boolean repeatDaily = call.getBoolean("repeatDaily");
|
|
String timezone = call.getString("timezone");
|
|
|
|
// Cancel existing reminder (use prefixed ID)
|
|
scheduler.cancelNotification("reminder_" + reminderId);
|
|
|
|
// Update in database
|
|
updateReminderInDatabase(reminderId, title, body, time, sound, vibration, priority, repeatDaily, timezone);
|
|
|
|
// Reschedule with new settings
|
|
if (title != null && body != null && time != null) {
|
|
// Create new reminder content
|
|
NotificationContent reminderContent = new NotificationContent();
|
|
reminderContent.setId("reminder_" + reminderId); // Prefix to identify as reminder
|
|
reminderContent.setTitle(title);
|
|
reminderContent.setBody(body);
|
|
reminderContent.setSound(sound != null ? sound : true);
|
|
reminderContent.setPriority(priority != null ? priority : "normal");
|
|
reminderContent.setFetchTime(System.currentTimeMillis());
|
|
|
|
// Calculate next trigger time
|
|
String[] timeParts = time.split(":");
|
|
int hour = Integer.parseInt(timeParts[0]);
|
|
int minute = Integer.parseInt(timeParts[1]);
|
|
|
|
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 (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
|
|
calendar.add(Calendar.DAY_OF_MONTH, 1);
|
|
}
|
|
|
|
reminderContent.setScheduledTime(calendar.getTimeInMillis());
|
|
|
|
// Schedule the updated notification
|
|
boolean scheduled = scheduler.scheduleNotification(reminderContent);
|
|
|
|
if (!scheduled) {
|
|
call.reject("Failed to reschedule updated reminder");
|
|
return;
|
|
}
|
|
}
|
|
|
|
Log.i(TAG, "Daily reminder updated: " + reminderId);
|
|
call.resolve();
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error updating daily reminder", e);
|
|
call.reject("Daily reminder update failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// Helper methods for reminder database operations
|
|
private void storeReminderInDatabase(String id, String title, String body, String time,
|
|
boolean sound, boolean vibration, String priority,
|
|
boolean repeatDaily, String timezone) {
|
|
try {
|
|
SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = prefs.edit();
|
|
|
|
editor.putString(id + "_title", title);
|
|
editor.putString(id + "_body", body);
|
|
editor.putString(id + "_time", time);
|
|
editor.putBoolean(id + "_sound", sound);
|
|
editor.putBoolean(id + "_vibration", vibration);
|
|
editor.putString(id + "_priority", priority);
|
|
editor.putBoolean(id + "_repeatDaily", repeatDaily);
|
|
editor.putString(id + "_timezone", timezone);
|
|
editor.putLong(id + "_createdAt", System.currentTimeMillis());
|
|
editor.putBoolean(id + "_isScheduled", true);
|
|
|
|
editor.apply();
|
|
Log.d(TAG, "Reminder stored in database: " + id);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error storing reminder in database", e);
|
|
}
|
|
}
|
|
|
|
private void removeReminderFromDatabase(String id) {
|
|
try {
|
|
SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = prefs.edit();
|
|
|
|
editor.remove(id + "_title");
|
|
editor.remove(id + "_body");
|
|
editor.remove(id + "_time");
|
|
editor.remove(id + "_sound");
|
|
editor.remove(id + "_vibration");
|
|
editor.remove(id + "_priority");
|
|
editor.remove(id + "_repeatDaily");
|
|
editor.remove(id + "_timezone");
|
|
editor.remove(id + "_createdAt");
|
|
editor.remove(id + "_isScheduled");
|
|
editor.remove(id + "_lastTriggered");
|
|
|
|
editor.apply();
|
|
Log.d(TAG, "Reminder removed from database: " + id);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error removing reminder from database", e);
|
|
}
|
|
}
|
|
|
|
private java.util.List<DailyReminderInfo> getRemindersFromDatabase() {
|
|
java.util.List<DailyReminderInfo> reminders = new java.util.ArrayList<>();
|
|
|
|
try {
|
|
SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE);
|
|
java.util.Map<String, ?> allEntries = prefs.getAll();
|
|
|
|
java.util.Set<String> reminderIds = new java.util.HashSet<>();
|
|
for (String key : allEntries.keySet()) {
|
|
if (key.endsWith("_title")) {
|
|
String id = key.substring(0, key.length() - 6); // Remove "_title"
|
|
reminderIds.add(id);
|
|
}
|
|
}
|
|
|
|
for (String id : reminderIds) {
|
|
DailyReminderInfo reminder = new DailyReminderInfo();
|
|
reminder.id = id;
|
|
reminder.title = prefs.getString(id + "_title", "");
|
|
reminder.body = prefs.getString(id + "_body", "");
|
|
reminder.time = prefs.getString(id + "_time", "");
|
|
reminder.sound = prefs.getBoolean(id + "_sound", true);
|
|
reminder.vibration = prefs.getBoolean(id + "_vibration", true);
|
|
reminder.priority = prefs.getString(id + "_priority", "normal");
|
|
reminder.repeatDaily = prefs.getBoolean(id + "_repeatDaily", true);
|
|
reminder.timezone = prefs.getString(id + "_timezone", null);
|
|
reminder.isScheduled = prefs.getBoolean(id + "_isScheduled", false);
|
|
reminder.createdAt = prefs.getLong(id + "_createdAt", 0);
|
|
reminder.lastTriggered = prefs.getLong(id + "_lastTriggered", 0);
|
|
|
|
// Calculate next trigger time
|
|
String[] timeParts = reminder.time.split(":");
|
|
int hour = Integer.parseInt(timeParts[0]);
|
|
int minute = Integer.parseInt(timeParts[1]);
|
|
|
|
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 (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
|
|
calendar.add(Calendar.DAY_OF_MONTH, 1);
|
|
}
|
|
|
|
reminder.nextTriggerTime = calendar.getTimeInMillis();
|
|
|
|
reminders.add(reminder);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error getting reminders from database", e);
|
|
}
|
|
|
|
return reminders;
|
|
}
|
|
|
|
private void updateReminderInDatabase(String id, String title, String body, String time,
|
|
Boolean sound, Boolean vibration, String priority,
|
|
Boolean repeatDaily, String timezone) {
|
|
try {
|
|
SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = prefs.edit();
|
|
|
|
if (title != null) editor.putString(id + "_title", title);
|
|
if (body != null) editor.putString(id + "_body", body);
|
|
if (time != null) editor.putString(id + "_time", time);
|
|
if (sound != null) editor.putBoolean(id + "_sound", sound);
|
|
if (vibration != null) editor.putBoolean(id + "_vibration", vibration);
|
|
if (priority != null) editor.putString(id + "_priority", priority);
|
|
if (repeatDaily != null) editor.putBoolean(id + "_repeatDaily", repeatDaily);
|
|
if (timezone != null) editor.putString(id + "_timezone", timezone);
|
|
|
|
editor.apply();
|
|
Log.d(TAG, "Reminder updated in database: " + id);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error updating reminder in database", e);
|
|
}
|
|
}
|
|
|
|
// Data class for reminder info
|
|
public static class DailyReminderInfo {
|
|
public String id;
|
|
public String title;
|
|
public String body;
|
|
public String time;
|
|
public boolean sound;
|
|
public boolean vibration;
|
|
public String priority;
|
|
public boolean repeatDaily;
|
|
public String timezone;
|
|
public boolean isScheduled;
|
|
public long nextTriggerTime;
|
|
public long createdAt;
|
|
public long lastTriggered;
|
|
}
|
|
}
|
|
|