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