Browse Source

feat(worker): wire native fetcher SPI in background fetch worker

PR2: Background Workers implementation
- Update DailyNotificationFetchWorker to use NativeNotificationContentFetcher SPI
- Remove TimeSafari coordination checks from worker (moved to host app)
- Add fetchContentWithTimeout() method that calls native fetcher via SPI
- Add fallback to legacy fetcher if no native fetcher is registered
- Update handleSuccessfulFetch() to process List<NotificationContent>
- Simplify retry logic to use SchedulingPolicy for exponential backoff
- Remove all TimeSafari-specific coordination methods from worker
- Add static getter in DailyNotificationPlugin for worker access to native fetcher

This completes the worker-side implementation of the dual-path SPI,
allowing background workers to reliably fetch content using native code.
master
Matthew Raymer 2 days ago
parent
commit
88ce1a8b9a
  1. 438
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java
  2. 211
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java

438
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<NotificationContent> 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<NotificationContent> 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<String, Object> 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<List<NotificationContent>> future = nativeFetcher.fetchContent(context);
List<NotificationContent> 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<NotificationContent>)
*
* @param contents List of successfully fetched notification contents
*/
private void handleSuccessfulFetch(List<NotificationContent> 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);
}
}
}

211
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;
}
}

Loading…
Cancel
Save