/** * TimeSafari Native Fetcher Implementation (Android/Kotlin) * * Example implementation of NativeNotificationContentFetcher for Android. * This runs in background workers and does NOT require JavaScript bridge. * * @author Matthew Raymer * @version 1.0.0 */ package com.timesafari.notification import com.timesafari.dailynotification.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext /** * TimeSafari native content fetcher * * Implements the NativeNotificationContentFetcher SPI to provide * TimeSafari-specific notification content fetching for background workers. */ class TimeSafariNativeFetcher( private val api: TimeSafariApi, private val tokenProvider: TokenProvider, private val starredPlanIds: List ) : NativeNotificationContentFetcher { override suspend fun fetchContent(context: FetchContext): List { return withContext(Dispatchers.IO) { try { // 1. Get fresh authentication token val jwt = tokenProvider.freshToken() // 2. Fetch TimeSafari data in parallel val offersToPerson = async { api.fetchOffersToPerson(jwt) } val offersToPlans = async { api.fetchOffersToPlans(jwt) } val projectUpdates = async { if (starredPlanIds.isNotEmpty()) { api.fetchProjectsLastUpdated(starredPlanIds, jwt) } else { null } } // Wait for all requests val offersPersonResult = offersToPerson.await() val offersPlansResult = offersToPlans.await() val updatesResult = projectUpdates.await() // 3. Convert to NotificationContent list buildList { // Add offers to person offersPersonResult?.data?.forEach { offer -> add( NotificationContent( id = "offer_person_${offer.id}", title = "New Offer: ${offer.title}", body = offer.description ?: "", scheduledTime = context.scheduledTime ?: (System.currentTimeMillis() + 3_600_000), fetchTime = context.fetchTime, mediaUrl = offer.imageUrl, ttlSeconds = 86_400, // 24 hours dedupeKey = "offer_person_${offer.id}_${offer.updatedAt}", priority = "default", metadata = mapOf( "offerId" to offer.id, "issuerDid" to offer.issuerDid, "source" to "offers_to_person" ) ) ) } // Add offers to plans offersPlansResult?.data?.forEach { offer -> add( NotificationContent( id = "offer_plan_${offer.id}", title = "Offer for Your Project: ${offer.projectName}", body = offer.description ?: "", scheduledTime = context.scheduledTime ?: (System.currentTimeMillis() + 3_600_000), fetchTime = context.fetchTime, mediaUrl = offer.imageUrl, ttlSeconds = 86_400, dedupeKey = "offer_plan_${offer.id}_${offer.updatedAt}", priority = "default", metadata = mapOf( "offerId" to offer.id, "planHandleId" to offer.planHandleId, "source" to "offers_to_plans" ) ) ) } // Add project updates updatesResult?.data?.forEach { update -> add( NotificationContent( id = "project_${update.planSummary.jwtId}", title = "${update.planSummary.name} Updated", body = "New updates for ${update.planSummary.name}", scheduledTime = context.scheduledTime ?: (System.currentTimeMillis() + 3_600_000), fetchTime = context.fetchTime, mediaUrl = update.planSummary.image, ttlSeconds = 86_400, dedupeKey = "project_${update.planSummary.handleId}_${update.planSummary.jwtId}", priority = "default", metadata = mapOf( "planHandleId" to update.planSummary.handleId, "jwtId" to update.planSummary.jwtId, "source" to "project_updates" ) ) ) } } } catch (e: Exception) { // Log error and return empty list // Plugin will handle retry based on SchedulingPolicy android.util.Log.e("TimeSafariFetcher", "Fetch failed", e) emptyList() } } } } /** * Registration in Application.onCreate() */ class TimeSafariApplication : Application() { override fun onCreate() { super.onCreate() // Register native fetcher val api = TimeSafariApiClient(context = this) val tokenProvider = JWTokenProvider(activeDid = getActiveDid()) val starredPlanIds = getStarredPlanIds() val fetcher = TimeSafariNativeFetcher( api = api, tokenProvider = tokenProvider, starredPlanIds = starredPlanIds ) DailyNotification.setNativeFetcher(fetcher) } }