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.
This commit is contained in:
@@ -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);
|
||||
Log.d(TAG, "PR2: 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
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user