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:
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* DailyNotificationRebootRecoveryManager.java
|
||||
*
|
||||
* Android Reboot Recovery Manager for notification restoration
|
||||
* Handles system reboots and time changes to restore scheduled notifications
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Manages recovery from system reboots and time changes
|
||||
*
|
||||
* This class implements the critical recovery functionality:
|
||||
* - Listens for system reboot broadcasts
|
||||
* - Handles time change events
|
||||
* - Restores scheduled notifications after reboot
|
||||
* - Adjusts notification times after time changes
|
||||
*/
|
||||
public class DailyNotificationRebootRecoveryManager {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private static final String TAG = "DailyNotificationRebootRecoveryManager";
|
||||
|
||||
// Broadcast actions
|
||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
private static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED";
|
||||
private static final String ACTION_PACKAGE_REPLACED = "android.intent.action.PACKAGE_REPLACED";
|
||||
private static final String ACTION_TIME_CHANGED = "android.intent.action.TIME_SET";
|
||||
private static final String ACTION_TIMEZONE_CHANGED = "android.intent.action.TIMEZONE_CHANGED";
|
||||
|
||||
// Recovery delay
|
||||
private static final long RECOVERY_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private final Context context;
|
||||
private final DailyNotificationScheduler scheduler;
|
||||
private final DailyNotificationExactAlarmManager exactAlarmManager;
|
||||
private final DailyNotificationRollingWindow rollingWindow;
|
||||
|
||||
// Broadcast receivers
|
||||
private BootCompletedReceiver bootCompletedReceiver;
|
||||
private TimeChangeReceiver timeChangeReceiver;
|
||||
|
||||
// Recovery state
|
||||
private boolean recoveryInProgress = false;
|
||||
private long lastRecoveryTime = 0;
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context Application context
|
||||
* @param scheduler Notification scheduler
|
||||
* @param exactAlarmManager Exact alarm manager
|
||||
* @param rollingWindow Rolling window manager
|
||||
*/
|
||||
public DailyNotificationRebootRecoveryManager(Context context,
|
||||
DailyNotificationScheduler scheduler,
|
||||
DailyNotificationExactAlarmManager exactAlarmManager,
|
||||
DailyNotificationRollingWindow rollingWindow) {
|
||||
this.context = context;
|
||||
this.scheduler = scheduler;
|
||||
this.exactAlarmManager = exactAlarmManager;
|
||||
this.rollingWindow = rollingWindow;
|
||||
|
||||
Log.d(TAG, "RebootRecoveryManager initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Register broadcast receivers
|
||||
*/
|
||||
public void registerReceivers() {
|
||||
try {
|
||||
Log.d(TAG, "Registering broadcast receivers");
|
||||
|
||||
// Register boot completed receiver
|
||||
bootCompletedReceiver = new BootCompletedReceiver();
|
||||
IntentFilter bootFilter = new IntentFilter();
|
||||
bootFilter.addAction(ACTION_BOOT_COMPLETED);
|
||||
bootFilter.addAction(ACTION_MY_PACKAGE_REPLACED);
|
||||
bootFilter.addAction(ACTION_PACKAGE_REPLACED);
|
||||
context.registerReceiver(bootCompletedReceiver, bootFilter);
|
||||
|
||||
// Register time change receiver
|
||||
timeChangeReceiver = new TimeChangeReceiver();
|
||||
IntentFilter timeFilter = new IntentFilter();
|
||||
timeFilter.addAction(ACTION_TIME_CHANGED);
|
||||
timeFilter.addAction(ACTION_TIMEZONE_CHANGED);
|
||||
context.registerReceiver(timeChangeReceiver, timeFilter);
|
||||
|
||||
Log.i(TAG, "Broadcast receivers registered successfully");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering broadcast receivers", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister broadcast receivers
|
||||
*/
|
||||
public void unregisterReceivers() {
|
||||
try {
|
||||
Log.d(TAG, "Unregistering broadcast receivers");
|
||||
|
||||
if (bootCompletedReceiver != null) {
|
||||
context.unregisterReceiver(bootCompletedReceiver);
|
||||
bootCompletedReceiver = null;
|
||||
}
|
||||
|
||||
if (timeChangeReceiver != null) {
|
||||
context.unregisterReceiver(timeChangeReceiver);
|
||||
timeChangeReceiver = null;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Broadcast receivers unregistered successfully");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error unregistering broadcast receivers", e);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Recovery Methods
|
||||
|
||||
/**
|
||||
* Handle system reboot recovery
|
||||
*
|
||||
* This method restores all scheduled notifications that were lost
|
||||
* during the system reboot.
|
||||
*/
|
||||
public void handleSystemReboot() {
|
||||
try {
|
||||
Log.i(TAG, "Handling system reboot recovery");
|
||||
|
||||
// Check if recovery is already in progress
|
||||
if (recoveryInProgress) {
|
||||
Log.w(TAG, "Recovery already in progress, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if recovery was recently performed
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (currentTime - lastRecoveryTime < RECOVERY_DELAY_MS) {
|
||||
Log.w(TAG, "Recovery performed recently, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryInProgress = true;
|
||||
lastRecoveryTime = currentTime;
|
||||
|
||||
// Perform recovery operations
|
||||
performRebootRecovery();
|
||||
|
||||
recoveryInProgress = false;
|
||||
|
||||
Log.i(TAG, "System reboot recovery completed");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error handling system reboot", e);
|
||||
recoveryInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle time change recovery
|
||||
*
|
||||
* This method adjusts all scheduled notifications to account
|
||||
* for system time changes.
|
||||
*/
|
||||
public void handleTimeChange() {
|
||||
try {
|
||||
Log.i(TAG, "Handling time change recovery");
|
||||
|
||||
// Check if recovery is already in progress
|
||||
if (recoveryInProgress) {
|
||||
Log.w(TAG, "Recovery already in progress, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
recoveryInProgress = true;
|
||||
|
||||
// Perform time change recovery
|
||||
performTimeChangeRecovery();
|
||||
|
||||
recoveryInProgress = false;
|
||||
|
||||
Log.i(TAG, "Time change recovery completed");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error handling time change", e);
|
||||
recoveryInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform reboot recovery operations
|
||||
*/
|
||||
private void performRebootRecovery() {
|
||||
try {
|
||||
Log.d(TAG, "Performing reboot recovery operations");
|
||||
|
||||
// Wait a bit for system to stabilize
|
||||
Thread.sleep(2000);
|
||||
|
||||
// Restore scheduled notifications
|
||||
scheduler.restoreScheduledNotifications();
|
||||
|
||||
// Restore rolling window
|
||||
rollingWindow.forceMaintenance();
|
||||
|
||||
// Log recovery statistics
|
||||
logRecoveryStatistics("reboot");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error performing reboot recovery", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform time change recovery operations
|
||||
*/
|
||||
private void performTimeChangeRecovery() {
|
||||
try {
|
||||
Log.d(TAG, "Performing time change recovery operations");
|
||||
|
||||
// Adjust scheduled notifications
|
||||
scheduler.adjustScheduledNotifications();
|
||||
|
||||
// Update rolling window
|
||||
rollingWindow.forceMaintenance();
|
||||
|
||||
// Log recovery statistics
|
||||
logRecoveryStatistics("time_change");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error performing time change recovery", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log recovery statistics
|
||||
*
|
||||
* @param recoveryType Type of recovery performed
|
||||
*/
|
||||
private void logRecoveryStatistics(String recoveryType) {
|
||||
try {
|
||||
// Get recovery statistics
|
||||
int restoredCount = scheduler.getRestoredNotificationCount();
|
||||
int adjustedCount = scheduler.getAdjustedNotificationCount();
|
||||
|
||||
Log.i(TAG, String.format("Recovery statistics (%s): restored=%d, adjusted=%d",
|
||||
recoveryType, restoredCount, adjustedCount));
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error logging recovery statistics", e);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Broadcast Receivers
|
||||
|
||||
/**
|
||||
* Broadcast receiver for boot completed events
|
||||
*/
|
||||
private class BootCompletedReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
try {
|
||||
String action = intent.getAction();
|
||||
Log.d(TAG, "BootCompletedReceiver received action: " + action);
|
||||
|
||||
if (ACTION_BOOT_COMPLETED.equals(action) ||
|
||||
ACTION_MY_PACKAGE_REPLACED.equals(action) ||
|
||||
ACTION_PACKAGE_REPLACED.equals(action)) {
|
||||
|
||||
// Handle system reboot
|
||||
handleSystemReboot();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error in BootCompletedReceiver", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast receiver for time change events
|
||||
*/
|
||||
private class TimeChangeReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
try {
|
||||
String action = intent.getAction();
|
||||
Log.d(TAG, "TimeChangeReceiver received action: " + action);
|
||||
|
||||
if (ACTION_TIME_CHANGED.equals(action) ||
|
||||
ACTION_TIMEZONE_CHANGED.equals(action)) {
|
||||
|
||||
// Handle time change
|
||||
handleTimeChange();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error in TimeChangeReceiver", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/**
|
||||
* Get recovery status
|
||||
*
|
||||
* @return Recovery status information
|
||||
*/
|
||||
public RecoveryStatus getRecoveryStatus() {
|
||||
return new RecoveryStatus(
|
||||
recoveryInProgress,
|
||||
lastRecoveryTime,
|
||||
System.currentTimeMillis() - lastRecoveryTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force recovery (for testing)
|
||||
*/
|
||||
public void forceRecovery() {
|
||||
Log.i(TAG, "Forcing recovery");
|
||||
handleSystemReboot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if recovery is needed
|
||||
*
|
||||
* @return true if recovery is needed
|
||||
*/
|
||||
public boolean isRecoveryNeeded() {
|
||||
// Check if system was recently rebooted
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeSinceLastRecovery = currentTime - lastRecoveryTime;
|
||||
|
||||
// Recovery needed if more than 1 hour since last recovery
|
||||
return timeSinceLastRecovery > TimeUnit.HOURS.toMillis(1);
|
||||
}
|
||||
|
||||
// MARK: - Status Classes
|
||||
|
||||
/**
|
||||
* Recovery status information
|
||||
*/
|
||||
public static class RecoveryStatus {
|
||||
public final boolean inProgress;
|
||||
public final long lastRecoveryTime;
|
||||
public final long timeSinceLastRecovery;
|
||||
|
||||
public RecoveryStatus(boolean inProgress, long lastRecoveryTime, long timeSinceLastRecovery) {
|
||||
this.inProgress = inProgress;
|
||||
this.lastRecoveryTime = lastRecoveryTime;
|
||||
this.timeSinceLastRecovery = timeSinceLastRecovery;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("RecoveryStatus{inProgress=%s, lastRecovery=%d, timeSince=%d}",
|
||||
inProgress, lastRecoveryTime, timeSinceLastRecovery);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user