Skip to content

Releases: brewkits/kmpworkmanager

v3.1.0 — Constraints.maxRetries + security fixes

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 15 Jul 19:28

Highlights

Constraints.maxRetries — a real, cross-platform retry ceiling. maxRetries = N caps a failing task at N + 1 total runs (1 initial + N retries), then marks it a permanent failure. It bounds both WorkerResult.Failure(shouldRetry = true) and a WorkerResult.Retry without an explicit attemptCap (a per-result attemptCap still wins). One-time and chained tasks only — periodic tasks ignore it. Default -1 keeps each platform's prior behavior.

  • Android: WorkManager has no native max-retry API, so shouldRetry = true previously retried until the OS quota ran out. It is now enforced inside the worker.
  • iOS: honored by both the single-task dispatcher and the chain executor. This also fixes a chain-retry bug — a chain that failed with retries remaining was dropped from the queue and never retried; it is now correctly re-enqueued.

Fixed

  • Resolved the outstanding CodeQL security alerts: demo-app allowBackup, a deprecated NetworkInfo API on an unreachable pre-API-23 branch, and an ignored File return status.

Additive API — no breaking changes. See the migration guide and the constraints guide.


Maven Central

implementation("dev.brewkits:kmpworkmanager:3.1.0")
implementation("dev.brewkits:kmpworkmanager-http:3.1.0") // only if you use the HTTP workers

v3.0.1

Choose a tag to compare

@github-actions github-actions released this 20 Jun 17:32

Patch release. Headline: fixes a crash on Android 8–11 (API 26–30) for immediate background tasks.

Fixed

  • Expedited tasks crashed on API < 31 with IllegalStateException: Not implemented.
    KmpWorker lost its getForegroundInfo() override during the v2.3.8 BaseKmpWorker
    refactor. The scheduler marks immediate, non-heavy tasks as expedited work, which runs
    as a foreground service on API < 31 — WorkManager then calls getForegroundInfo(), and
    the default CoroutineWorker implementation throws, crashing the task at runtime.
    The fallback foreground-info override is restored.
    • Affected: v2.3.8 → v3.0.0 on API 26–30. Upgrade to 3.0.1. (API 31+ was never affected.)

Build / CI (maintenance — no consumer impact)

  • Upgraded the build toolchain to Gradle 9.0.0. Published Gradle Module Metadata is
    unchanged (formatVersion 1.1), so consumers need no Gradle upgrade.
  • Re-enabled the Android instrumented test matrix (moved to Ubuntu + KVM after GitHub's
    macOS runners migrated to Apple Silicon).
  • Added a CI-level Robolectric regression guard so the API < 31 getForegroundInfo() crash
    can never slip through CI again.

Compatibility

  • No public API changes — drop-in upgrade from 3.0.0.
implementation("dev.brewkits:kmpworkmanager:3.0.1")          // core engine (no Ktor)
implementation("dev.brewkits:kmpworkmanager-http:3.0.1")     // optional — Ktor 3 HTTP workers

Full changelog: https://github.com/brewkits/kmpworkmanager/blob/main/CHANGELOG.md

v3.0.0 — Ktor 3 + HTTP workers extracted

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 14 Jun 19:54
d80fa33

First major release: Ktor 3 support (#33) and a module split that removes Ktor from the core engine.

⚠️ Breaking changes

1. Requires Ktor 3 (for the HTTP workers)

Ktor 2.3.123.1.1. Ktor 2 and Ktor 3 share the same Maven coordinates and are binary-incompatible — an app classpath can hold only one. If you're still on Ktor 2, pin dev.brewkits:kmpworkmanager:2.5.1 until you migrate.

2. HTTP workers moved to a new artifact

The core kmpworkmanager no longer depends on Ktor at all. The six Ktor-based workers (HttpRequest, HttpSync, HttpDownload, HttpUpload, ParallelHttpDownload, ParallelHttpUpload) + HttpClientProvider now live in dev.brewkits:kmpworkmanager-http.

implementation("dev.brewkits:kmpworkmanager:3.0.0")        // core engine (no Ktor)
implementation("dev.brewkits:kmpworkmanager-http:3.0.0")   // optional — Ktor 3 HTTP workers

Register the HTTP workers via HttpWorkerRegistry (e.g. in a CompositeWorkerFactory). Worker class names and config are unchanged — persisted task IDs and JSON keep working.

Fixed

  • HTTP download EOF handling — Ktor 3 returns -1 at end-of-stream (Ktor 2 returned 0); download loops now break correctly.
  • iOS network reachability — real NWPathMonitor implementation, replacing the hardcoded placeholder (#40).
  • iOS test flakiness — fixed a queue-counter data race (#38) and a maintenance-job leak that crashed test runs (#43).

Upgrade guide

docs/MIGRATION_V3.0.0.md

Known issues

  • iOS FileCompressionWorker has no real ZIP codec yet (#39) — fail-fast by default, opt-in uncompressed fallback.
  • CI Android-instrumented emulator boot is flaky (#45); 2 pre-existing AlarmManager tests fail on device (#46). Neither affects the core/HTTP/iOS suites.

Full changelog: CHANGELOG.md

v2.5.1 — Documentation Hotfix

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 20 May 04:42

What changed

This is a docs-only patch release. No API changes, no behavior changes — all existing code continues to work identically.

Fixed

Five documentation errors that caused copy-paste code from the guides to fail to compile (Issue #32):

Doc Problem Fix
README.md, docs/quickstart.md @KmpWorker annotation shown — does not exist Corrected to @Worker(name = "...") from kmpworker-annotations
docs/quickstart.md iOS Koin init called KoinIOSKt.doInitKoinIos() — function does not exist Corrected to KoinInitializerKt.doInitKoin(platformModule: IOSModuleKt.iosModule)
docs/platform-setup.md §6 Worker implementation showed pre-library CoroutineWorker pattern incompatible with current API Replaced with AndroidWorker + @Worker(name = ...) pattern
docs/examples.md WorkerResult.Failure(...) // Retry with backoffFailure does not retry by default Corrected to WorkerResult.Retry(reason = ...)
docs/quickstart.md, docs/platform-setup.md Android init used deprecated startKoin { kmpWorkerModule() } Replaced with KmpWorkManager.initialize(context, workerFactory)

Important note on @Worker(name = ...)

The name argument is required for production safety. It must match the workerClassName you pass to scheduler.enqueue(...). If you omit it, the KSP processor defaults to the class simple name and emits a warning — this breaks silently after ProGuard/R8 rename or class refactoring.

// androidMain
@Worker(name = "SyncWorker")   // ✅ stable key — survives rename/obfuscation
class SyncAndroidWorker : AndroidWorker { ... }

// iosMain
@Worker(name = "SyncWorker", bgTaskId = "com.example.sync-task")
class SyncIosWorker : IosWorker { ... }

Upgrade from v2.5.0

No code changes needed. This release only fixes documentation.

// libs.versions.toml
kmpworkmanager = "2.5.1"

Full migration guide: docs/MIGRATION_V2.5.0.md (unchanged from v2.5.0)

v2.5.0 — Production hardening release

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 18 May 06:03
c3c6054

Released: 2026-05-16
Type: Minor release (additive features + breaking iOS default for
FileCompressionWorker).
Migration guide: docs/MIGRATION_V2.5.0.md


Why this release

Driven by an architecture review of kmpworkmanager in production camera-app
contexts. Three categories of risk surfaced:

  1. Correctness gaps that the existing test suite did not catch — file
    compression silently copying, PendingIntent request-code collisions, an
    in-memory map that didn't survive iOS cold-launch, BroadcastReceiver
    coroutine leaks. v2.5 closes each with a regression test that pins the new
    contract.
  2. Feature parity with native_workmanager
    (the Flutter sibling)
    — parallel HTTP downloads, parallel uploads,
    checksum verification, duplicate-file policy. These were already production-
    proven in Flutter; v2.5 brings the KMP API to parity.
  3. Documenting the physical limits of iOS background execution.
    BGTaskScheduler is opportunistic; no library can change that. v2.5 ships
    IOS_BGTASK_LIMITS.md, ANDROID_FGS_GUIDE.md, and
    APPLE_APP_STORE_REVIEW_GUIDELINES.md so adopters can plan around the
    hard limits instead of discovering them in production.

Highlights

Parallel HTTP workers

// 4-chunk parallel download with SHA-256 verification + rename on collision
val cfg = ParallelHttpDownloadConfig(
    url = "https://cdn.example.com/raw/IMG_9999.dng",
    savePath = "$documents/raw/IMG_9999.dng",
    numChunks = 4,
    expectedChecksum = "e3b0c44…",
    checksumAlgorithm = ChecksumAlgorithm.SHA256,
    skipExisting = false,
)
scheduler.enqueueTask(
    TaskRequest(
        workerClassName = "ParallelHttpDownloadWorker",
        inputJson = json.encodeToString(cfg),
    ),
    id = "raw-${photo.id}",
)
  • 1..16 concurrent Range chunks (default 4), .partN files persisted for
    per-chunk resume.
  • Automatic sequential fallback when the server returns no Content-Length
    or no Accept-Ranges: bytes.
  • Per-chunk resume skips parts whose .partN matches the expected slice size
    exactly (ParallelHttpDownloadWorkerTest.parallel_resumesPreviousAttempt_whenPartFilesAreComplete).
// 3-concurrent parallel upload with per-file 5xx retry
val cfg = ParallelHttpUploadConfig(
    url = "https://api.example.com/v1/photos/batch",
    files = listOf(
        ParallelUploadFile(filePath = "$docs/photo-1.heic", fieldName = "files[]"),
        ParallelUploadFile(filePath = "$docs/photo-2.heic", fieldName = "files[]"),
        ParallelUploadFile(filePath = "$docs/photo-3.heic", fieldName = "files[]"),
    ),
    maxConcurrent = 3,
    maxRetries = 1,  // 5xx + network only; 4xx is never retried
)
  • Semaphore-bounded concurrency.
  • WorkerResult.Success.data.fileResults exposes per-file outcomes
    (status, attempts, bytesSent, error).

Checksum verification + duplicate policy on HttpDownloadWorker

val cfg = HttpDownloadConfig(
    url = "https://cdn.example.com/file.bin",
    savePath = "$documents/file.bin",
    expectedChecksum = "e3b0c44…",
    checksumAlgorithm = ChecksumAlgorithm.SHA256,
    onDuplicate = DuplicatePolicy.RENAME, // → file.bin, file_1.bin, file_2.bin, …
)
  • Algorithms: MD5, SHA1, SHA256, SHA512 (via Okio's HashingSource).
  • OVERWRITE (default, pre-v2.5 behaviour), SKIP (return Success without
    network — short-circuits before any I/O), RENAME (bounded at 10 000
    suffix probes).

iOS background URLSession download (experimental)

IosBackgroundDownloadWorker hands the transfer to nsurlsessiond so it
survives full app termination. The completion event is delivered when iOS
cold-launches the app.

Critical fix in this release: the manager now persists (sessionIdentifier, taskIdentifier) → (savePath, workerName) to disk via
BackgroundDownloadStateStore. Pre-v2.5 used an in-memory map that was wiped
by process death, orphaning every download that completed after app kill.

val cfg = IosBackgroundDownloadConfig(
    url = "https://cdn.example.com/raw/IMG.dng",
    savePath = "$documents/raw/IMG.dng",
    sessionIdentifier = "com.example.cameraapp.bg.raw-import",
    isDiscretionary = false,
    allowsCellularAccess = false,
)

See docs/IOS_BACKGROUND_URL_SESSION.md for the
AppDelegate wiring and the cold-launch survival timeline.

iOS chain retry honoring

WorkerResult.Retry(delayMs, attemptCap) is now respected by ChainExecutor:

  • delayMs is captured into ChainExecutor.requestedNextBgTaskDelayMs;
    Swift hosts read it to re-arm BGProcessingTaskRequest with
    earliestBeginDate = now + delayMs.
  • attemptCap is enforced via ChainProgress.stepRetryCounts — a step that
    exceeds the cap abandons the chain.
  • Per-step counter is independent from chain-level retryCount so a long
    chain with a single flaky step doesn't trip the chain cap.

Android FGS type for camera workloads

class VideoTranscodeWorker(
    appContext: Context,
    params: WorkerParameters,
    factory: AndroidWorkerFactory,
) : KmpHeavyWorker(appContext, params, factory) {
    override val foregroundServiceType: Int
        get() = if (Build.VERSION.SDK_INT >= 35) {
            FGS_MEDIA_PROCESSING  // Android 15+
        } else {
            FGS_DATA_SYNC
        }
}

KmpHeavyWorker is now open with protected open val foregroundServiceType: Int.
Companion-object aliases avoid forcing android.content.pm.ServiceInfo imports
into host code. Wrong-type symptoms (Play Store policy flags, Android 15
6-hour cap, ForegroundServiceStartNotAllowedException) are spelled out in
docs/ANDROID_FGS_GUIDE.md.


Hardening — Fixes for bugs the existing test suite missed

Bug Adversarial test
PendingIntent request-code collision on UUID-style IDs (e.g. "Aa".hashCode() == "BB".hashCode()) PendingIntentCodesAdversarialTest — proves CRC32 fixes the canonical pair + 10 000-UUID generator
BaseAlarmReceiver coroutine outliving the receiver's 10 s budget BaseAlarmReceiverLifecycleTest — Robolectric proves withTimeout cancels stuck work, finally always runs
iOS IosBackgroundUrlSessionManager orphaning downloads on cold-launch BackgroundDownloadStateStoreTest — simulates process-death + cache invalidation, asserts disk-backed lookup
ChainProgress schema regression breaking pending tasks on upgrade BackwardCompatibilityTest — drops v2.4.3-shaped JSON, asserts v2.5 reader loads it without data loss
AppendOnlyQueue bloating under enqueue-heavy workload QueueScaleStressTest — 2 000-op stress + 6 MB file-size compaction trigger

Late-cycle pre-publish QA pass (2 review rounds, 8 bugs)

A targeted audit on cancellation paths, lock-ordering, exception
classification, and inter-process safety bypass
uncovered 8 more bugs not
caught by the existing 1 300+ test suite. Each is pinned by a V250…Test.kt
regression test that was verified to fail under a temporary revert of its fix:

Bug Severity Regression test
BaseKmpWorker deleted overflow input file on CancellationException → WorkManager re-run lost user payload P0 V250CancellationPreservesOverflowFileTest (Android)
iOS NSFileCoordinator bypassed the lock on timeout → App↔Extension write interleave corrupts queue.jsonl (CRC32 fail) P1 V250FileCoordinatorTimeoutTest (iOS)
DynamicTaskDispatcher.requestShutdownSync cancelled an unused job — in-flight worker kept running past iOS BGTask budget → Watchdog SIGKILL P2 V250DispatcherShutdownTest (iOS)
BaseKmpWorker classified transient NPE/IAE/NumberFormatException as permanent → workers dropped on flaky-server null fields (see Migration §5) BAD V250ExceptionClassificationTest (Android)
IosFileStorage.close() skipped cancel + join when flushNow() threw → coroutine leak across teardown BAD V250CloseFinallyTest (iOS)
iOS DynamicTaskDispatcher silently dropped WorkerResult.Retry / Failure(shouldRetry=true) (see Migration §6) P0 V250DynamicRetryTest (iOS, 6 cases)
IosFileStorage.replaceChainAtomic bypassed enqueueMutex → concurrent enqueueChain raced through size check, exceeding MAX_QUEUE_SIZE P0 V250ReplaceChainMutexRaceTest (iOS, reproduces 1001 > 1000 under revert)
ChainExecutor inner catch (TimeoutCancellationException) mis-attributed outer-scope cancellations as chain-timeout AND swallowed them, defeating the outer's timeout P1 V250NestedTimeoutMisattributionTest (iOS)
ChainExecutor.executeTask had the same outer-cancel misattribution at the deeper task-timeout layer — emitted spurious "Timeout after Xms" telemetry to TaskEventBus when an outer scope cancelled the worker mid-delay P1 V250TaskTimeoutMisattributionTest (iOS)
IosBackgroundTaskHandler.handleSingleTask (entry point for dedicated-identifier BGTasks — the common iOS user path) silently dropped WorkerResult.Retry and Failure(shouldRetry=true), leaving iOS with no pending BGTaskRequest. Same bug class as DynamicTaskDispatcher.handleOneTimeResult, different code path. (See Migration §6) P0 V250HandleSingleTaskRetryTest (iOS, 6 cases)
ChainExecutor.executeStep dropped Failure.shouldRetry flag — workers returning `Failure(shou...
Read more

v2.4.3 - Security hardening, stability fixes, and infrastructure upgrade.

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 01 May 07:05

Security hardening, stability fixes, and infrastructure upgrade.

Highlights

  • Security: SSRF-safe redirect following — all HTTP workers now validate each redirect hop
    against SecurityValidator, and credential headers are stripped on cross-origin hops (RFC 7235 §3.1).
  • Stability: BGTask expiration race condition eliminated — setTaskCompletedWithSuccess is now
    guarded by an AtomicInt and the expiration handler is wired before the coroutine launches.
  • Android alarm reliability: PendingIntent request codes now use CRC32 instead of hashCode()
    to prevent silent collision-based alarm replacement.
  • Periodic task safety: TaskTrigger.Periodic enforces the 15-minute OS minimum at construction
    (IllegalArgumentException for intervals below MIN_INTERVAL_MS).
  • Kotlin 2.1.21: Upgraded from 2.1.0; KSP updated to 2.1.21-2.0.1.

🐛 Bug Fixes

Security

  • SSRF redirect bypass (Android + iOS): Disabled engine-level auto-redirect following and added
    a custom HttpSend interceptor that validates every Location URL via SecurityValidator before
    following. Maximum 10 redirect hops enforced.
  • Auth header leak on cross-origin redirect: Authorization and Cookie headers are now removed
    when a redirect targets a different host, preventing credential forwarding to unintended servers.
  • Path traversal in HttpDownloadWorker: savePath is now validated via
    SecurityValidator.validateFilePath() before the file is written.

iOS

  • BGTask double-completion (race condition): handleSingleTask now uses an AtomicInt
    (compareAndSet) guard to ensure setTaskCompletedWithSuccess is called exactly once.
    The expiration handler is assigned before scope.launch to eliminate the window where an
    expiration fires before the handler is set.

Android

  • PendingIntent request code collision: Replaced id.hashCode() (32-bit, collision-prone) with a
    CRC32-based taskIdToRequestCode() in NativeTaskScheduler. Prevents silent exact-alarm
    replacement when two task IDs happen to share a hash.

Common

  • Periodic minimum interval not enforced: TaskTrigger.Periodic now throws
    IllegalArgumentException for intervalMs < MIN_INTERVAL_MS (15 minutes) at construction time,
    matching the Android WorkManager and iOS BGTaskScheduler OS requirements.

Infrastructure

  • Kotlin upgraded 2.1.02.1.21
  • KSP upgraded 2.1.0-1.0.292.1.21-2.0.1 (KSP adopted independent versioning in 2.0+)
  • BugFixes_v243_IosTest: replaced uncooperative usleep spin loop test (caused infinite hang
    in runTest cleanup) with a cooperative coroutine-based equivalent.

Breaking Changes

  • TaskTrigger.Periodic(intervalMs) now throws IllegalArgumentException if intervalMs is
    below 15 minutes. Previously accepted silently; the OS would silently enforce its own floor.
    Add MIN_INTERVAL_MS companion constant available as TaskTrigger.Periodic.MIN_INTERVAL_MS.

v2.4.2 - Hardening the iOS implementation

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 28 Apr 06:40

This release focuses on hardening the iOS implementation, specifically around memory management and scheduling accuracy. We've also cleaned up some architectural boundaries to make the library easier to maintain and localize.

Highlights

🚀 iOS Performance & Reliability

  • Memory Streaming: We now use Okio to stream large log and event files. This keeps RAM usage constant (O(1)) even if you have thousands of pending events, preventing OOM crashes on older devices.
  • Drift Correction: Fixed a bug where periodic tasks on iOS would slowly drift over time. Tasks are now "anchored" to their original start time for consistent execution windows.
  • UTF-8 Safety: The new streaming logic correctly handles multi-byte characters (like Emojis) that happen to sit on chunk boundaries, preventing data corruption in your logs.

🐛 Bug Fixes

  • Periodic Task Immediate Execution: Fixed a regression where runImmediately = true (or initialDelay) on periodic tasks did not trigger the task immediately. This was caused by strict default flex intervals on Android and drift-correction delays on iOS.
  • iOS: Periodic Task Loop (Simulator): Fixed an infinite loop in the iOS Simulator fallback where periodic tasks would reschedule and execute immediately without delay. Resolved by switching to ExistingPolicy.KEEP and runImmediately = false during internal reschedules.
  • Startup Deadlocks: Resolved a race condition on iOS where the scheduler could hang during migration if the default dispatcher pool was saturated.
  • History Store: Fixed a crash when requesting the last 0 records from the execution history.
  • Test Integrity: Updated our stress tests to use real coroutine concurrency, catching race conditions that sequential loops would miss.

🛠 Architectural Cleanup

  • Clean UI Boundaries: We removed English display strings from the ScheduleResult domain enum. You should now map these results to localized strings in your UI layer.
  • Protected Internals: Added @InternalKmpWorkManagerApi to hide testing hooks from your autocomplete while keeping them accessible for our own integration tests.

Migration Guide

ScheduleResult Messages

If you were calling result.toDisplayMessage(), you now need to handle that mapping in your UI.

New way:

val result = scheduler.enqueue(...)
val message = when(result) {
    ScheduleResult.ACCEPTED -> "Task scheduled!"
    ScheduleResult.REJECTED_OS_POLICY -> "Rejected by system"
    // ... handle others
}

Internal Changes

  • Switched to GCD-backed IO dispatchers for safer background initialization.
  • Added comprehensive UTF-8 boundary safety tests for iOS.
  • Enhanced test isolation to prevent cross-test file contention.

v2.4.1 - iOS Dynamic Tasks & Reliability Update

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 24 Apr 03:25

🚀 What's New in v2.4.1

This release brings significant architectural improvements to the iOS engine, making background task scheduling truly seamless across platforms.

📱 iOS Dynamic Task Scheduling

Say goodbye to fixed identifiers in Info.plist. v2.4.1 introduces the Master Dispatcher pattern, allowing you to use unlimited dynamic task IDs (e.g., UUID-based) out of the box. You only need to register one identifier in your plist: `kmp_master_dispatcher_task`.

⏱️ Periodic Task Enhancements

Gain absolute control over the first run of your periodic tasks:

  • initialDelayMs: Set a precise delay for the first execution.
  • runImmediately: A new flag (default: `true`) to control if the task should fire as soon as possible or wait for the first interval.

🛡️ Security Hardening

  • Built-in SSRF Protection: `HttpRequestWorker` now validates URLs using `SecurityValidator` to prevent unauthorized internal network access.
  • Path Traversal Protection: Enforced strict directory boundaries for task metadata storage.

⚡ Performance & Reliability

  • iOS Memory Optimization: Metadata scanning now uses lazy `Sequence` (O(1) memory), making it safe for apps with tens of thousands of tasks.
  • Drift Correction: Improved periodic scheduling logic on iOS to ensure consistent intervals over time.
  • Watchdog Protection: Added an adaptive execution budget to gracefully stop workers before iOS terminates the background process.
  • Fail-safe Init: Added timeout protection for storage migration to prevent app hangs during startup.

🛠 Fixes

  • Fixed a critical data loss bug during iOS queue sorting.
  • Resolved "Zombie Coroutines" on iOS by properly canceling jobs when a background task expires.
  • Fixed a bug where `CancellationException` was swallowed, preventing clean execution shutdown.
  • Fixed duplicate class compilation errors in the demo app.

📦 Artifacts

Maven artifacts are available on Maven Central.

  • Library: `dev.brewkits:kmpworkmanager:2.4.1`
  • Annotations: `dev.brewkits:kmpworker-annotations:2.4.1`
  • KSP Processor: `dev.brewkits:kmpworker-ksp:2.4.1`

v2.4.0: Native iOS Background Handler & Stability Fixes

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 16 Apr 14:07

[2.4.0] - 2026-04-16

Added

  • iOS Native Background Handler: Created IosBackgroundTaskHandler in iosMain module, providing a native Kotlin API for iOS background task execution. Handles metadata resolution, execution, and auto-rescheduling for periodic tasks.
  • iOS Simulator Fallback: Implemented automatic fallback in NativeTaskScheduler for iOS Simulators. Tasks now execute immediately in-process on simulators where BGTaskScheduler is unavailable, simplifying UI development and manual testing.
  • Android Exact Alarm Fallback: Added DefaultAlarmReceiver and automatic manifest registration. TaskTrigger.Exact now works out-of-the-box without requiring custom receiver implementation in the host app.
  • Unified Background Engine: Refactored the library and sample app to use a single consolidated background engine, resolving Swift namespace collisions and simplifying dependency injection.

Fixed

  • Android: Expedited Task Compatibility: Fixed IllegalArgumentException when scheduling expedited tasks with incompatible constraints (e.g., requiresCharging). The library now gracefully falls back to standard execution.
  • Android: Periodic Task Payload Limit: Implemented enforcement for the 8KB payload limit on periodic tasks. Since WorkManager doesn't support disk-spill for periodic tasks, large payloads are now rejected with REJECTED_OS_POLICY instead of causing silent data truncation.
  • iOS: Time-Slicing Logic: Improved adaptive budget calculation in ChainExecutor to allow execution even when the remaining BGTask window is smaller than the standard task timeout.
  • iOS: File Protection: Directories created by IosFileStorage now use NSFileProtectionCompleteUntilFirstUserAuthentication by default, ensuring BGTasks can access data after the first unlock even while the screen is locked.

v2.3.9 Hot fix

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 11 Apr 07:23

[2.3.9] - 2026-04-11

Fixed

Android: Missing foreground service permissions cause SecurityException at runtime (HIGH)

  • Root cause: The library's AndroidManifest.xml did not declare FOREGROUND_SERVICE or FOREGROUND_SERVICE_DATA_SYNC. KmpHeavyWorker.setForeground() throws SecurityException on Android 9+ without FOREGROUND_SERVICE, and on Android 14+ without FOREGROUND_SERVICE_DATA_SYNC when using FOREGROUND_SERVICE_TYPE_DATA_SYNC.
  • Fixed by adding both <uses-permission> declarations to the library manifest. Android Gradle plugin merges these automatically into the consumer app's AndroidManifest.xml.

KSP: Workers with an intermediate base class silently excluded from generated factory (HIGH)

  • Root cause: WorkerProcessor used classDecl.superTypes to check for AndroidWorker/IosWorker — inspecting only the direct parent. A class extending a custom base (e.g. class MyWorker : BaseAppWorker() where BaseAppWorker : AndroidWorker) was silently skipped. At runtime these tasks failed with "worker not found".
  • Fixed: extendsWorkerType() now traverses the full inheritance hierarchy with a cycle guard.

Android: Deprecated 2-arg constructor throws cryptic NPE instead of actionable error (LOW)

  • Root cause: BaseKmpWorker(Context, WorkerParameters) and KmpHeavyWorker(Context, WorkerParameters) called KmpWorkManagerKoin.getKoin().get() without a try-catch. An uninitialised Koin threw IllegalStateException with a generic message, with no hint for the developer.
  • Fixed: the call is now wrapped in a try-catch that rethrows with a clear message pointing to KmpWorkManager.initialize() and KmpWorkerFactory.

Added

KSP: @Worker(aliases = [...]) for safe class renames

  • Workers can now declare one or more alias names that also resolve to the same factory lambda. Safe rename path: add the old simple name to aliases, remove it once all devices have drained their queues.
  • Example: @Worker(name = "SyncWorkerV2", aliases = ["SyncWorker"]).

KSP: ProGuard / rename warning when name is absent

  • WorkerProcessor now emits a logger.warn(...) at build time when @Worker has no explicit name argument. The generated key defaults to the simple class name, which breaks persisted tasks on rename or ProGuard obfuscation.
  • Fix: add @Worker(name = "StableIdentifier").

Changed

iOS: IosFileStorage — removed deprecated DELETED_MARKER_MAX_AGE_MS constant

  • IosFileStorage.Companion.DELETED_MARKER_MAX_AGE_MS was marked @Deprecated since v2.3.4. It is now removed. Use IosFileStorageConfig.deletedMarkerMaxAgeMs instead.

Tests

  • New KmpWorkerForegroundInfoCompatTest.testHeavyTask_onApi31Plus_schedulesWithoutSecurityException — verifies KmpHeavyWorker schedules without SecurityException on any API level.
  • New WorkerProcessorTest.testCodeGeneration_IndirectAndroidInheritance_GeneratesFactory and testCodeGeneration_IndirectIosInheritance_GeneratesFactory — regression tests for the deep-inheritance KSP fix.
  • New WorkerProcessorTest.testCodeGeneration_WrongBaseClass_EmitsWarning_SkipsWorker — verifies workers not extending AndroidWorker/IosWorker are excluded with a warning.
  • New WorkerProcessorTest.testCodeGeneration_AliasesGenerateAdditionalEntries — verifies alias entries are generated in the factory map.