tauri-plugin-background-service 0.5.3

Background service lifecycle plugin for Tauri v2 — run long-lived tasks on Android, iOS, and desktop
Documentation
package app.tauri.backgroundservice

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import org.json.JSONArray

@InvokeArg class StartKeepaliveArgs {
    var label: String = "Service running"
    var foregroundServiceType: String = "dataSync"
}

@InvokeArg
class GetAutoStartConfigResult {
    var pending: Boolean = false
    var label: String? = null
    var serviceType: String? = null
}

@TauriPlugin
class BackgroundServicePlugin(private val activity: Activity) : Plugin(activity) {

    private var allowedFgsTypes: List<String> = listOf("dataSync")
    private var validateFgsType: Boolean = true
    private var onTimeoutPolicy: String = "notifyUser"
    private var notificationChannelId: String = "bg_service"
    private var notificationChannelName: String = "Background Service"
    private var notificationId: Int = 9001
    private var notificationSmallIcon: String? = null
    private var showStopAction: Boolean = true

    private fun prefs() =
        activity.getSharedPreferences("bg_service", Context.MODE_PRIVATE)

    override fun load(webView: android.webkit.WebView) {
        super.load(webView)
        loadConfig()
        // Request POST_NOTIFICATIONS once so Rust's Notifier can fire freely
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
            activity.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS)
            != android.content.pm.PackageManager.PERMISSION_GRANTED
        ) {
            activity.requestPermissions(
                arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), 1001)
        }

        // Register timeout callback so LifecycleService can emit events to JS.
        onTimeoutEvent = { errorMessage ->
            val data = JSObject()
            data.put("type", "stopped")
            data.put("reason", "timeout")
            data.put("platformError", errorMessage)
            trigger("timeout", data)
        }
    }

    override fun onDestroy() {
        onTimeoutEvent = null
        super.onDestroy()
    }

    private fun loadConfig() {
        val configJson = handle?.config ?: return
        val json = try { org.json.JSONObject(configJson) } catch (_: Exception) { return }
        val typesArray = json.optJSONArray("androidForegroundServiceTypes")
        if (typesArray != null) {
            allowedFgsTypes = (0 until typesArray.length()).map { typesArray.getString(it) }
        }
        validateFgsType = json.optBoolean("androidValidateForegroundServiceType", true)
        onTimeoutPolicy = json.optString("androidOnTimeout", "notifyUser")
        notificationChannelId = json.optString("androidNotificationChannelId", "bg_service")
        notificationChannelName = json.optString("androidNotificationChannelName", "Background Service")
        notificationId = json.optInt("androidNotificationId", 9001)
        notificationSmallIcon = json.optString("androidNotificationSmallIcon").ifEmpty { null }
        showStopAction = json.optBoolean("androidShowStopAction", true)
    }

    @Command
    fun startKeepalive(invoke: Invoke) {
        val args = invoke.parseArgs(StartKeepaliveArgs::class.java)

        val validationError = validateForegroundServiceType(
            args.foregroundServiceType, allowedFgsTypes, validateFgsType
        )
        if (validationError != null) {
            invoke.reject(validationError)
            return
        }

        val intent = Intent(activity, LifecycleService::class.java).apply {
            action = LifecycleService.ACTION_START
            putExtra(LifecycleService.EXTRA_LABEL, args.label)
            putExtra(LifecycleService.EXTRA_SERVICE_TYPE, args.foregroundServiceType)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            activity.startForegroundService(intent)
        else
            activity.startService(intent)
        prefs().edit()
            .putString("bg_service_label", args.label)
            .putString("bg_service_type", args.foregroundServiceType)
            .putString("bg_notif_channel_id", notificationChannelId)
            .putString("bg_notif_channel_name", notificationChannelName)
            .putInt("bg_notif_id", notificationId)
            .putString("bg_notif_small_icon", notificationSmallIcon)
            .putBoolean("bg_show_stop_action", showStopAction)
            .putString("bg_on_timeout_policy", onTimeoutPolicy)
            .apply()
        invoke.resolve()
    }

    @Command
    fun stopKeepalive(invoke: Invoke) {
        prefs().edit()
            .remove("bg_service_label")
            .remove("bg_service_type")
            .remove("bg_auto_start_pending")
            .remove("bg_auto_start_label")
            .remove("bg_auto_start_type")
            .remove("bg_notif_channel_id")
            .remove("bg_notif_channel_name")
            .remove("bg_notif_id")
            .remove("bg_notif_small_icon")
            .remove("bg_show_stop_action")
            .remove("bg_on_timeout_policy")
            .apply()
        DurableState.clear(activity)
        activity.startService(Intent(activity, LifecycleService::class.java)
            .apply { action = LifecycleService.ACTION_STOP })
        invoke.resolve()
    }

    @Command
    fun getAutoStartConfig(invoke: Invoke) {
        val p = prefs()
        val result = GetAutoStartConfigResult()
        result.pending = p.getBoolean("bg_auto_start_pending", false)
        result.label = p.getString("bg_auto_start_label", null)
        result.serviceType = p.getString("bg_auto_start_type", null)
        invoke.resolveObject(result)
    }

    @Command
    fun clearAutoStartConfig(invoke: Invoke) {
        prefs().edit()
            .remove("bg_auto_start_pending")
            .remove("bg_auto_start_label")
            .remove("bg_auto_start_type")
            .apply()
        invoke.resolve()
    }

    @Command
    fun moveTaskToBackground(invoke: Invoke) {
        activity.moveTaskToBack(true)
        invoke.resolve()
    }

    companion object {
        @Volatile
        internal var onTimeoutEvent: ((String) -> Unit)? = null

        fun validateForegroundServiceType(
            requestedType: String,
            allowedTypes: List<String>,
            validate: Boolean
        ): String? {
            if (!validate) return null
            if (allowedTypes.contains(requestedType)) return null
            return "foreground service type '$requestedType' is not in the configured allowlist $allowedTypes. " +
                "Add it to androidForegroundServiceTypes in your plugin config."
        }
    }
}