Releases: brewkits/kmpworkmanager
Release list
v3.1.0 — Constraints.maxRetries + security fixes
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 = truepreviously 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 deprecatedNetworkInfoAPI on an unreachable pre-API-23 branch, and an ignoredFilereturn 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 workersv3.0.1
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.
KmpWorkerlost itsgetForegroundInfo()override during the v2.3.8BaseKmpWorker
refactor. The scheduler marks immediate, non-heavy tasks as expedited work, which runs
as a foreground service on API < 31 — WorkManager then callsgetForegroundInfo(), and
the defaultCoroutineWorkerimplementation 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 workersFull changelog: https://github.com/brewkits/kmpworkmanager/blob/main/CHANGELOG.md
v3.0.0 — Ktor 3 + HTTP workers extracted
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.12 → 3.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 workersRegister 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
-1at end-of-stream (Ktor 2 returned0); download loops now break correctly. - iOS network reachability — real
NWPathMonitorimplementation, 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
Known issues
- iOS
FileCompressionWorkerhas 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
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 backoff — Failure 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
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:
- Correctness gaps that the existing test suite did not catch — file
compression silently copying,PendingIntentrequest-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. - 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. - Documenting the physical limits of iOS background execution.
BGTaskScheduleris opportunistic; no library can change that. v2.5 ships
IOS_BGTASK_LIMITS.md,ANDROID_FGS_GUIDE.md, and
APPLE_APP_STORE_REVIEW_GUIDELINES.mdso 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
Rangechunks (default 4),.partNfiles persisted for
per-chunk resume. - Automatic sequential fallback when the server returns no
Content-Length
or noAccept-Ranges: bytes. - Per-chunk resume skips parts whose
.partNmatches 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.fileResultsexposes 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'sHashingSource). OVERWRITE(default, pre-v2.5 behaviour),SKIP(returnSuccesswithout
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:
delayMsis captured intoChainExecutor.requestedNextBgTaskDelayMs;
Swift hosts read it to re-armBGProcessingTaskRequestwith
earliestBeginDate = now + delayMs.attemptCapis enforced viaChainProgress.stepRetryCounts— a step that
exceeds the cap abandons the chain.- Per-step counter is independent from chain-level
retryCountso 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... |
v2.4.3 - Security hardening, stability fixes, and infrastructure upgrade.
Security hardening, stability fixes, and infrastructure upgrade.
Highlights
- Security: SSRF-safe redirect following — all HTTP workers now validate each redirect hop
againstSecurityValidator, and credential headers are stripped on cross-origin hops (RFC 7235 §3.1). - Stability: BGTask expiration race condition eliminated —
setTaskCompletedWithSuccessis now
guarded by anAtomicIntand 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.Periodicenforces the 15-minute OS minimum at construction
(IllegalArgumentExceptionfor intervals belowMIN_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 customHttpSendinterceptor that validates everyLocationURL viaSecurityValidatorbefore
following. Maximum 10 redirect hops enforced. - Auth header leak on cross-origin redirect:
AuthorizationandCookieheaders are now removed
when a redirect targets a different host, preventing credential forwarding to unintended servers. - Path traversal in HttpDownloadWorker:
savePathis now validated via
SecurityValidator.validateFilePath()before the file is written.
iOS
- BGTask double-completion (race condition):
handleSingleTasknow uses anAtomicInt
(compareAndSet) guard to ensuresetTaskCompletedWithSuccessis called exactly once.
The expiration handler is assigned beforescope.launchto 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-basedtaskIdToRequestCode()inNativeTaskScheduler. Prevents silent exact-alarm
replacement when two task IDs happen to share a hash.
Common
- Periodic minimum interval not enforced:
TaskTrigger.Periodicnow throws
IllegalArgumentExceptionforintervalMs < MIN_INTERVAL_MS(15 minutes) at construction time,
matching the Android WorkManager and iOS BGTaskScheduler OS requirements.
Infrastructure
- Kotlin upgraded
2.1.0→2.1.21 - KSP upgraded
2.1.0-1.0.29→2.1.21-2.0.1(KSP adopted independent versioning in 2.0+) BugFixes_v243_IosTest: replaced uncooperativeusleepspin loop test (caused infinite hang
inrunTestcleanup) with a cooperative coroutine-based equivalent.
Breaking Changes
TaskTrigger.Periodic(intervalMs)now throwsIllegalArgumentExceptionifintervalMsis
below 15 minutes. Previously accepted silently; the OS would silently enforce its own floor.
AddMIN_INTERVAL_MScompanion constant available asTaskTrigger.Periodic.MIN_INTERVAL_MS.
v2.4.2 - Hardening the iOS implementation
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(orinitialDelay) 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.KEEPandrunImmediately = falseduring 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
ScheduleResultdomain enum. You should now map these results to localized strings in your UI layer. - Protected Internals: Added
@InternalKmpWorkManagerApito 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
🚀 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
[2.4.0] - 2026-04-16
Added
- iOS Native Background Handler: Created
IosBackgroundTaskHandleriniosMainmodule, 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
NativeTaskSchedulerfor iOS Simulators. Tasks now execute immediately in-process on simulators whereBGTaskScheduleris unavailable, simplifying UI development and manual testing. - Android Exact Alarm Fallback: Added
DefaultAlarmReceiverand automatic manifest registration.TaskTrigger.Exactnow 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
IllegalArgumentExceptionwhen 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_POLICYinstead of causing silent data truncation. - iOS: Time-Slicing Logic: Improved adaptive budget calculation in
ChainExecutorto allow execution even when the remaining BGTask window is smaller than the standard task timeout. - iOS: File Protection: Directories created by
IosFileStoragenow useNSFileProtectionCompleteUntilFirstUserAuthenticationby default, ensuring BGTasks can access data after the first unlock even while the screen is locked.
v2.3.9 Hot fix
[2.3.9] - 2026-04-11
Fixed
Android: Missing foreground service permissions cause SecurityException at runtime (HIGH)
- Root cause: The library's
AndroidManifest.xmldid not declareFOREGROUND_SERVICEorFOREGROUND_SERVICE_DATA_SYNC.KmpHeavyWorker.setForeground()throwsSecurityExceptionon Android 9+ withoutFOREGROUND_SERVICE, and on Android 14+ withoutFOREGROUND_SERVICE_DATA_SYNCwhen usingFOREGROUND_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'sAndroidManifest.xml.
KSP: Workers with an intermediate base class silently excluded from generated factory (HIGH)
- Root cause:
WorkerProcessorusedclassDecl.superTypesto check forAndroidWorker/IosWorker— inspecting only the direct parent. A class extending a custom base (e.g.class MyWorker : BaseAppWorker()whereBaseAppWorker : 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)andKmpHeavyWorker(Context, WorkerParameters)calledKmpWorkManagerKoin.getKoin().get()without a try-catch. An uninitialised Koin threwIllegalStateExceptionwith 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()andKmpWorkerFactory.
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
WorkerProcessornow emits alogger.warn(...)at build time when@Workerhas no explicitnameargument. 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_MSwas marked@Deprecatedsince v2.3.4. It is now removed. UseIosFileStorageConfig.deletedMarkerMaxAgeMsinstead.
Tests
- New
KmpWorkerForegroundInfoCompatTest.testHeavyTask_onApi31Plus_schedulesWithoutSecurityException— verifiesKmpHeavyWorkerschedules withoutSecurityExceptionon any API level. - New
WorkerProcessorTest.testCodeGeneration_IndirectAndroidInheritance_GeneratesFactoryandtestCodeGeneration_IndirectIosInheritance_GeneratesFactory— regression tests for the deep-inheritance KSP fix. - New
WorkerProcessorTest.testCodeGeneration_WrongBaseClass_EmitsWarning_SkipsWorker— verifies workers not extendingAndroidWorker/IosWorkerare excluded with a warning. - New
WorkerProcessorTest.testCodeGeneration_AliasesGenerateAdditionalEntries— verifies alias entries are generated in the factory map.