tauri-plugin-background-service 1.0.1

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

import android.content.Context

data class DurableState(
    val desiredRunning: Boolean = false,
    val lastServiceLabel: String = "",
    val lastServiceType: String = "remoteMessaging",
    val lastStartEpochMs: Long = 0L,
    val lastHeartbeatEpochMs: Long = 0L,
    val lastNativeState: String = "unknown",
    val lastPlatformError: String? = null,
    val restartAttempt: Int = 0,
    val recoveryPending: Boolean = false,
    val recoveryReason: String? = null,
    // BGS-21 (doc-08 Step 12): discriminator for notification-permission status.
    // Android `shouldShowRequestPermissionRationale` is FALSE for both never-asked
    // and permanently-denied, so the status mapping cannot distinguish them without
    // a persisted "have we ever asked" flag. Default false (fresh install = never
    // asked). MUST be wired in BOTH load() and save() or it compiles yet never
    // round-trips across reboot — a silent defect.
    val hasAskedNotificationPermission: Boolean = false,
) {
    companion object {
        private const val PREFS_NAME = "tauri_bg_service_state"

        private const val KEY_DESIRED_RUNNING = "desired_running"
        private const val KEY_LAST_SERVICE_LABEL = "last_service_label"
        private const val KEY_LAST_SERVICE_TYPE = "last_service_type"
        private const val KEY_LAST_START_EPOCH_MS = "last_start_epoch_ms"
        private const val KEY_LAST_HEARTBEAT_EPOCH_MS = "last_heartbeat_epoch_ms"
        private const val KEY_LAST_NATIVE_STATE = "last_native_state"
        private const val KEY_LAST_PLATFORM_ERROR = "last_platform_error"
        private const val KEY_RESTART_ATTEMPT = "restart_attempt"
        private const val KEY_RECOVERY_PENDING = "recovery_pending"
        private const val KEY_RECOVERY_REASON = "recovery_reason"
        private const val KEY_HAS_ASKED_NOTIFICATION = "has_asked_notification"

        fun load(context: Context): DurableState {
            val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            return DurableState(
                desiredRunning = prefs.getBoolean(KEY_DESIRED_RUNNING, false),
                lastServiceLabel = prefs.getString(KEY_LAST_SERVICE_LABEL, "") ?: "",
                lastServiceType = prefs.getString(KEY_LAST_SERVICE_TYPE, "remoteMessaging") ?: "remoteMessaging",
                lastStartEpochMs = prefs.getLong(KEY_LAST_START_EPOCH_MS, 0L),
                lastHeartbeatEpochMs = prefs.getLong(KEY_LAST_HEARTBEAT_EPOCH_MS, 0L),
                lastNativeState = prefs.getString(KEY_LAST_NATIVE_STATE, "unknown") ?: "unknown",
                lastPlatformError = prefs.getString(KEY_LAST_PLATFORM_ERROR, null),
                restartAttempt = prefs.getInt(KEY_RESTART_ATTEMPT, 0),
                recoveryPending = prefs.getBoolean(KEY_RECOVERY_PENDING, false),
                recoveryReason = prefs.getString(KEY_RECOVERY_REASON, null),
                hasAskedNotificationPermission = prefs.getBoolean(KEY_HAS_ASKED_NOTIFICATION, false),
            )
        }

        fun save(context: Context, state: DurableState) {
            val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            // commit() (synchronous), not apply(): a foreground-start failure
            // persists lastPlatformError and then self-stops immediately, so the
            // write must flush before the process is torn down or the UI loses
            // the reason the service died (spec-compliance W1 / R-W1.3, NFR-1).
            prefs.edit()
                .putBoolean(KEY_DESIRED_RUNNING, state.desiredRunning)
                .putString(KEY_LAST_SERVICE_LABEL, state.lastServiceLabel)
                .putString(KEY_LAST_SERVICE_TYPE, state.lastServiceType)
                .putLong(KEY_LAST_START_EPOCH_MS, state.lastStartEpochMs)
                .putLong(KEY_LAST_HEARTBEAT_EPOCH_MS, state.lastHeartbeatEpochMs)
                .putString(KEY_LAST_NATIVE_STATE, state.lastNativeState)
                .putString(KEY_LAST_PLATFORM_ERROR, state.lastPlatformError)
                .putInt(KEY_RESTART_ATTEMPT, state.restartAttempt)
                .putBoolean(KEY_RECOVERY_PENDING, state.recoveryPending)
                .putString(KEY_RECOVERY_REASON, state.recoveryReason)
                .putBoolean(KEY_HAS_ASKED_NOTIFICATION, state.hasAskedNotificationPermission)
                .commit()
        }

        fun clear(context: Context) {
            val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            prefs.edit().clear().apply()
        }
    }
}