Browse Source
- Add NativeNotificationContentFetcher interface for host app implementations - Add FetchContext class to pass fetch parameters (trigger, scheduledTime, fetchTime) - Add SchedulingPolicy class for retry backoff configuration - Add TypeScript definitions for content fetcher SPI in src/definitions.ts - Export SPI types from src/index.ts This enables host apps to provide their own content fetching implementation for background workers, following the Integration Point Refactor (PR2).master
5 changed files with 458 additions and 0 deletions
@ -0,0 +1,130 @@ |
|||||
|
/** |
||||
|
* FetchContext.java |
||||
|
* |
||||
|
* Context information provided to content fetchers about why a fetch was triggered. |
||||
|
* |
||||
|
* This class is part of the Integration Point Refactor (PR1) SPI implementation. |
||||
|
* It provides fetchers with metadata about the fetch request, including trigger |
||||
|
* type, scheduling information, and optional metadata. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
package com.timesafari.dailynotification; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import java.util.Collections; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* Context provided to content fetchers about why fetch was triggered |
||||
|
* |
||||
|
* This follows the TypeScript interface from src/types/content-fetcher.ts and |
||||
|
* ensures type safety between JS and native fetcher implementations. |
||||
|
*/ |
||||
|
public class FetchContext { |
||||
|
|
||||
|
/** |
||||
|
* Reason why the fetch was triggered |
||||
|
* |
||||
|
* Valid values: "background_work", "prefetch", "manual", "scheduled" |
||||
|
*/ |
||||
|
@NonNull |
||||
|
public final String trigger; |
||||
|
|
||||
|
/** |
||||
|
* When notification is scheduled for (optional, epoch milliseconds) |
||||
|
* |
||||
|
* Only present when trigger is "prefetch" or "scheduled" |
||||
|
*/ |
||||
|
@Nullable |
||||
|
public final Long scheduledTime; |
||||
|
|
||||
|
/** |
||||
|
* When the fetch was triggered (required, epoch milliseconds) |
||||
|
*/ |
||||
|
public final long fetchTime; |
||||
|
|
||||
|
/** |
||||
|
* Additional context metadata (optional) |
||||
|
* |
||||
|
* Plugin may populate with app state, network info, etc. |
||||
|
* Fetcher can use for logging, debugging, or conditional logic. |
||||
|
*/ |
||||
|
@NonNull |
||||
|
public final Map<String, Object> metadata; |
||||
|
|
||||
|
/** |
||||
|
* Constructor with all fields |
||||
|
* |
||||
|
* @param trigger Trigger type (required) |
||||
|
* @param scheduledTime Scheduled time (optional) |
||||
|
* @param fetchTime When fetch triggered (required) |
||||
|
* @param metadata Additional metadata (optional, can be null) |
||||
|
*/ |
||||
|
public FetchContext( |
||||
|
@NonNull String trigger, |
||||
|
@Nullable Long scheduledTime, |
||||
|
long fetchTime, |
||||
|
@Nullable Map<String, Object> metadata) { |
||||
|
if (trigger == null || trigger.isEmpty()) { |
||||
|
throw new IllegalArgumentException("trigger is required"); |
||||
|
} |
||||
|
|
||||
|
this.trigger = trigger; |
||||
|
this.scheduledTime = scheduledTime; |
||||
|
this.fetchTime = fetchTime; |
||||
|
this.metadata = metadata != null ? |
||||
|
Collections.unmodifiableMap(new HashMap<>(metadata)) : |
||||
|
Collections.emptyMap(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Constructor with minimal fields (no metadata) |
||||
|
* |
||||
|
* @param trigger Trigger type |
||||
|
* @param scheduledTime Scheduled time (can be null) |
||||
|
* @param fetchTime When fetch triggered |
||||
|
*/ |
||||
|
public FetchContext( |
||||
|
@NonNull String trigger, |
||||
|
@Nullable Long scheduledTime, |
||||
|
long fetchTime) { |
||||
|
this(trigger, scheduledTime, fetchTime, null); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get metadata value by key |
||||
|
* |
||||
|
* @param key Metadata key |
||||
|
* @return Value or null if not present |
||||
|
*/ |
||||
|
@Nullable |
||||
|
public Object getMetadata(@NonNull String key) { |
||||
|
return metadata.get(key); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if metadata contains key |
||||
|
* |
||||
|
* @param key Metadata key |
||||
|
* @return True if key exists |
||||
|
*/ |
||||
|
public boolean hasMetadata(@NonNull String key) { |
||||
|
return metadata.containsKey(key); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "FetchContext{" + |
||||
|
"trigger='" + trigger + '\'' + |
||||
|
", scheduledTime=" + scheduledTime + |
||||
|
", fetchTime=" + fetchTime + |
||||
|
", metadataSize=" + metadata.size() + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,86 @@ |
|||||
|
/** |
||||
|
* NativeNotificationContentFetcher.java |
||||
|
* |
||||
|
* Service Provider Interface (SPI) for native content fetchers. |
||||
|
* |
||||
|
* This interface is part of the Integration Point Refactor (PR1) that allows |
||||
|
* host apps to provide their own content fetching logic without hardcoding |
||||
|
* TimeSafari-specific code in the plugin. |
||||
|
* |
||||
|
* Host apps implement this interface in native code (Kotlin/Java) and register |
||||
|
* it with the plugin. The plugin calls this interface from background workers |
||||
|
* (WorkManager) to fetch notification content. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
package com.timesafari.dailynotification; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import java.util.List; |
||||
|
import java.util.concurrent.CompletableFuture; |
||||
|
|
||||
|
/** |
||||
|
* Native content fetcher interface for host app implementations |
||||
|
* |
||||
|
* This interface enables the plugin to call host app's native code for |
||||
|
* fetching notification content. This is the ONLY path used by background |
||||
|
* workers, as JavaScript bridges are unreliable in background contexts. |
||||
|
* |
||||
|
* Implementation Requirements: |
||||
|
* - Must be thread-safe (may be called from WorkManager background threads) |
||||
|
* - Must complete within reasonable time (plugin enforces timeout) |
||||
|
* - Should return empty list on failure rather than throwing exceptions |
||||
|
* - Should handle errors gracefully and log for debugging |
||||
|
* |
||||
|
* Example Implementation: |
||||
|
* <pre> |
||||
|
* class TimeSafariNativeFetcher implements NativeNotificationContentFetcher { |
||||
|
* private final TimeSafariApi api; |
||||
|
* private final TokenProvider tokenProvider; |
||||
|
* |
||||
|
* @Override |
||||
|
* public CompletableFuture<List<NotificationContent>> fetchContent( |
||||
|
* FetchContext context) { |
||||
|
* return CompletableFuture.supplyAsync(() -> { |
||||
|
* try { |
||||
|
* String jwt = tokenProvider.freshToken(); |
||||
|
* // Fetch from TimeSafari API
|
||||
|
* // Convert to NotificationContent[]
|
||||
|
* return notificationContents; |
||||
|
* } catch (Exception e) { |
||||
|
* Log.e("Fetcher", "Fetch failed", e); |
||||
|
* return Collections.emptyList(); |
||||
|
* } |
||||
|
* }); |
||||
|
* } |
||||
|
* } |
||||
|
* </pre> |
||||
|
*/ |
||||
|
public interface NativeNotificationContentFetcher { |
||||
|
|
||||
|
/** |
||||
|
* Fetch notification content from external source |
||||
|
* |
||||
|
* This method is called by the plugin when: |
||||
|
* - Background fetch work is triggered (WorkManager) |
||||
|
* - Prefetch is scheduled before notification time |
||||
|
* - Manual refresh is requested (if native fetcher enabled) |
||||
|
* |
||||
|
* The plugin will: |
||||
|
* - Enforce a timeout (default 30 seconds, configurable via SchedulingPolicy) |
||||
|
* - Handle empty lists gracefully (no notifications scheduled) |
||||
|
* - Log errors for debugging |
||||
|
* - Retry on failure based on SchedulingPolicy |
||||
|
* |
||||
|
* @param context Context about why fetch was triggered, including |
||||
|
* trigger type, scheduled time, and optional metadata |
||||
|
* @return CompletableFuture that resolves to list of NotificationContent. |
||||
|
* Empty list indicates no content available (not an error). |
||||
|
* The future should complete exceptionally only on unrecoverable errors. |
||||
|
*/ |
||||
|
@NonNull |
||||
|
CompletableFuture<List<NotificationContent>> fetchContent(@NonNull FetchContext context); |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,198 @@ |
|||||
|
/** |
||||
|
* SchedulingPolicy.java |
||||
|
* |
||||
|
* Policy configuration for notification scheduling, fetching, and retry behavior. |
||||
|
* |
||||
|
* This class is part of the Integration Point Refactor (PR1) SPI implementation. |
||||
|
* It allows host apps to configure scheduling behavior including retry backoff, |
||||
|
* prefetch timing, deduplication windows, and cache TTL. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
package com.timesafari.dailynotification; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
|
||||
|
/** |
||||
|
* Scheduling policy configuration |
||||
|
* |
||||
|
* Controls how the plugin schedules fetches, handles retries, manages |
||||
|
* deduplication, and enforces TTL policies. |
||||
|
* |
||||
|
* This follows the TypeScript interface from src/types/content-fetcher.ts |
||||
|
* and ensures consistency between JS and native configuration. |
||||
|
*/ |
||||
|
public class SchedulingPolicy { |
||||
|
|
||||
|
/** |
||||
|
* How early to prefetch before scheduled notification time (milliseconds) |
||||
|
* |
||||
|
* Example: If set to 300000 (5 minutes), and notification is scheduled for |
||||
|
* 8:00 AM, the fetch will be triggered at 7:55 AM. |
||||
|
* |
||||
|
* Default: 5 minutes (300000ms) |
||||
|
*/ |
||||
|
@Nullable |
||||
|
public Long prefetchWindowMs; |
||||
|
|
||||
|
/** |
||||
|
* Retry backoff configuration (required) |
||||
|
* |
||||
|
* Controls exponential backoff behavior for failed fetches |
||||
|
*/ |
||||
|
@NonNull |
||||
|
public RetryBackoff retryBackoff; |
||||
|
|
||||
|
/** |
||||
|
* Maximum items to fetch per batch |
||||
|
* |
||||
|
* Limits the number of NotificationContent items that can be fetched |
||||
|
* in a single operation. Helps prevent oversized responses. |
||||
|
* |
||||
|
* Default: 50 |
||||
|
*/ |
||||
|
@Nullable |
||||
|
public Integer maxBatchSize; |
||||
|
|
||||
|
/** |
||||
|
* Deduplication window (milliseconds) |
||||
|
* |
||||
|
* Prevents duplicate notifications within this time window. Plugin |
||||
|
* uses dedupeKey (or id) to detect duplicates. |
||||
|
* |
||||
|
* Default: 24 hours (86400000ms) |
||||
|
*/ |
||||
|
@Nullable |
||||
|
public Long dedupeHorizonMs; |
||||
|
|
||||
|
/** |
||||
|
* Default cache TTL if item doesn't specify ttlSeconds (seconds) |
||||
|
* |
||||
|
* Used when NotificationContent doesn't have ttlSeconds set. |
||||
|
* Determines how long cached content remains valid. |
||||
|
* |
||||
|
* Default: 6 hours (21600 seconds) |
||||
|
*/ |
||||
|
@Nullable |
||||
|
public Integer cacheTtlSeconds; |
||||
|
|
||||
|
/** |
||||
|
* Whether exact alarms are allowed (Android 12+) |
||||
|
* |
||||
|
* Controls whether plugin should attempt to use exact alarms. |
||||
|
* Requires SCHEDULE_EXACT_ALARM permission on Android 12+. |
||||
|
* |
||||
|
* Default: false (use inexact alarms) |
||||
|
*/ |
||||
|
@Nullable |
||||
|
public Boolean exactAlarmsAllowed; |
||||
|
|
||||
|
/** |
||||
|
* Fetch timeout in milliseconds |
||||
|
* |
||||
|
* Maximum time to wait for native fetcher to complete. |
||||
|
* Plugin enforces this timeout when calling fetchContent(). |
||||
|
* |
||||
|
* Default: 30 seconds (30000ms) |
||||
|
*/ |
||||
|
@Nullable |
||||
|
public Long fetchTimeoutMs; |
||||
|
|
||||
|
/** |
||||
|
* Default constructor with required field |
||||
|
* |
||||
|
* @param retryBackoff Retry backoff configuration (required) |
||||
|
*/ |
||||
|
public SchedulingPolicy(@NonNull RetryBackoff retryBackoff) { |
||||
|
this.retryBackoff = retryBackoff; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Retry backoff configuration |
||||
|
* |
||||
|
* Controls exponential backoff behavior for retryable failures. |
||||
|
* Delay = min(max(minMs, lastDelay * factor), maxMs) * (1 + jitterPct/100 * random) |
||||
|
*/ |
||||
|
public static class RetryBackoff { |
||||
|
|
||||
|
/** |
||||
|
* Minimum delay between retries (milliseconds) |
||||
|
* |
||||
|
* First retry will wait at least this long. |
||||
|
* Default: 2000ms (2 seconds) |
||||
|
*/ |
||||
|
public long minMs; |
||||
|
|
||||
|
/** |
||||
|
* Maximum delay between retries (milliseconds) |
||||
|
* |
||||
|
* Retry delay will never exceed this value. |
||||
|
* Default: 600000ms (10 minutes) |
||||
|
*/ |
||||
|
public long maxMs; |
||||
|
|
||||
|
/** |
||||
|
* Exponential backoff multiplier |
||||
|
* |
||||
|
* Each retry multiplies previous delay by this factor. |
||||
|
* Example: factor=2 means delays: 2s, 4s, 8s, 16s, ... |
||||
|
* Default: 2.0 |
||||
|
*/ |
||||
|
public double factor; |
||||
|
|
||||
|
/** |
||||
|
* Jitter percentage (0-100) |
||||
|
* |
||||
|
* Adds randomness to prevent thundering herd. |
||||
|
* Final delay = calculatedDelay * (1 + jitterPct/100 * random(0-1)) |
||||
|
* Default: 20 (20% jitter) |
||||
|
*/ |
||||
|
public int jitterPct; |
||||
|
|
||||
|
/** |
||||
|
* Default constructor with sensible defaults |
||||
|
*/ |
||||
|
public RetryBackoff() { |
||||
|
this.minMs = 2000; |
||||
|
this.maxMs = 600000; |
||||
|
this.factor = 2.0; |
||||
|
this.jitterPct = 20; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Constructor with all parameters |
||||
|
* |
||||
|
* @param minMs Minimum delay (ms) |
||||
|
* @param maxMs Maximum delay (ms) |
||||
|
* @param factor Exponential multiplier |
||||
|
* @param jitterPct Jitter percentage (0-100) |
||||
|
*/ |
||||
|
public RetryBackoff(long minMs, long maxMs, double factor, int jitterPct) { |
||||
|
this.minMs = minMs; |
||||
|
this.maxMs = maxMs; |
||||
|
this.factor = factor; |
||||
|
this.jitterPct = jitterPct; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create default policy with sensible defaults |
||||
|
* |
||||
|
* @return Default SchedulingPolicy instance |
||||
|
*/ |
||||
|
@NonNull |
||||
|
public static SchedulingPolicy createDefault() { |
||||
|
SchedulingPolicy policy = new SchedulingPolicy(new RetryBackoff()); |
||||
|
policy.prefetchWindowMs = 300000L; // 5 minutes
|
||||
|
policy.maxBatchSize = 50; |
||||
|
policy.dedupeHorizonMs = 86400000L; // 24 hours
|
||||
|
policy.cacheTtlSeconds = 21600; // 6 hours
|
||||
|
policy.exactAlarmsAllowed = false; |
||||
|
policy.fetchTimeoutMs = 30000L; // 30 seconds
|
||||
|
return policy; |
||||
|
} |
||||
|
} |
||||
|
|
||||
Loading…
Reference in new issue