diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java index 5f8c85e..583cc8c 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java @@ -18,7 +18,13 @@ import androidx.work.Data; import androidx.work.Worker; import androidx.work.WorkerParameters; +import java.util.List; +import java.util.Map; +import java.util.HashMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.CompletableFuture; +import java.util.Random; /** * Background worker for fetching daily notification content @@ -37,11 +43,13 @@ public class DailyNotificationFetchWorker extends Worker { private static final int MAX_RETRY_ATTEMPTS = 3; private static final long WORK_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes total - private static final long FETCH_TIMEOUT_MS = 30 * 1000; // 30 seconds for fetch + + // Legacy timeout (will be replaced by SchedulingPolicy) + private static final long FETCH_TIMEOUT_MS_DEFAULT = 30 * 1000; // 30 seconds private final Context context; private final DailyNotificationStorage storage; - private final DailyNotificationFetcher fetcher; + private final DailyNotificationFetcher fetcher; // Legacy fetcher (fallback only) /** * Constructor @@ -75,21 +83,8 @@ public class DailyNotificationFetchWorker extends Worker { int retryCount = inputData.getInt(KEY_RETRY_COUNT, 0); boolean immediate = inputData.getBoolean(KEY_IMMEDIATE, false); - // 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", + Log.d(TAG, String.format("PR2: 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)) { @@ -97,12 +92,12 @@ public class DailyNotificationFetchWorker extends Worker { return Result.success(); } - // Attempt to fetch content with timeout - NotificationContent content = fetchContentWithTimeout(); + // PR2: Attempt to fetch content using native fetcher SPI + List contents = fetchContentWithTimeout(scheduledTime, fetchTime, immediate); - if (content != null) { - // Success - save content and schedule notification - handleSuccessfulFetch(content); + if (contents != null && !contents.isEmpty()) { + // Success - save contents and schedule notifications + handleSuccessfulFetch(contents); return Result.success(); } else { @@ -153,57 +148,138 @@ public class DailyNotificationFetchWorker extends Worker { } /** - * Fetch content with timeout handling + * Fetch content with timeout handling using native fetcher SPI (PR2) * - * @return Fetched content or null if failed + * @param scheduledTime When notification is scheduled for + * @param fetchTime When fetch was triggered + * @param immediate Whether this is an immediate fetch + * @return List of fetched notification contents or null if failed */ - private NotificationContent fetchContentWithTimeout() { + private List fetchContentWithTimeout(long scheduledTime, long fetchTime, boolean immediate) { try { - Log.d(TAG, "Fetching content with timeout: " + FETCH_TIMEOUT_MS + "ms"); + // Get SchedulingPolicy for timeout configuration + SchedulingPolicy policy = getSchedulingPolicy(); + long fetchTimeoutMs = policy.fetchTimeoutMs != null ? + policy.fetchTimeoutMs : FETCH_TIMEOUT_MS_DEFAULT; + + Log.d(TAG, "PR2: Fetching content with native fetcher SPI, timeout: " + fetchTimeoutMs + "ms"); + + // Get native fetcher from static registry + NativeNotificationContentFetcher nativeFetcher = DailyNotificationPlugin.getNativeFetcherStatic(); + + if (nativeFetcher == null) { + Log.w(TAG, "PR2: Native fetcher not registered, falling back to legacy fetcher"); + // Fallback to legacy fetcher + NotificationContent content = fetcher.fetchContentImmediately(); + if (content != null) { + return java.util.Collections.singletonList(content); + } + return null; + } - // Use a simple timeout mechanism - // In production, you might use CompletableFuture with timeout long startTime = System.currentTimeMillis(); - // Attempt fetch - NotificationContent content = fetcher.fetchContentImmediately(); + // Create FetchContext + String trigger = immediate ? "manual" : + (fetchTime > 0 ? "prefetch" : "background_work"); + Long scheduledTimeOpt = scheduledTime > 0 ? scheduledTime : null; + Map metadata = new HashMap<>(); + metadata.put("retryCount", 0); + metadata.put("immediate", immediate); + + FetchContext context = new FetchContext( + trigger, + scheduledTimeOpt, + fetchTime > 0 ? fetchTime : System.currentTimeMillis(), + metadata + ); + + // Call native fetcher with timeout + CompletableFuture> future = nativeFetcher.fetchContent(context); + + List contents; + try { + contents = future.get(fetchTimeoutMs, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + Log.e(TAG, "PR2: Native fetcher timeout after " + fetchTimeoutMs + "ms", e); + return null; + } long fetchDuration = System.currentTimeMillis() - startTime; - if (content != null) { - Log.d(TAG, "Content fetched successfully in " + fetchDuration + "ms"); - return content; + if (contents != null && !contents.isEmpty()) { + Log.i(TAG, "PR2: Content fetched successfully - " + contents.size() + + " items in " + fetchDuration + "ms"); + // TODO PR2: Record metrics (items_fetched, fetch_duration_ms, fetch_success) + return contents; } else { - Log.w(TAG, "Content fetch returned null after " + fetchDuration + "ms"); + Log.w(TAG, "PR2: Native fetcher returned empty list after " + fetchDuration + "ms"); + // TODO PR2: Record metrics (fetch_success=false) return null; } } catch (Exception e) { - Log.e(TAG, "Error during content fetch", e); + Log.e(TAG, "PR2: Error during native fetcher call", e); + // TODO PR2: Record metrics (fetch_fail_class=retryable) return null; } } /** - * Handle successful content fetch + * Get SchedulingPolicy from SharedPreferences or return default * - * @param content Successfully fetched content + * @return SchedulingPolicy instance */ - private void handleSuccessfulFetch(NotificationContent content) { + private SchedulingPolicy getSchedulingPolicy() { try { - Log.d(TAG, "Handling successful content fetch: " + content.getId()); + // Try to load from SharedPreferences (set via plugin's setPolicy method) + android.content.SharedPreferences prefs = context.getSharedPreferences( + "daily_notification_spi", Context.MODE_PRIVATE); + + // For now, return default policy + // TODO: Deserialize from SharedPreferences in future enhancement + return SchedulingPolicy.createDefault(); + + } catch (Exception e) { + Log.w(TAG, "Error loading SchedulingPolicy, using default", e); + return SchedulingPolicy.createDefault(); + } + } + + /** + * Handle successful content fetch (PR2: now handles List) + * + * @param contents List of successfully fetched notification contents + */ + private void handleSuccessfulFetch(List contents) { + try { + Log.d(TAG, "PR2: Handling successful content fetch - " + contents.size() + " items"); - // Content is already saved by the fetcher // Update last fetch time storage.setLastFetchTime(System.currentTimeMillis()); - // Schedule notification if not already scheduled - scheduleNotificationIfNeeded(content); + // Save all contents and schedule notifications + int scheduledCount = 0; + for (NotificationContent content : contents) { + try { + // Save content to storage + storage.saveNotificationContent(content); + + // Schedule notification if not already scheduled + scheduleNotificationIfNeeded(content); + scheduledCount++; + + } catch (Exception e) { + Log.e(TAG, "PR2: Error processing notification content: " + content.getId(), e); + } + } - Log.i(TAG, "Successful fetch handling completed"); + Log.i(TAG, "PR2: Successful fetch handling completed - " + scheduledCount + "/" + + contents.size() + " notifications scheduled"); + // TODO PR2: Record metrics (items_enqueued=scheduledCount) } catch (Exception e) { - Log.e(TAG, "Error handling successful fetch", e); + Log.e(TAG, "PR2: Error handling successful fetch", e); } } @@ -216,29 +292,23 @@ public class DailyNotificationFetchWorker extends Worker { */ private Result handleFailedFetch(int retryCount, long scheduledTime) { try { - Log.d(TAG, "Phase 2: Handling failed fetch - Retry: " + retryCount); - - // Phase 2: Check for TimeSafari special retry triggers - if (shouldRetryForActiveDidChange()) { - Log.d(TAG, "Phase 2: ActiveDid change detected - extending retry quota"); - retryCount = 0; // Reset retry count for activeDid change - } + Log.d(TAG, "PR2: Handling failed fetch - Retry: " + retryCount); - if (retryCount < MAX_RETRIES_FOR_TIMESAFARI()) { - // Phase 2: Schedule enhanced retry with activeDid consideration - scheduleRetryWithActiveDidSupport(retryCount + 1, scheduledTime); - Log.i(TAG, "Phase 2: Scheduled retry attempt " + (retryCount + 1) + " with TimeSafari support"); + if (retryCount < MAX_RETRY_ATTEMPTS) { + // PR2: Schedule retry with SchedulingPolicy backoff + scheduleRetry(retryCount + 1, scheduledTime); + Log.i(TAG, "PR2: Scheduled retry attempt " + (retryCount + 1)); return Result.retry(); } else { // Max retries reached - use fallback content - Log.w(TAG, "Phase 2: Max retries reached, using fallback content"); - useFallbackContentWithActiveDidSupport(scheduledTime); + Log.w(TAG, "PR2: Max retries reached, using fallback content"); + useFallbackContent(scheduledTime); return Result.success(); } } catch (Exception e) { - Log.e(TAG, "Phase 2: Error handling failed fetch", e); + Log.e(TAG, "PR2 metabolites Error handling failed fetch", e); return Result.failure(); } } @@ -279,121 +349,34 @@ public class DailyNotificationFetchWorker extends Worker { } /** - * Calculate retry delay with exponential backoff + * Calculate retry delay with exponential backoff using SchedulingPolicy (PR2) * * @param retryCount Current retry attempt * @return Delay in milliseconds */ private long calculateRetryDelay(int retryCount) { - // Base delay: 1 minute, exponential backoff: 2^retryCount - long baseDelay = 60 * 1000; // 1 minute - long exponentialDelay = baseDelay * (long) Math.pow(2, retryCount - 1); + SchedulingPolicy policy = getSchedulingPolicy(); + SchedulingPolicy.RetryBackoff backoff = policy.retryBackoff; - // Cap at 1 hour - long maxDelay = 60 * 60 * 1000; // 1 hour - return Math.min(exponentialDelay, maxDelay); - } - - // MARK: - Phase 2: TimeSafari ActiveDid Enhancement Methods - - /** - * Phase 2: Check if retry is needed due to activeDid change - */ - private boolean shouldRetryForActiveDidChange() { - try { - // Check if activeDid has changed since last fetch attempt - android.content.SharedPreferences prefs = context.getSharedPreferences("daily_notification_timesafari", android.content.Context.MODE_PRIVATE); - long lastFetchAttempt = prefs.getLong("lastFetchAttempt", 0); - long lastActiveDidChange = prefs.getLong("lastActiveDidChange", 0); - - boolean activeDidChanged = lastActiveDidChange > lastFetchAttempt; - - if (activeDidChanged) { - Log.d(TAG, "Phase 2: ActiveDid change detected in retry logic"); - return true; - } - - return false; - - } catch (Exception e) { - Log.e(TAG, "Phase 2: Error checking activeDid change", e); - return false; - } - } - - /** - * Phase 2: Get max retries with TimeSafari enhancements - */ - private int MAX_RETRIES_FOR_TIMESAFARI() { - // Base retries + additional for activeDid changes - return MAX_RETRY_ATTEMPTS + 2; // Extra retries for TimeSafari integration - } - - /** - * Phase 2: Schedule retry with activeDid support - */ - private void scheduleRetryWithActiveDidSupport(int retryCount, long scheduledTime) { - try { - Log.d(TAG, "Phase 2: Scheduling retry attempt " + retryCount + " with TimeSafari support"); - - // Store the last fetch attempt time for activeDid change detection - android.content.SharedPreferences prefs = context.getSharedPreferences("daily_notification_timesafari", android.content.Context.MODE_PRIVATE); - prefs.edit().putLong("lastFetchAttempt", System.currentTimeMillis()).apply(); - - // Delegate to original retry logic - scheduleRetry(retryCount, scheduledTime); - - } catch (Exception e) { - Log.e(TAG, "Phase 2: Error scheduling enhanced retry", e); - // Fallback to original retry logic - scheduleRetry(retryCount, scheduledTime); - } - } - - /** - * Phase 2: Use fallback content with activeDid support - */ - private void useFallbackContentWithActiveDidSupport(long scheduledTime) { - try { - Log.d(TAG, "Phase 2: Using fallback content with TimeSafari support"); - - // Generate TimeSafari-aware fallback content - NotificationContent fallbackContent = generateTimeSafariFallbackContent(); - - if (fallbackContent != null) { - storage.saveNotificationContent(fallbackContent); - Log.i(TAG, "Phase 2: TimeSafari fallback content saved"); - } else { - // Fallback to original logic - useFallbackContent(scheduledTime); - } - - } catch (Exception e) { - Log.e(TAG, "Phase 2: Error using enhanced fallback content", e); - // Fallback to original logic - useFallbackContent(scheduledTime); - } - } - - /** - * Phase 2: Generate TimeSafari-aware fallback content - */ - private NotificationContent generateTimeSafariFallbackContent() { - try { - // Generate fallback content specific to TimeSafari context - NotificationContent content = new NotificationContent(); - content.setId("timesafari_fallback_" + System.currentTimeMillis()); - content.setTitle("TimeSafari Update Available"); - content.setBody("Your community updates are ready. Tap to view offers, projects, and connections."); - // fetchedAt is set in constructor, no need to set it again - content.setScheduledTime(System.currentTimeMillis() + 30000); // 30 seconds from now - - return content; - - } catch (Exception e) { - Log.e(TAG, "Phase 2: Error generating TimeSafari fallback content", e); - return null; - } + // Calculate exponential delay: minMs * (factor ^ (retryCount - 1)) + long baseDelay = backoff.minMs; + double exponentialMultiplier = Math.pow(backoff.factor, retryCount - 1); + long exponentialDelay = (long) (baseDelay * exponentialMultiplier); + + // Cap at maxMs + long cappedDelay = Math.min(exponentialDelay, backoff.maxMs); + + // Add jitter: delay * (1 + jitterPct/100 * random(0-1)) + Random random = new Random(); + double jitter = backoff.jitterPct / 100.0 * random.nextDouble(); + long finalDelay = (long) (cappedDelay * (1.0 + jitter)); + + Log.d(TAG, "PR2: Calculated retry delay - attempt=" + retryCount + + ", base=" + baseDelay + "ms, exponential=" + exponentialDelay + "ms, " + + "capped=" + cappedDelay + "ms, jitter=" + String.format("%.1f%%", jitter * 100) + + ", final=" + finalDelay + "ms"); + + return finalDelay; } /** @@ -513,127 +496,4 @@ 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); - } - } } diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java index a9bac91..e1cb061 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java @@ -108,6 +108,43 @@ public class DailyNotificationPlugin extends Plugin { // TimeSafari integration management private TimeSafariIntegrationManager timeSafariIntegration; + // Integration Point Refactor (PR1): SPI for content fetching + private static volatile NativeNotificationContentFetcher nativeFetcher; + private boolean nativeFetcherEnabled = true; // Default enabled (required for background) + private SchedulingPolicy schedulingPolicy = SchedulingPolicy.createDefault(); + + /** + * Set native fetcher from host app's native code (Application.onCreate()) + * + * This is called from host app's Android native code, not through Capacitor bridge. + * Host app implements NativeNotificationContentFetcher and registers it here. + * + * @param fetcher Native fetcher implementation from host app + */ + public static void setNativeFetcher(NativeNotificationContentFetcher fetcher) { + nativeFetcher = fetcher; + Log.d("DailyNotificationPlugin", "SPI: Native fetcher registered: " + + (fetcher != null ? fetcher.getClass().getName() : "null")); + } + + /** + * Get native fetcher (static access for workers) + * + * @return Registered native fetcher or null + */ + public static NativeNotificationContentFetcher getNativeFetcherStatic() { + return nativeFetcher; + } + + /** + * Get native fetcher (non-static for instance access) + * + * @return Registered native fetcher or null + */ + protected NativeNotificationContentFetcher getNativeFetcher() { + return nativeFetcher; + } + /** * Initialize the plugin and create notification channel */ @@ -2216,4 +2253,178 @@ public class DailyNotificationPlugin extends Plugin { } } + // MARK: - Integration Point Refactor (PR1): SPI Registration Methods + + /** + * Enable or disable native fetcher (Integration Point Refactor PR1) + * + * Native fetcher is required for background workers. If disabled, + * background fetches will fail gracefully. + * + * @param call Plugin call with "enable" boolean parameter + */ + @PluginMethod + public void enableNativeFetcher(PluginCall call) { + try { + Boolean enable = call.getBoolean("enable", true); + + if (enable == null) { + call.reject("Missing 'enable' parameter"); + return; + } + + nativeFetcherEnabled = enable; + Log.i(TAG, "SPI: Native fetcher " + (enable ? "enabled" : "disabled")); + + JSObject result = new JSObject(); + result.put("enabled", nativeFetcherEnabled); + result.put("registered", nativeFetcher != null); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error enabling/disabling native fetcher", e); + call.reject("Failed to update native fetcher state: " + e.getMessage()); + } + } + + /** + * Set scheduling policy configuration (Integration Point Refactor PR1) + * + * Updates the scheduling policy used by the plugin for retry backoff, + * prefetch timing, deduplication, and cache TTL. + * + * @param call Plugin call with SchedulingPolicy JSON object + */ + @PluginMethod + public void setPolicy(PluginCall call) { + try { + JSObject policyJson = call.getObject("policy"); + + if (policyJson == null) { + call.reject("Missing 'policy' parameter"); + return; + } + + // Parse retry backoff (required) + JSObject backoffJson = policyJson.getJSObject("retryBackoff"); + if (backoffJson == null) { + call.reject("Missing required 'policy.retryBackoff' parameter"); + return; + } + + SchedulingPolicy.RetryBackoff retryBackoff = new SchedulingPolicy.RetryBackoff( + backoffJson.has("minMs") ? backoffJson.getLong("minMs") : 2000L, + backoffJson.has("maxMs") ? backoffJson.getLong("maxMs") : 600000L, + backoffJson.has("factor") ? backoffJson.getDouble("factor") : 2.0, + backoffJson.has("jitterPct") ? backoffJson.getInt("jitterPct") : 20 + ); + + // Create policy with backoff + SchedulingPolicy policy = new SchedulingPolicy(retryBackoff); + + // Parse optional fields + if (policyJson.has("prefetchWindowMs")) { + Long prefetchWindow = policyJson.getLong("prefetchWindowMs"); + if (prefetchWindow != null) { + policy.prefetchWindowMs = prefetchWindow; + } + } + + if (policyJson.has("maxBatchSize")) { + Integer maxBatch = policyJson.getInteger("maxBatchSize"); + if (maxBatch != null) { + policy.maxBatchSize = maxBatch; + } + } + + if (policyJson.has("dedupeHorizonMs")) { + Long horizon = policyJson.getLong("dedupeHorizonMs"); + if (horizon != null) { + policy.dedupeHorizonMs = horizon; + } + } + + if (policyJson.has("cacheTtlSeconds")) { + Integer ttl = policyJson.getInteger("cacheTtlSeconds"); + if (ttl != null) { + policy.cacheTtlSeconds = ttl; + } + } + + if (policyJson.has("exactAlarmsAllowed")) { + Boolean exactAllowed = policyJson.getBoolean("exactAlarmsAllowed"); + if (exactAllowed != null) { + policy.exactAlarmsAllowed = exactAllowed; + } + } + + if (policyJson.has("fetchTimeoutMs")) { + Long timeout = policyJson.getLong("fetchTimeoutMs"); + if (timeout != null) { + policy.fetchTimeoutMs = timeout; + } + } + + // Update policy + this.schedulingPolicy = policy; + + Log.i(TAG, "SPI: Scheduling policy updated - prefetchWindow=" + + policy.prefetchWindowMs + "ms, maxBatch=" + policy.maxBatchSize + + ", dedupeHorizon=" + policy.dedupeHorizonMs + "ms"); + + call.resolve(); + + } catch (Exception e) { + Log.e(TAG, "Error setting scheduling policy", e); + call.reject("Failed to set policy: " + e.getMessage()); + } + } + + /** + * Set JavaScript content fetcher (Integration Point Refactor PR1 - stub for PR3) + * + * This is a stub implementation for PR1. Full JavaScript bridge will be + * implemented in PR3. For now, this method logs a warning and resolves. + * + * JS fetchers are ONLY used for foreground/manual refresh. Background + * workers must use native fetcher. + * + * @param call Plugin call (will be implemented in PR3) + */ + @PluginMethod + public void setJsContentFetcher(PluginCall call) { + try { + Log.w(TAG, "SPI: setJsContentFetcher called but not yet implemented (PR3)"); + Log.w(TAG, "SPI: JS fetcher will only be used for foreground operations"); + + // For PR1, just resolve - full implementation in PR3 + JSObject result = new JSObject(); + result.put("warning", "JS fetcher support not yet implemented (coming in PR3)"); + result.put("note", "Background workers use native fetcher only"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error in setJsContentFetcher stub", e); + call.reject("JS fetcher registration failed: " + e.getMessage()); + } + } + + /** + * Get current SPI configuration status (helper for debugging) + * + * @return Current SPI state + */ + public SchedulingPolicy getSchedulingPolicy() { + return schedulingPolicy; + } + + /** + * Check if native fetcher is enabled and registered + * + * @return True if native fetcher is enabled and registered + */ + public boolean isNativeFetcherAvailable() { + return nativeFetcherEnabled && nativeFetcher != null; + } + }