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
import java.io.File

/**
 * Rust/Kotlin-readable UI-locale store — Kotlin read half (doc-08 / BGS-19 Step 16 T2).
 *
 * The UI locale is webview-only (`localStorage["lang"]`); the Rust `set_locale`
 * command mirrors it to `{app_data}/data/locale.json` (`{"locale":"ar"}`). This
 * object loads that SAME file so the Android notification composition path (which
 * runs without the webview — message action labels, boot-recovery text) honors the
 * user's chosen locale. Mirrors the Rust `locale_store::LocaleRecord::load`.
 *
 * The data dir matches `HeadlessBridge.dataDir`
 * (`applicationInfo.dataDir` + `"data"`), which is the same path Rust's
 * `BaseDirectory::AppData`/`"data"` resolves to on Android (one app, one UID, one
 * data dir) — so the Kotlin read sees exactly what the Rust write persists. A
 * missing/corrupt file or an unrecognized code falls back to `"en"`; headless
 * composition never fails on a bad store (same contract as the Rust side).
 *
 * Parsing intentionally AVOIDS `org.json`: the file shape is fixed
 * (`{"locale": <code>}`) and a substring scan keeps this object pure-JVM-testable
 * — the android.jar `org.json` stub nulls out under non-Robolectric unit tests
 * (`testOptions.unitTests.isReturnDefaultValues = true`), so a `JSONObject` parse
 * here would NPE in the pure test path (memory
 * `android-pure-jvm-test-orgjson-stub-npe-isreturndefaultvalues`).
 *
 * CROSS-DOC: doc 06 owns notification *rendering*; this is the i18n
 * composition/locale-plumbing seam (Kotlin half).
 */
object LocaleStore {
    private const val FILE_NAME = "locale.json"

    /** Supported locale codes persisted to/loaded from the store. */
    const val EN = "en"
    const val AR = "ar"
    const val FR = "fr"

    /** Load the persisted locale code for [context]'s data dir (default "en"). */
    fun load(context: Context): String = loadFromDir(dataDir(context))

    /**
     * Load the locale code from an explicit data dir (testable without a
     * `Context`). Missing file / parse error / unrecognized code ⇒ "en".
     */
    fun loadFromDir(dataDir: File): String {
        val text = runCatching { File(dataDir, FILE_NAME).readText() }.getOrNull()
            ?: return EN
        return normalize(extractLocaleCode(text))
    }

    /** Map a raw code to one of the supported codes (unrecognized ⇒ "en"). */
    fun normalize(code: String): String = when (code.trim().lowercase()) {
        AR -> AR
        FR -> FR
        else -> EN
    }

    private fun dataDir(context: Context): File {
        val base = context.applicationInfo?.dataDir
        return if (!base.isNullOrEmpty()) File(base, "data") else File(context.filesDir, "data")
    }

    /**
     * Extract the `locale` value from a `{"locale":"ar"}` payload WITHOUT
     * `org.json` (pure-JVM-safe). Tolerates the Rust `serde_json::to_string_pretty`
     * shape (`{\n  "locale": "ar"\n}`) and minor spacing variance.
     */
    private fun extractLocaleCode(text: String): String {
        val key = "\"locale\""
        val keyIdx = text.indexOf(key)
        if (keyIdx < 0) return ""
        val colonIdx = text.indexOf(':', keyIdx + key.length)
        if (colonIdx < 0) return ""
        val openQuote = text.indexOf('"', colonIdx + 1)
        if (openQuote < 0) return ""
        val closeQuote = text.indexOf('"', openQuote + 1)
        if (closeQuote < 0) return ""
        return text.substring(openQuote + 1, closeQuote)
    }
}