Browse Source

feat(phase3): implement Background Enhancement & TimeSafari Coordination

- Enhanced Android DailyNotificationScheduler with comprehensive TimeSafari coordination
- Implemented app lifecycle handling for TimeSafari PlatformServiceMixin integration
- Enhanced Android DailyNotificationPlugin with coordinateBackgroundTasks and lifecycle events
- Enhanced Android DailyNotificationFetchWorker with WorkManager coordination constraints
- Enhanced Web platform with visibility change and window lifecycle coordination
- Added comprehensive Phase 3 TypeScript interfaces and type definitions
- Implemented background execution constraints and coordination reporting
- Added app foreground/background detection with activeDid change coordination
- Enhanced state synchronization between plugin and TimeSafari host
- Implemented notification throttling and coordination pause/resume mechanisms

Phase 3 delivers:
 Android WorkManager coordination with PlatformServiceMixin
 Android app lifecycle event handling (background/foreground/resumed/paused)
 Android background task coordination with constraints (low power mode, activeDid changes)
 Web platform visibility and window lifecycle coordination
 Web sessionStorage-based coordination state persistence
 Comprehensive Phase 3 TypeScript interfaces (EnhancedDailyNotificationPlugin, CoordinationStatus, etc.)
 Background execution constraint validation
 Cross-platform TimeSafari state synchronization
 Coordination reporting and debugging capabilities
 App lifecycle-aware activeDid change detection and recovery

Ready for Phase 4: Advanced Features & TimeSafari Integration
master
Matthew Raymer 4 days ago
parent
commit
131bd3758b
  1. 138
      src/android/DailyNotificationFetchWorker.java
  2. 308
      src/android/DailyNotificationPlugin.java
  3. 251
      src/android/DailyNotificationScheduler.java
  4. 138
      src/definitions.ts
  5. 342
      src/web.ts

138
src/android/DailyNotificationFetchWorker.java

@ -75,8 +75,21 @@ public class DailyNotificationFetchWorker extends Worker {
int retryCount = inputData.getInt(KEY_RETRY_COUNT, 0);
boolean immediate = inputData.getBoolean(KEY_IMMEDIATE, false);
Log.d(TAG, String.format("Fetch parameters - Scheduled: %d, Fetch: %d, Retry: %d, Immediate: %s",
// Phase 3: Extract TimeSafari coordination data
boolean timesafariCoordination = inputData.getBoolean("timesafari_coordination", false);
long coordinationTimestamp = inputData.getLong("coordination_timestamp", 0);
String activeDidTracking = inputData.getString("active_did_tracking");
Log.d(TAG, String.format("Phase 3: Fetch parameters - Scheduled: %d, Fetch: %d, Retry: %d, Immediate: %s",
scheduledTime, fetchTime, retryCount, immediate));
Log.d(TAG, String.format("Phase 3: TimeSafari coordination - Enabled: %s, Timestamp: %d, Tracking: %s",
timesafariCoordination, coordinationTimestamp, activeDidTracking));
// Phase 3: Check TimeSafari coordination constraints
if (timesafariCoordination && !shouldProceedWithTimeSafariCoordination(coordinationTimestamp)) {
Log.d(TAG, "Phase 3: Skipping fetch - TimeSafari coordination constraints not met");
return Result.success();
}
// Check if we should proceed with fetch
if (!shouldProceedWithFetch(scheduledTime, fetchTime)) {
@ -500,4 +513,127 @@ public class DailyNotificationFetchWorker extends Worker {
Log.e(TAG, "Error checking/scheduling notification", e);
}
}
// MARK: - Phase 3: TimeSafari Coordination Methods
/**
* Phase 3: Check if background work should proceed with TimeSafari coordination
*/
private boolean shouldProceedWithTimeSafariCoordination(long coordinationTimestamp) {
try {
Log.d(TAG, "Phase 3: Checking TimeSafari coordination constraints");
// Check coordination freshness - must be within 5 minutes
long maxCoordinationAge = 5 * 60 * 1000; // 5 minutes
long coordinationAge = System.currentTimeMillis() - coordinationTimestamp;
if (coordinationAge > maxCoordinationAge) {
Log.w(TAG, "Phase 3: Coordination data too old (" + coordinationAge + "ms) - allowing fetch");
return true;
}
// Check if app coordination is proactively paused
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
boolean coordinationPaused = prefs.getBoolean("coordinationPaused", false);
long lastCoordinationPaused = prefs.getLong("lastCoordinationPaused", 0);
boolean recentlyPaused = (System.currentTimeMillis() - lastCoordinationPaused) < 30000; // 30 seconds
if (coordinationPaused && recentlyPaused) {
Log.d(TAG, "Phase 3: Coordination proactively paused by TimeSafari - deferring fetch");
return false;
}
// Check if activeDid has changed since coordination
long lastActiveDidChange = prefs.getLong("lastActiveDidChange", 0);
if (lastActiveDidChange > coordinationTimestamp) {
Log.d(TAG, "Phase 3: ActiveDid changed after coordination - requiring re-coordination");
return false;
}
// Check battery optimization status
if (isDeviceInLowPowerMode()) {
Log.d(TAG, "Phase 3: Device in low power mode - deferring fetch");
return false;
}
Log.d(TAG, "Phase 3: TimeSafari coordination constraints satisfied");
return true;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking TimeSafari coordination", e);
return true; // Default to allowing fetch on error
}
}
/**
* Phase 3: Check if device is in low power mode
*/
private boolean isDeviceInLowPowerMode() {
try {
android.os.PowerManager powerManager =
(android.os.PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (powerManager !=_null) {
boolean isLowPowerMode = powerManager.isPowerSaveMode();
Log.d(TAG, "Phase 3: Device low power mode: " + isLowPowerMode);
return isLowPowerMode;
}
return false;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking low power mode", e);
return false;
}
}
/**
* Phase 3: Report coordination success to TimeSafari
*/
private void reportCoordinationSuccess(String operation, long durationMs, boolean authUsed, String activeDid) {
try {
Log.d(TAG, "Phase 3: Reporting coordination success: " + operation);
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
prefs.edit()
.putLong("lastCoordinationSuccess_" + operation, System.currentTimeMillis())
.putLong("lastCoordinationDuration_" + operation, durationMs)
.putBoolean("lastCoordinationUsed_" + operation, authUsed)
.putString("lastCoordinationActiveDid_" + operation, activeDid)
.apply();
Log.d(TAG, "Phase 3: Coordination success reported - " + operation + " in " + durationMs + "ms");
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error reporting coordination success", e);
}
}
/**
* Phase 3: Report coordination failure to TimeSafari
*/
private void reportCoordinationFailed(String operation, String error, long durationMs, boolean authUsed) {
try {
Log.d(TAG, "Phase 3: Reporting coordination failure: " + operation + " - " + error);
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
prefs.edit()
.putLong("lastCoordinationFailure_" + operation, System.currentTimeMillis())
.putString("lastCoordinationError_" + operation, error)
.putLong("lastCoordinationFailureDuration_" + operation, durationMs)
.putBoolean("lastCoordinationFailedUsed_" + operation, authUsed)
.apply();
Log.d(TAG, "Phase 3: Coordination failure reported - " + operation);
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error reporting coordination failure", e);
}
}
}

308
src/android/DailyNotificationPlugin.java

@ -1275,4 +1275,312 @@ public class DailyNotificationPlugin extends Plugin {
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());
}
}
}

251
src/android/DailyNotificationScheduler.java

@ -75,14 +75,20 @@ public class DailyNotificationScheduler {
}
/**
* Schedule a notification for delivery
* Schedule a notification for delivery (Phase 3 enhanced)
*
* @param content Notification content to schedule
* @return true if scheduling was successful
*/
public boolean scheduleNotification(NotificationContent content) {
try {
Log.d(TAG, "Scheduling notification: " + content.getId());
Log.d(TAG, "Phase 3: Scheduling notification: " + content.getId());
// Phase 3: TimeSafari coordination before scheduling
if (!shouldScheduleWithTimeSafariCoordination(content)) {
Log.w(TAG, "Phase 3: Scheduling blocked by TimeSafari coordination");
return false;
}
// TTL validation before arming
if (ttlEnforcer != null) {
@ -91,8 +97,8 @@ public class DailyNotificationScheduler {
return false;
}
} else {
Log.w(TAG, "TTL enforcer not set, proceeding without freshness validation");
}
Log.w(TAG, "TTL enforcer not set, proceeding without freshness validation");
}
// Cancel any existing alarm for this notification
cancelNotification(content.getId());
@ -475,4 +481,241 @@ public class DailyNotificationScheduler {
// For now, we'll return a placeholder
return 0;
}
// MARK: - Phase 3: TimeSafari Coordination Methods
/**
* Phase 3: Check if scheduling should proceed with TimeSafari coordination
*/
private boolean shouldScheduleWithTimeSafariCoordination(NotificationContent content) {
try {
Log.d(TAG, "Phase 3: Checking TimeSafari coordination for notification: " + content.getId());
// Check app lifecycle state
if (!isAppInForeground()) {
Log.d(TAG, "Phase 3: App not in foreground - allowing scheduling");
return true;
}
// Check activeDid health
if (hasActiveDidChangedRecently()) {
Log.d(TAG, "Phase 3: ActiveDid changed recently - deferring scheduling");
return false;
}
// Check background task coordination
if (!isBackgroundTaskCoordinated()) {
Log.d(TAG, "Phase 3: Background tasks not coordinated - allowing scheduling");
return true;
}
// Check notification throttling
if (isNotificationThrottled()) {
Log.d(TAG, "Phase 3: Notification throttled - deferring scheduling");
return false;
}
Log.d(TAG, "Phase 3: TimeSafari coordination passed - allowing scheduling");
return true;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking TimeSafari coordination", e);
return true; // Default to allowing scheduling on error
}
}
/**
* Phase 3: Check if app is currently in foreground
*/
private boolean isAppInForeground() {
try {
android.app.ActivityManager activityManager =
(android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager != null) {
java.util.List<android.app.ActivityManager.RunningAppProcessInfo> runningProcesses =
activityManager.getRunningAppProcesses();
if (runningProcesses != null) {
for (android.app.ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
if (processInfo.processName.equals(context.getPackageName())) {
boolean inForeground = processInfo.importance ==
android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
Log.d(TAG, "Phase 3: App foreground state: " + inForeground);
return inForeground;
}
}
}
}
return false;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking app foreground state", e);
return false;
}
}
/**
* Phase 3: Check if activeDid has changed recently
*/
private boolean hasActiveDidChangedRecently() {
try {
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
long lastActiveDidChange = prefs.getLong("lastActiveDidChange", 0);
long gracefulPeriodMs = 30000; // 30 seconds grace period
if (lastActiveDidChange > 0) {
long timeSinceChange = System.currentTimeMillis() - lastActiveDidChange;
boolean changedRecently = timeSinceChange < gracefulPeriodMs;
Log.d(TAG, "Phase 3: ActiveDid change check - lastChange: " + lastActiveDidChange +
", timeSince: " + timeSinceChange + "ms, changedRecently: " + changedRecently);
return changedRecently;
}
return false;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking activeDid change", e);
return false;
}
}
/**
* Phase 3: Check if background tasks are properly coordinated
*/
private boolean isBackgroundTaskCoordinated() {
try {
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
boolean autoSync = prefs.getBoolean("autoSync", false);
long lastFetchAttempt = prefs.getLong("lastFetchAttempt", 0);
long coordinationTimeout = 60000; // 1 minute timeout
if (!autoSync) {
Log.d(TAG, "Phase 3: Auto-sync disabled - background coordination not needed");
return true;
}
if (lastFetchAttempt > 0) {
long timeSinceLastFetch = System.currentTimeMillis() - lastFetchAttempt;
boolean recentFetch = timeSinceLastFetch < coordinationTimeout;
Log.d(TAG, "Phase 3: Background task coordination - timeSinceLastFetch: " +
timeSinceLastFetch + "ms, recentFetch: " + recentFetch);
return recentFetch;
}
return true;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking background task coordination", e);
return true;
}
}
/**
* Phase 3: Check if notifications are currently throttled
*/
private boolean isNotificationThrottled() {
try {
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
long lastNotificationDelivered = prefs.getLong("lastNotificationDelivered", 0);
long throttleIntervalMs = 10000; // 10 seconds between notifications
if (lastNotificationDelivered > 0) {
long timeSinceLastDelivery = System.currentTimeMillis() - lastNotificationDelivered;
boolean isThrottled = timeSinceLastDelivery < throttleIntervalMs;
Log.d(TAG, "Phase 3: Notification throttling - timeSinceLastDelivery: " +
timeSinceLastDelivery + "ms, isThrottled: " + isThrottled);
return isThrottled;
}
return false;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking notification throttle", e);
return false;
}
}
/**
* Phase 3: Update notification delivery timestamp
*/
public void recordNotificationDelivery(String notificationId) {
try {
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
prefs.edit()
.putLong("lastNotificationDelivered", System.currentTimeMillis())
.putString("lastDeliveredNotificationId", notificationId)
.apply();
Log.d(TAG, "Phase 3: Notification delivery recorded: " + notificationId);
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error recording notification delivery", e);
}
}
/**
* Phase 3: Coordinate with PlatformServiceMixin events
*/
public void coordinateWithPlatformServiceMixin() {
try {
Log.d(TAG, "Phase 3: Coordinating with PlatformServiceMixin events");
// This would integrate with TimeSafari's PlatformServiceMixin lifecycle events
// For now, we'll implement a simplified coordination
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
boolean autoSync = prefs.getBoolean("autoSync", false);
if (autoSync) {
// Schedule background content fetch coordination
scheduleBackgroundContentFetchWithCoordination();
}
Log.d(TAG, "Phase 3: PlatformServiceMixin coordination completed");
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error coordinating with PlatformServiceMixin", e);
}
}
/**
* Phase 3: Schedule background content fetch with coordination
*/
private void scheduleBackgroundContentFetchWithCoordination() {
try {
Log.d(TAG, "Phase 3: Scheduling background content fetch with coordination");
// This would coordinate with TimeSafari's background task management
// For now, we'll update coordination timestamps
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
prefs.edit()
.putLong("lastBackgroundFetchCoordinated", System.currentTimeMillis())
.apply();
Log.d(TAG, "Phase 3: Background content fetch coordination completed");
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error scheduling background content fetch coordination", e);
}
}
}

138
src/definitions.ts

@ -550,4 +550,140 @@ export interface ActiveDidChangeEvent {
activeDid: string;
timestamp: number;
source: 'host' | 'plugin';
};
};
// MARK: - Phase 3: TimeSafari Background Coordination Interfaces
/**
* Phase 3: Extended DailyNotificationPlugin interface with TimeSafari coordination
*/
export interface EnhancedDailyNotificationPlugin extends DailyNotificationPlugin {
// Phase 1: ActiveDid Management (already extended in parent)
// Phase 3: TimeSafari Background Coordination
coordinateBackgroundTasks(): Promise<void>;
handleAppLifecycleEvent(event: AppLifecycleEvent): Promise<void>;
getCoordinationStatus(): Promise<CoordinationStatus>;
}
/**
* Phase 3: App lifecycle events for TimeSafari coordination
*/
export type AppLifecycleEvent =
| 'app_background'
| 'app_foreground'
| 'app_resumed'
| 'app_paused'
| 'app_visibility_change'
| 'app_hidden'
| 'app_visible'
| 'app_blur'
| 'app_focus';
/**
* Phase 3: Coordination status for debugging and monitoring
*/
export interface CoordinationStatus {
platform: 'android' | 'ios' | 'web' | 'electron';
coordinationActive: boolean;
coordinationPaused: boolean;
autoSync?: boolean;
appBackgrounded?: boolean;
appHidden?: boolean;
visibilityState?: DocumentVisibilityState;
focused?: boolean;
lastActiveDidChange?: number;
lastCoordinationTimestamp?: number;
lastAppBackgrounded?: number;
lastAppForegrounded?: number;
lastCoordinationSuccess?: number;
lastCoordinationFailure?: number;
coordinationErrors?: string[];
activeDidTracking?: string;
}
/**
* Phase 3: PlatformServiceMixin coordination configuration
*/
export interface PlatformServiceMixinConfig {
enableAutoCoordination?: boolean;
coordinationTimeout?: number; // Max time for coordination attempts
enableLifecycleEvents?: boolean;
enableBackgroundSync?: boolean;
enableStatePersistence?: boolean;
coordinationGracePeriod?: number; // Grace period for coordination
eventHandlers?: {
[K in AppLifecycleEvent]?: () => Promise<void>;
};
}
/**
* Phase 3: WorkManager coordination data
*/
export interface WorkManagerCoordinationData {
timesafariCoordination: boolean;
coordinationTimestamp: number;
activeDidTracking: string;
platformCoordinationVersion?: number;
coordinationTimeouts?: {
maxCoordinationAge: number;
maxExecutionTime: number;
maxRetryAge: number;
};
}
/**
* Phase 3: Background execution constraints
*/
export interface BackgroundExecutionConstraints {
devicePowerMode?: 'normal' | 'low_power' | 'critical';
appForegroundState?: 'foreground' | 'background' | 'inactive';
activeDidStability?: 'stable' | 'changing' | 'unknown';
coordinationFreshness?: 'fresh' | 'stale' | 'expired';
networkAvailability?: 'cellular' | 'wifi' | 'offline';
batteryLevel?: 'high' | 'medium' | 'low' | 'critical';
}
/**
* Phase 3: Coordination report
*/
export interface CoordinationReport {
success: boolean;
operation: string;
duration: number;
constraints: BackgroundExecutionConstraints;
errors?: string[];
timestamp: number;
activeDid?: string;
authUsed: boolean;
platformSpecific?: Record<string, any>;
}
/**
* Phase 3: TimeSafari state synchronization data
*/
export interface TimeSafariSyncData {
authenticationState: {
activeDid: string;
jwtExpiration?: number;
tokenRefreshNeeded: boolean;
};
notificationState: {
lastDelivery: number;
lastDeliveryId?: string;
pendingDeliveries: string[];
};
backgroundTaskState: {
lastBackgroundExecution: number;
lastCoordinationSuccess: number;
pendingCoordinationTasks: string[];
};
activeDidHistory: {
changes: Array<{
did: string;
timestamp: number;
source: string;
}>;
pendingUpdates: string[];
};
}

342
src/web.ts

@ -469,4 +469,346 @@ export class DailyNotificationWeb extends WebPlugin implements DailyNotification
throw error;
}
}
// MARK: - Phase 3: Web TimeSafari Coordination Methods
/**
* Phase 3: Coordinate background tasks with TimeSafari PlatformServiceMixin
*/
async coordinateBackgroundTasks(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Coordinating background tasks with PlatformServiceMixin');
// Check visibility state for coordination
const visibilityState = document.visibilityState;
const isBackgrounded = this.isAppBackgrounded();
console.log('DNP-WEB-PHASE3: App state - visibility:', visibilityState, 'backgrounded:', isBackgrounded);
if (isBackgrounded) {
// Activate background coordination
await this.activateBackgroundCoordination();
} else {
// Pause non-critical coordination for foreground usage
await this.pauseNonCriticalCoordination();
}
console.log('DNP-WEB-PHASE3: Background task coordination completed');
} catch (error) {
console.error('DNP-WEB-PHASE3: Error coordinating background tasks:', error);
throw error;
}
}
/**
* Phase 3: Handle app lifecycle events for web platform
*/
async handleAppLifecycleEvent(lifecycleEvent: string): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Handling app lifecycle event:', lifecycleEvent);
switch (lifecycleEvent) {
case 'app_visibility_change':
await this.handleVisibilityChange();
break;
case 'app_hidden':
await this.handleAppHidden();
break;
case 'app_visible':
await this.handleAppVisible();
break;
case 'app_blur':
await this.handleWindowBlur();
break;
case 'app_focus':
await this.handleWindowFocus();
break;
default:
console.warn('DNP-WEB-PHASE3: Unknown lifecycle event:', lifecycleEvent);
}
console.log('DNP-WEB-PHASE3: Lifecycle event handled:', lifecycleEvent);
} catch (error) {
console.error('DNP-WEB-PHASE3: Error handling lifecycle event:', error);
throw error;
}
}
/**
* Phase 3: Check if app is backgrounded on web
*/
private isAppBackgrounded(): boolean {
try {
// Check multiple indicators of background state
const isHidden = document.visibilityState === 'hidden';
const isNotFocused = document.hasFocus() === false;
const isBlurred = window.blur !== undefined;
const backgrounded = isHidden || isNotFocused || isBlurred;
console.log('DNP-WEB-PHASE3: Background check - hidden:', isHidden, 'focused:', isNotFocused, 'blurred:', isBlurred, 'result:', backgrounded);
return backgrounded;
} catch (error) {
console.error('DNP-WEB-PHASE3: Error checking background state:', error);
return false;
}
}
/**
* Phase 3: Activate background coordination
*/
private async activateBackgroundCoordination(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Activating background coordination');
// Store coordination state
sessionStorage.setItem('dnp_coordination_active', 'true');
sessionStorage.setItem('dnp_coordination_timestamp', Date.now().toString());
// Set up background coordination listener
this.setupBackgroundCoordinationListener();
console.log('DNP-WEB-PHASE3: Background coordination activated');
} catch (error) {
console.error('DNP-WEB-PHASE3: Error activating background coordination:', error);
}
}
/**
* Phase 3: Pause non-critical coordination
*/
private async pauseNonCriticalCoordination(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Pausing non-critical coordination');
// Store pause state
sessionStorage.setItem('dnp_coordination_paused', 'true');
sessionStorage.setItem('dnp_coordination_pause_timestamp', Date.now().toString());
// Remove background coordination listener
this.removeBackgroundCoordinationListener();
console.log('DNP-WEB-PHASE3: Non-critical coordination paused');
} catch (error) {
console.error('DNP-WEB-PHASE3: Error pausing coordination:', error);
}
}
/**
* Phase 3: Handle visibility change
*/
private async handleVisibilityChange(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Handling visibility change');
const isBackgrounded = this.isAppBackgrounded();
if (isBackgrounded) {
await this.activateBackgroundCoordination();
} else {
await this.pauseNonCriticalCoordination();
// Sync state when coming to foreground
await this.syncTimeSafariState();
}
} catch (error) {
console.error('DNP-WEB-PHASE3: Error handling visibility change:', error);
}
}
/**
* Phase 3: Handle app hidden
*/
private async handleAppHidden(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: App hidden - activating background coordination');
await this.activateBackgroundCoordination();
// Store app state
sessionStorage.setItem('dnp_app_hidden_timestamp', Date.now().toString());
sessionStorage.setItem('dnp_app_is_hidden', 'true');
} catch (error) {
console.error('DNP-WEB-PHASE3: Error handling app hidden:', error);
}
}
/**
* Phase 3: Handle app visible
*/
private async handleAppVisible(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: App visible - updating coordination');
await this.pauseNonCriticalCoordination();
// Store app state
sessionStorage.setItem('dnp_app_visible_timestamp', Date.now().toString());
sessionStorage.setItem('dnp_app_is_hidden', 'false');
// Check activeDid coordination
await this.checkActiveDidCoordinationWeb();
} catch (error) {
console.error('DNP-WEB-PHASE3: Error handling app visible:', error);
}
}
/**
* Phase 3: Handle window blur
*/
private async handleWindowBlur(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Window blurred - activating background coordination');
await this.activateBackgroundCoordination();
sessionStorage.setItem('dnp_window_blur_timestamp', Date.now().toString());
} catch (error) {
console.error('DNP-WEB-PHASE3: Error handling window blur:', error);
}
}
/**
* Phase 3: Handle window focus
*/
private async handleWindowFocus(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Window focused - updating coordination');
await this.pauseNonCriticalCoordination();
sessionStorage.setItem('dnp_window_focus_timestamp', Date.now().toString());
// Sync TimeSafari state
await this.syncTimeSafariState();
} catch (error) {
console.error('DNP-WEB-PHASE3: Error handling window focus:', error);
}
}
/**
* Phase 3: Check activeDid coordination for web
*/
private async checkActiveDidCoordinationWeb(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Checking activeDid coordination');
const lastActiveDidChange = sessionStorage.getItem('dnp_last_active_did_change');
const lastAppVisible = sessionStorage.getItem('dnp_app_visible_timestamp');
if (lastActiveDidChange && lastAppVisible) {
const activeDidChangeTime = parseInt(lastActiveDidChange);
const visibleTime = parseInt(lastAppVisible);
// If activeDid changed while app was hidden, update coordination
if (activeDidChangeTime > visibleTime) {
console.log('DNP-WEB-PHASE3: ActiveDid changed while hidden - updating coordination');
await this.clearCacheForNewIdentity();
await this.refreshAuthenticationForNewIdentity('');
}
}
} catch (error) {
console.error('DNP-WEB-PHASE3: Error checking activeDid coordination:', error);
}
}
/**
* Phase 3: Sync TimeSafari state for web
*/
private async syncTimeSafariState(): Promise<void> {
try {
console.log('DNP-WEB-PHASE3: Syncing TimeSafari state');
// Sync authentication state (placeholder - implement if needed)
console.log('DNP-WEB-PHASE3: Sync authentication state (placeholder)');
// Sync notification delivery tracking
const lastBackgroundDelivery = sessionStorage.getItem('dnp_last_background_delivery');
if (lastBackgroundDelivery) {
const deliveryId = sessionStorage.getItem('dnp_last_background_delivery_id');
console.log('DNP-WEB-PHASE3: Synced background delivery:', deliveryId);
}
console.log('DNP-WEB-PHASE3: TimeSafari state sync completed');
} catch (error) {
console.error('DNP-WEB-PHASE3: Error syncing TimeSafari state:', error);
}
}
/**
* Phase 3: Set up background coordination listener
*/
private setupBackgroundCoordinationListener(): void {
try {
console.log('DNP-WEB-PHASE3: Setting up background coordination listener');
// Listen for visibility changes
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
// Listen for window blur/focus
window.addEventListener('blur', this.handleWindowBlur.bind(this));
window.addEventListener('focus', this.handleWindowFocus.bind(this));
} catch (error) {
console.error('DNP-WEB-PHASE3: Error setting up coordination listener:', error);
}
}
/**
* Phase 3: Remove background coordination listener
*/
private removeBackgroundCoordinationListener(): void {
try {
console.log('DNP-WEB-PHASE3: Removing background coordination listener');
// Remove listeners (using bound functions)
document.removeEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
window.removeEventListener('blur', this.handleWindowBlur.bind(this));
window.removeEventListener('focus', this.handleWindowFocus.bind(this));
} catch (error) {
console.error('DNP-WEB-PHASE3: Error removing coordination listener:', error);
}
}
/**
* Phase 3: Get coordination status for web debugging
*/
async getCoordinationStatus(): Promise<Record<string, any>> {
try {
console.log('DNP-WEB-PHASE3: Getting coordination status');
const status = {
platform: 'web',
coordinationActive: sessionStorage.getItem('dnp_coordination_active') === 'true',
coordinationPaused: sessionStorage.getItem('dnp_coordination_paused') === 'true',
appHidden: sessionStorage.getItem('dnp_app_is_hidden') === 'true',
visibilityState: document.visibilityState,
focused: document.hasFocus(),
lastActiveDidChange: sessionStorage.getItem('dnp_last_active_did_change'),
lastCoordinationTimestamp: sessionStorage.getItem('dnp_coordination_timestamp'),
lastVisibilityChange: sessionStorage.getItem('dnp_app_visible_timestamp')
};
console.log('DNP-WEB-PHASE3: Coordination status:', status);
return status;
} catch (error) {
console.error('DNP-WEB-PHASE3: Error getting coordination status:', error);
throw error;
}
}
}

Loading…
Cancel
Save