From cbd975b296e281812c1dcef242f7c442d207e6f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nguye=CC=82=CC=83n=20Tua=CC=82=CC=81n=20Vie=CC=A3=CC=82t?=
Date: Wed, 6 May 2026 23:48:45 +0700
Subject: [PATCH 1/5] chore: prepare release v1.2.5
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index 91d22a3..272f4af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ migrate_working_dir/
GEMINI.md
.gemini/
.gemini_security/
+CLAUSE.md
# Claude Code
.claude/
From 5e84631d5d631cfc9ea3761e0a3cf5a1ff71920f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nguye=CC=82=CC=83n=20Tua=CC=82=CC=81n=20Vie=CC=A3=CC=82t?=
Date: Thu, 7 May 2026 00:33:31 +0700
Subject: [PATCH 2/5] fix: restore flexMs KotlinLong constructor and clarify
bridge comments
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Revert `flexMs as? KotlinLong` (always nil) back to explicit
`KotlinLong(value: flexMs!)` constructor. The as? cast silently
dropped flexInterval for all iOS periodic tasks.
- Add KMP-BRIDGE-FROZEN marker to prevent future simplification.
- Fix misleading comment: "inherently not running immediately" → clear
explanation that runImmediately is forced true to satisfy KMP library.
---
.../NativeWorkmanagerPlugin+Enqueue.kt | 4 ++--
.../native_workmanager/KMPSchedulerBridge.swift | 11 ++++++++---
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt b/android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt
index cc5a253..16314b5 100644
--- a/android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt
+++ b/android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt
@@ -303,8 +303,8 @@ internal fun NativeWorkmanagerPlugin.handleEnqueue(call: MethodCall, result: Res
val initialDelayMs = (triggerMap?.get("initialDelayMs") as? Number)?.toLong() ?: 0L
var runImmediately = triggerMap?.get("runImmediately") as? Boolean ?: true
- // Resolve KMP Library "Ambiguous" conflict: if initial delay is provided,
- // the task is inherently not running immediately.
+ // KMP library rejects runImmediately=false when initialDelayMs>0 ("Ambiguous" error).
+ // Force true so the library accepts the request; initialDelayMs controls first-run timing.
if (initialDelayMs > 0L) {
runImmediately = true
}
diff --git a/ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift b/ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift
index 605eb8e..a9be716 100644
--- a/ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift
@@ -73,15 +73,20 @@ class KMPSchedulerBridge {
let initialDelayMs = (map["initialDelayMs"] as? NSNumber)?.int64Value ?? 0
var runImmediately = map["runImmediately"] as? Bool ?? true
- // Resolve KMP Library "Ambiguous" conflict: if initial delay is provided,
- // the task is inherently not running immediately.
+ // ⚠️ KMP-BRIDGE-FROZEN: flexMs Int64? → KotlinLong? requires explicit constructor.
+ // Swift cannot cast Int64 to boxed Kotlin types directly.
+ // Do NOT simplify to 'as? KotlinLong'.
+ let flexMsValue = flexMs != nil ? KotlinLong(value: flexMs!) : nil
+
+ // KMP library rejects runImmediately=false when initialDelayMs>0 ("Ambiguous" error).
+ // Force true so the library accepts the request; initialDelayMs controls first-run timing.
if initialDelayMs > 0 {
runImmediately = true
}
return TaskTriggerPeriodic(
intervalMs: intervalMs,
- flexMs: flexMs as? KotlinLong,
+ flexMs: flexMsValue,
initialDelayMs: initialDelayMs,
runImmediately: runImmediately
)
From edd584126a58cf1521f1cc2a4dec1f747d9dba8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nguye=CC=82=CC=83n=20Tua=CC=82=CC=81n=20Vie=CC=A3=CC=82t?=
Date: Thu, 7 May 2026 00:38:12 +0700
Subject: [PATCH 3/5] safety: multi-layer bridge regression prevention
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- test: add PeriodicTrigger parameter combination matrix (8 combos,
including issue_26 regression) to task_trigger_test.dart
- test: add bridge_parameter_passthrough_test.dart — verifies exact
map key names and value types for all trigger types
- chore: add scripts/pre-commit-bridge-check.sh + install as git hook;
requires 'yes' acknowledgment before committing bridge files
- fix: Android logs warning when 'intervalMs' is missing from periodic
trigger map instead of silently defaulting
- fix: iOS logs error when 'intervalMs' is missing before returning nil
---
.../NativeWorkmanagerPlugin+Enqueue.kt | 6 +-
.../KMPSchedulerBridge.swift | 1 +
scripts/pre-commit-bridge-check.sh | 48 +++++
.../bridge_parameter_passthrough_test.dart | 167 ++++++++++++++++++
test/unit/task_trigger_test.dart | 102 +++++++++++
5 files changed, 323 insertions(+), 1 deletion(-)
create mode 100644 scripts/pre-commit-bridge-check.sh
create mode 100644 test/unit/bridge_parameter_passthrough_test.dart
diff --git a/android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt b/android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt
index 16314b5..39e9074 100644
--- a/android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt
+++ b/android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt
@@ -298,7 +298,11 @@ internal fun NativeWorkmanagerPlugin.handleEnqueue(call: MethodCall, result: Res
val triggerType = triggerMap?.get("type") as? String ?: "oneTime"
val trigger: TaskTrigger = when (triggerType) {
"periodic" -> {
- val intervalMs = (triggerMap?.get("intervalMs") as? Number)?.toLong() ?: 900_000L
+ val rawIntervalMs = (triggerMap?.get("intervalMs") as? Number)?.toLong()
+ if (rawIntervalMs == null) {
+ NativeLogger.w("periodic trigger missing 'intervalMs' — defaulting to 15 min. Dart bridge bug?")
+ }
+ val intervalMs = rawIntervalMs ?: 900_000L
val flexMs = (triggerMap?.get("flexMs") as? Number)?.toLong()
val initialDelayMs = (triggerMap?.get("initialDelayMs") as? Number)?.toLong() ?: 0L
var runImmediately = triggerMap?.get("runImmediately") as? Boolean ?: true
diff --git a/ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift b/ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift
index a9be716..dfa5bf5 100644
--- a/ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift
@@ -67,6 +67,7 @@ class KMPSchedulerBridge {
case "periodic":
guard let intervalMs = (map["intervalMs"] as? NSNumber)?.int64Value else {
+ NativeLogger.d("ERROR: periodic trigger missing 'intervalMs' — rejecting. Dart bridge bug?")
return nil
}
let flexMs = (map["flexMs"] as? NSNumber)?.int64Value
diff --git a/scripts/pre-commit-bridge-check.sh b/scripts/pre-commit-bridge-check.sh
new file mode 100644
index 0000000..b6d5e30
--- /dev/null
+++ b/scripts/pre-commit-bridge-check.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+# Pre-commit check for KMP bridge files.
+# Install: cp scripts/pre-commit-bridge-check.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
+
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+BRIDGE_FILES=(
+ "ios/native_workmanager/Sources/native_workmanager/KMPSchedulerBridge.swift"
+ "android/src/main/kotlin/dev/brewkits/native_workmanager/NativeWorkmanagerPlugin+Enqueue.kt"
+)
+
+changed_bridges=()
+for file in "${BRIDGE_FILES[@]}"; do
+ if git diff --cached --name-only | grep -qF "$file"; then
+ changed_bridges+=("$file")
+ fi
+done
+
+if [ ${#changed_bridges[@]} -eq 0 ]; then
+ exit 0
+fi
+
+echo -e "${RED}⚠️ KMP BRIDGE FILE(S) CHANGED:${NC}"
+for f in "${changed_bridges[@]}"; do
+ echo " • $f"
+done
+echo ""
+echo -e "${YELLOW}Mandatory checklist before committing:${NC}"
+echo " 1. KotlinLong/KotlinInt: still using explicit KotlinLong(value:) constructor?"
+echo " (NOT 'as? KotlinLong' — that always returns nil silently)"
+echo " 2. Every Dart map key parsed by the bridge is still named correctly?"
+echo " 3. New/changed parameter has a test in test/unit/bridge_parameter_passthrough_test.dart?"
+echo " 4. If this is a second 'cleanup' commit, treated with same scrutiny as the fix commit?"
+echo ""
+# Skip interactive prompt in CI or non-interactive shells
+if [ ! -t 1 ] || [ "${CI:-}" = "true" ] || [ "${SKIP_BRIDGE_CHECK:-}" = "1" ]; then
+ echo "(non-interactive — skipping prompt, proceeding)"
+ exit 0
+fi
+
+echo -n "Type 'yes' to confirm checklist complete: "
+read -r answer ());
+ expect(map['intervalMs'], equals(const Duration(hours: 2).inMilliseconds));
+ });
+
+ test('flexMs is int when set, null when absent', () {
+ final withFlex = TaskTrigger.periodic(
+ const Duration(hours: 1),
+ flexInterval: const Duration(minutes: 20),
+ ).toMap();
+ expect(withFlex['flexMs'], isA());
+ expect(withFlex['flexMs'], equals(const Duration(minutes: 20).inMilliseconds));
+
+ final noFlex = TaskTrigger.periodic(const Duration(hours: 1)).toMap();
+ expect(noFlex['flexMs'], isNull);
+ });
+
+ test('initialDelayMs is int when set, null when absent', () {
+ final withDelay = TaskTrigger.periodic(
+ const Duration(hours: 1),
+ initialDelay: const Duration(minutes: 15),
+ ).toMap();
+ expect(withDelay['initialDelayMs'], isA());
+ expect(withDelay['initialDelayMs'],
+ equals(const Duration(minutes: 15).inMilliseconds));
+
+ final noDelay = TaskTrigger.periodic(const Duration(hours: 1)).toMap();
+ expect(noDelay['initialDelayMs'], isNull);
+ });
+
+ test('runImmediately is bool', () {
+ final trueMap =
+ TaskTrigger.periodic(const Duration(hours: 1)).toMap();
+ expect(trueMap['runImmediately'], isA());
+ expect(trueMap['runImmediately'], isTrue);
+
+ final falseMap = TaskTrigger.periodic(
+ const Duration(hours: 1),
+ runImmediately: false,
+ ).toMap();
+ expect(falseMap['runImmediately'], isA());
+ expect(falseMap['runImmediately'], isFalse);
+ });
+ });
+
+ group('OneTimeTrigger map keys and types', () {
+ test('all expected keys are present', () {
+ final map =
+ TaskTrigger.oneTime(const Duration(minutes: 5)).toMap();
+ expect(map.containsKey('type'), isTrue);
+ expect(map.containsKey('initialDelayMs'), isTrue);
+ });
+
+ test('type is exactly "oneTime"', () {
+ final map = TaskTrigger.oneTime().toMap();
+ expect(map['type'], equals('oneTime'));
+ });
+
+ test('initialDelayMs is int (milliseconds)', () {
+ final map =
+ TaskTrigger.oneTime(const Duration(minutes: 10)).toMap();
+ expect(map['initialDelayMs'], isA());
+ expect(
+ map['initialDelayMs'], equals(const Duration(minutes: 10).inMilliseconds));
+ });
+
+ test('zero delay emits 0 not null', () {
+ final map = TaskTrigger.oneTime().toMap();
+ expect(map['initialDelayMs'], equals(0));
+ });
+ });
+
+ group('ExactTrigger map keys and types', () {
+ test('all expected keys are present', () {
+ final map = TaskTrigger.exact(DateTime(2030)).toMap();
+ expect(map.containsKey('type'), isTrue);
+ expect(map.containsKey('scheduledTimeMs'), isTrue);
+ });
+
+ test('type is exactly "exact"', () {
+ final map = TaskTrigger.exact(DateTime(2030)).toMap();
+ expect(map['type'], equals('exact'));
+ });
+
+ test('scheduledTimeMs is epoch milliseconds as int', () {
+ final dt = DateTime(2030, 6, 15, 10, 0, 0);
+ final map = TaskTrigger.exact(dt).toMap();
+ expect(map['scheduledTimeMs'], isA());
+ expect(map['scheduledTimeMs'], equals(dt.millisecondsSinceEpoch));
+ });
+ });
+
+ group('WindowedTrigger map keys and types', () {
+ test('all expected keys are present', () {
+ final map = TaskTrigger.windowed(
+ earliest: const Duration(hours: 1),
+ latest: const Duration(hours: 2),
+ ).toMap();
+ expect(map.containsKey('type'), isTrue);
+ expect(map.containsKey('earliestMs'), isTrue);
+ expect(map.containsKey('latestMs'), isTrue);
+ });
+
+ test('type is exactly "windowed"', () {
+ final map = TaskTrigger.windowed(
+ earliest: const Duration(hours: 1),
+ latest: const Duration(hours: 2),
+ ).toMap();
+ expect(map['type'], equals('windowed'));
+ });
+
+ test('earliestMs and latestMs are int milliseconds', () {
+ final map = TaskTrigger.windowed(
+ earliest: const Duration(hours: 1),
+ latest: const Duration(hours: 3),
+ ).toMap();
+ expect(map['earliestMs'], isA());
+ expect(map['latestMs'], isA());
+ expect(map['earliestMs'], equals(const Duration(hours: 1).inMilliseconds));
+ expect(map['latestMs'], equals(const Duration(hours: 3).inMilliseconds));
+ });
+ });
+ });
+}
diff --git a/test/unit/task_trigger_test.dart b/test/unit/task_trigger_test.dart
index 895eed5..c0704cc 100644
--- a/test/unit/task_trigger_test.dart
+++ b/test/unit/task_trigger_test.dart
@@ -207,6 +207,108 @@ void main() {
expect(str, contains('1:00:00'));
expect(str, contains('0:15:00'));
});
+
+ // ── Parameter Combination Matrix ──────────────────────────────────────
+ // Each combination must have an explicit test. If a combination is not
+ // listed here, it is NOT protected against regressions.
+ // Bridge keys verified: 'intervalMs', 'flexMs', 'initialDelayMs', 'runImmediately'
+ group('parameter combination matrix', () {
+ // Baseline
+ test('combo: interval only → defaults', () {
+ final map = TaskTrigger.periodic(
+ const Duration(hours: 1),
+ ).toMap();
+ expect(map['intervalMs'], const Duration(hours: 1).inMilliseconds);
+ expect(map['flexMs'], isNull);
+ expect(map['initialDelayMs'], isNull);
+ expect(map['runImmediately'], isTrue);
+ });
+
+ test('combo: interval + flex', () {
+ final map = TaskTrigger.periodic(
+ const Duration(hours: 6),
+ flexInterval: const Duration(minutes: 30),
+ ).toMap();
+ expect(map['intervalMs'], const Duration(hours: 6).inMilliseconds);
+ expect(map['flexMs'], const Duration(minutes: 30).inMilliseconds);
+ expect(map['initialDelayMs'], isNull);
+ expect(map['runImmediately'], isTrue);
+ });
+
+ test('combo: interval + initialDelay', () {
+ final map = TaskTrigger.periodic(
+ const Duration(hours: 1),
+ initialDelay: const Duration(minutes: 5),
+ ).toMap();
+ expect(map['intervalMs'], const Duration(hours: 1).inMilliseconds);
+ expect(map['flexMs'], isNull);
+ expect(map['initialDelayMs'], const Duration(minutes: 5).inMilliseconds);
+ expect(map['runImmediately'], isTrue);
+ });
+
+ test('combo: interval + runImmediately:false', () {
+ final map = TaskTrigger.periodic(
+ const Duration(hours: 1),
+ runImmediately: false,
+ ).toMap();
+ expect(map['intervalMs'], const Duration(hours: 1).inMilliseconds);
+ expect(map['flexMs'], isNull);
+ expect(map['initialDelayMs'], isNull);
+ expect(map['runImmediately'], isFalse);
+ });
+
+ test('combo: interval + flex + initialDelay', () {
+ final map = TaskTrigger.periodic(
+ const Duration(hours: 6),
+ flexInterval: const Duration(minutes: 30),
+ initialDelay: const Duration(minutes: 10),
+ ).toMap();
+ expect(map['intervalMs'], const Duration(hours: 6).inMilliseconds);
+ expect(map['flexMs'], const Duration(minutes: 30).inMilliseconds);
+ expect(map['initialDelayMs'], const Duration(minutes: 10).inMilliseconds);
+ expect(map['runImmediately'], isTrue);
+ });
+
+ test('combo: interval + flex + runImmediately:false', () {
+ final map = TaskTrigger.periodic(
+ const Duration(hours: 6),
+ flexInterval: const Duration(minutes: 30),
+ runImmediately: false,
+ ).toMap();
+ expect(map['intervalMs'], const Duration(hours: 6).inMilliseconds);
+ expect(map['flexMs'], const Duration(minutes: 30).inMilliseconds);
+ expect(map['initialDelayMs'], isNull);
+ expect(map['runImmediately'], isFalse);
+ });
+
+ // issue_26: this combination was blocked by an assert in v1.2.4
+ test('combo: interval + initialDelay + runImmediately:false [issue_26]', () {
+ final map = TaskTrigger.periodic(
+ const Duration(hours: 1),
+ initialDelay: const Duration(minutes: 30),
+ runImmediately: false,
+ ).toMap();
+ expect(map['intervalMs'], const Duration(hours: 1).inMilliseconds);
+ expect(map['flexMs'], isNull);
+ expect(map['initialDelayMs'], const Duration(minutes: 30).inMilliseconds);
+ // Dart emits false; native bridge overrides to true when initialDelayMs>0
+ // to satisfy KMP library. See KMPSchedulerBridge.swift and +Enqueue.kt.
+ expect(map['runImmediately'], isFalse);
+ });
+
+ test('combo: all four — interval + flex + initialDelay + runImmediately:false', () {
+ final map = TaskTrigger.periodic(
+ const Duration(hours: 6),
+ flexInterval: const Duration(minutes: 30),
+ initialDelay: const Duration(minutes: 10),
+ runImmediately: false,
+ ).toMap();
+ expect(map['intervalMs'], const Duration(hours: 6).inMilliseconds);
+ expect(map['flexMs'], const Duration(minutes: 30).inMilliseconds);
+ expect(map['initialDelayMs'], const Duration(minutes: 10).inMilliseconds);
+ expect(map['runImmediately'], isFalse);
+ });
+ });
});
group('ExactTrigger', () {
From 955fdf543660acb887fce196b6f6cab951ba328c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nguye=CC=82=CC=83n=20Tua=CC=82=CC=81n=20Vie=CC=A3=CC=82t?=
Date: Thu, 7 May 2026 01:19:35 +0700
Subject: [PATCH 4/5] chore: pre-publish review for v1.2.5
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- pubignore: exclude CLAUSE.md, coverage/, test_results.txt
- README: fix iOS badge (13.0+ → 14.0+), update install version to ^1.2.5
- CHANGELOG: remove duplicate --- separator and stray performance/acknowledgments block
- doc: remove BUG_FIX_VERIFICATION.md from public tracking (moved to doc/internal/)
- iOS workers: replace 148 bare print() calls with NativeLogger.d/w/e()
- iOS HttpDownloadWorker, HttpUploadWorker: remove internal sprint/T3 planning comments
- test: add periodic_trigger_reproduction_test.dart (8 cases, issue_26 regression coverage)
- native_workmanager_gen: update README version to ^1.2.5, reformat CHANGELOG
---
.pubignore | 3 +
CHANGELOG.md | 40 ---
README.md | 4 +-
doc/BUG_FIX_VERIFICATION.md | 191 -------------
.../periodic_trigger_reproduction_test.dart | 264 ++++++++++++++++++
example/ios/Flutter/AppFrameworkInfo.plist | 2 -
example/ios/Podfile.lock | 4 +-
.../engine/FlutterEngineManager.swift | 26 +-
.../workers/DartCallbackWorker.swift | 16 +-
.../workers/HttpDownloadWorker.swift | 131 ++++-----
.../workers/HttpUploadWorker.swift | 84 +++---
.../workers/ImageProcessWorker.swift | 12 +-
.../workers/IosWorker.swift | 2 +-
.../workers/MoveToSharedStorageWorker.swift | 8 +-
.../workers/ParallelHttpDownloadWorker.swift | 30 +-
.../workers/ParallelHttpUploadWorker.swift | 10 +-
native_workmanager_gen/CHANGELOG.md | 84 +++---
native_workmanager_gen/README.md | 2 +-
18 files changed, 472 insertions(+), 441 deletions(-)
delete mode 100644 doc/BUG_FIX_VERIFICATION.md
create mode 100644 example/integration_test/periodic_trigger_reproduction_test.dart
diff --git a/.pubignore b/.pubignore
index 794c0a7..13b3069 100644
--- a/.pubignore
+++ b/.pubignore
@@ -1,5 +1,6 @@
GEMINI.md
CLAUDE.md
+CLAUSE.md
CONTRIBUTING.md
ROADMAP.md
.github/
@@ -10,4 +11,6 @@ benchmark/
scripts/
tool/
assets/
+coverage/
+test_results.txt
doc/internal/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc66ca5..16c2f95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
----
-
## [1.2.5] - 2026-05-06
### Fixed
@@ -93,41 +91,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Global Middleware API**: Global interceptors for task configuration (Headers, RemoteConfig, Logging).
- **Code Generation Enhancements**: `native_workmanager_gen` now generates type-safe enqueue wrappers and automatic worker registries from `@WorkerCallback` annotations.
- **Task Graphs (DAG)**: Support for complex non-linear task dependencies on Android.
-
----
-
-### ⚡ **Performance**
-
-All workers maintain high performance with low resource usage:
-
-| Worker | Memory Usage | Startup Time | Battery Impact |
-|--------|-------------|--------------|----------------|
-| HttpDownloadWorker | Low | Fast | Minimal |
-| HttpUploadWorker | Low | Fast | Minimal |
-| FileDecompressionWorker | Low | Fast | Minimal |
-| CryptoWorker | Low | Fast | Minimal |
-
-**Key:** Streaming I/O keeps memory low regardless of file size.
-
----
-
-### 🙏 **Acknowledgments**
-
-Built on [kmpworkmanager v2.4.3](https://github.com/brewkits/kmpworkmanager/releases/tag/v2.4.3) for Kotlin Multiplatform.
-
----
-
-## Links
-
-- [GitHub Repository](https://github.com/brewkits/native_workmanager)
-- [Issue Tracker](https://github.com/brewkits/native_workmanager/issues)
-- [Documentation](https://github.com/brewkits/native_workmanager#readme)
-- [KMP WorkManager](https://github.com/brewkits/kmpworkmanager)
-- [Migration Guide](doc/MIGRATION_GUIDE.md)
-
----
-
-**Latest Version:** 1.2.5
-**Status:** Production Ready - Stable release for all production apps
-**KMP Parity:** 100% (kmpworkmanager v2.4.3)
-**Platforms:** Android | iOS
diff --git a/README.md b/README.md
index 8b8629e..8f10b21 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-
+
---
@@ -46,7 +46,7 @@ No boilerplate. No native code to write. No `AndroidManifest.xml` changes. Each
```yaml
dependencies:
- native_workmanager: ^1.2.4
+ native_workmanager: ^1.2.5
```
**2. Initialize once in `main()`:**
diff --git a/doc/BUG_FIX_VERIFICATION.md b/doc/BUG_FIX_VERIFICATION.md
deleted file mode 100644
index 7e0972e..0000000
--- a/doc/BUG_FIX_VERIFICATION.md
+++ /dev/null
@@ -1,191 +0,0 @@
-# WorkManager 2.10.0+ Bug Fix Verification
-
-## Bug Report
-
-**Issue:** IllegalStateException: Not implemented at androidx.work.CoroutineWorker.getForegroundInfo(CoroutineWorker.kt:92)
-**Severity:** Critical - All Android users affected
-
-## Root Cause Analysis
-
-### The Problem
-
-WorkManager 2.10.0+ changed internal behavior:
-- For expedited OneTime tasks, WorkManager now calls `getForegroundInfoAsync()` in the execution path
-- This method requires `getForegroundInfo()` to be overridden
-- **kmpworkmanager < 2.3.3** did not override this method
-- Default `CoroutineWorker` implementation throws `IllegalStateException: Not implemented`
-
-### Impact
-
-All tasks using:
-- `setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)` (default for OneTime tasks)
-- WorkManager 2.10.0 or higher
-
-Would crash immediately upon execution.
-
-## The Fix
-
-### kmpworkmanager 2.3.3
-
-**File:** `kmpworker/src/androidMain/kotlin/dev/brewkits/kmpworkmanager/background/data/KmpWorker.kt`
-
-```kotlin
-override suspend fun getForegroundInfo(): ForegroundInfo {
- ensureNotificationChannel()
- val title = applicationContext.getString(R.string.kmp_worker_notification_title)
- val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
- .setSmallIcon(android.R.drawable.ic_popup_sync)
- .setContentTitle(title)
- .setPriority(NotificationCompat.PRIORITY_MIN)
- .setSilent(true)
- .setOngoing(false)
- .build()
- return ForegroundInfo(NOTIFICATION_ID, notification)
-}
-```
-
-**Key Points:**
-- Overrides `getForegroundInfo()` to provide valid notification
-- Uses string resources for i18n support
-- Creates minimal-priority, silent notification
-- Separate channel ID (`kmp_worker_tasks`) from `KmpHeavyWorker` (`kmp_heavy_worker_channel`)
-
-### Additional Fixes in 2.3.3
-
-1. **Chain Heavy-Task Routing Bug**
- - `NativeTaskScheduler.createWorkRequest()` was using `KmpWorker` for all chain tasks
- - Fixed: Heavy tasks (`isHeavyTask=true`) now correctly use `KmpHeavyWorker`
-
-2. **Notification Localization**
- - Added `res/values/strings.xml` with 5 notification string resources
- - Host apps can override per locale (e.g., `res/values-ja/strings.xml`)
- - Backward compatible: Falls back to hardcoded English if resources not found
-
-### native_workmanager 1.0.4
-
-**File:** `android/build.gradle`
-
-```gradle
-// Before (workaround):
-api("androidx.work:work-runtime-ktx:2.9.1") // Pinned to avoid crash
-
-// After (proper fix):
-api("dev.brewkits:kmpworkmanager:2.3.3") // Upgraded from 2.3.1
-api("androidx.work:work-runtime-ktx:2.10.1") // Now safe to use 2.10.1+
-```
-
-## Verification
-
-### Build Verification
-
-✅ **Maven Central Availability**
-```bash
-$ curl -s "https://repo1.maven.org/maven2/dev/brewkits/kmpworkmanager/2.3.3/kmpworkmanager-2.3.3.pom" | head -5
-
-
- 4.0.0
- dev.brewkits
- kmpworkmanager
- 2.3.3
-```
-
-✅ **Clean Build from Maven Central**
-```bash
-$ cd native_workmanager/example
-$ flutter clean
-$ flutter pub get
-$ flutter build apk --debug
-
-Running Gradle task 'assembleDebug'... 21.1s
-✓ Built build/app/outputs/flutter-apk/app-debug.apk
-```
-
-### Test Coverage
-
-#### Integration Test
-**File:** `example/integration_test/workmanager_2_10_bug_fix_test.dart`
-
-Tests:
-1. ✅ OneTime expedited task (original crash scenario)
-2. ✅ Multiple concurrent expedited tasks (stress test)
-3. ✅ Periodic task (non-expedited, should still work)
-4. ✅ Task chain with expedited tasks
-5. ✅ Notification localization support
-
-#### Interactive Demo
-**File:** `example/lib/screens/bug_fix_demo_screen.dart`
-
-Visual demo proving bug fix:
-- Shows bug information and fix details
-- Runs 5 test scenarios:
- - OneTime expedited task
- - 3 concurrent expedited tasks
- - 2-step task chain
-- Real-time status updates (running → passed/failed)
-- Summary dialog showing pass/fail counts
-
-**Access:** Open example app → "🐛 Bug Fix" tab
-
-### Runtime Verification
-
-Expected behavior on Android with WorkManager 2.10.1+:
-- ✅ Expedited tasks execute without crash
-- ✅ Notification appears briefly in system tray (silent, min priority)
-- ✅ Tasks complete successfully
-- ✅ No `IllegalStateException` in logcat
-
-## Release Timeline
-
-| Date | Version | Event |
-|------|---------|-------|
-| 2026-02-16 | native_workmanager 1.0.3 | Initial bug report from community user |
-| 2026-02-17 | kmpworkmanager 2.3.3 | Fix released to Maven Central |
-| 2026-02-18 | native_workmanager 1.0.4 | Updated dependency, bug verified fixed |
-
-## Migration Guide
-
-### For Existing Users
-
-**No code changes required!** Just upgrade:
-
-```yaml
-# pubspec.yaml
-dependencies:
- native_workmanager: ^1.2.2 # was ^1.2.1
-```
-
-Then:
-```bash
-flutter pub get
-flutter clean
-flutter build apk
-```
-
-### Notification Localization (Optional)
-
-Host apps can override notification strings:
-
-**Example: Japanese localization**
-
-Create `android/app/src/main/res/values-ja/strings.xml`:
-```xml
-
- バックグラウンドタスク
- バックグラウンドタスクを実行中
- 重いタスク
- タスクを処理中
- 重いタスクを処理中…
-
-```
-
-Android will automatically use the correct locale based on device language.
-
-## Conclusion
-
-✅ **Bug completely fixed**
-- Root cause: Missing `getForegroundInfo()` override in kmpworkmanager
-- Solution: Added proper override in 2.3.3
-- Verification: Both Android + iOS builds pass, demo confirms no crashes
-- Bonus fixes: Chain routing bug + notification i18n support
-
-**All users should upgrade to the latest stable version (^1.2.2) immediately.**
diff --git a/example/integration_test/periodic_trigger_reproduction_test.dart b/example/integration_test/periodic_trigger_reproduction_test.dart
new file mode 100644
index 0000000..0ab0270
--- /dev/null
+++ b/example/integration_test/periodic_trigger_reproduction_test.dart
@@ -0,0 +1,264 @@
+// ignore_for_file: avoid_print
+// ============================================================
+// Periodic Trigger Reproduction & Regression Tests
+// ============================================================
+//
+// Covers all valid TaskTrigger.periodic parameter combinations,
+// including Issue #26 (initialDelay + runImmediately: false).
+//
+// Run on a real device or emulator:
+// cd example && flutter test integration_test/periodic_trigger_reproduction_test.dart
+// ============================================================
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:native_workmanager/native_workmanager.dart';
+
+/// Unique task IDs to avoid collisions across test runs.
+String _id(String name) =>
+ 'prt_${name}_${DateTime.now().millisecondsSinceEpoch}';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ setUpAll(() async {
+ await NativeWorkManager.initialize();
+ // Cancel any leftover tasks from previous runs.
+ await NativeWorkManager.cancelAll();
+ });
+
+ tearDownAll(() async {
+ await NativeWorkManager.cancelAll();
+ });
+
+ group('Periodic Trigger – parameter acceptance', () {
+ // ──────────────────────────────────────────────────────────
+ // REGRESSION – Issue #26
+ // Previously an AssertionError was thrown for this combo because
+ // "initialDelay + runImmediately: false seemed contradictory".
+ // Both params are independently valid; the OS decides ordering.
+ // ──────────────────────────────────────────────────────────
+ testWidgets(
+ 'issue_26: periodic with initialDelay + runImmediately:false is accepted',
+ (tester) async {
+ final id = _id('issue_26');
+
+ final result = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: TaskTrigger.periodic(
+ const Duration(minutes: 15),
+ initialDelay: const Duration(minutes: 5),
+ runImmediately: false,
+ ),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ );
+
+ expect(
+ result.scheduleResult,
+ ScheduleResult.accepted,
+ reason:
+ 'issue_26: periodic with initialDelay + runImmediately:false must be accepted',
+ );
+
+ final status = await NativeWorkManager.getTaskStatus(taskId: id);
+ expect(status, isNotNull, reason: 'Task must be tracked after enqueue');
+
+ await NativeWorkManager.cancel(taskId: id);
+ },
+ );
+
+ // ──────────────────────────────────────────────────────────
+ // Baseline: minimum valid periodic task (15-min interval only)
+ // ──────────────────────────────────────────────────────────
+ testWidgets('periodic – minimum interval (15 min) is accepted',
+ (tester) async {
+ final id = _id('min_interval');
+
+ final result = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: const TaskTrigger.periodic(Duration(minutes: 15)),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ );
+
+ expect(
+ result.scheduleResult,
+ ScheduleResult.accepted,
+ reason: 'Minimum 15-min periodic must be accepted',
+ );
+
+ await NativeWorkManager.cancel(taskId: id);
+ });
+
+ // ──────────────────────────────────────────────────────────
+ // flexInterval: tests the KMP bridge flexMs conversion.
+ // A wrong cast (flexMs as? KotlinLong) silently returns nil —
+ // the KotlinLong(value:) constructor is the correct path.
+ // ──────────────────────────────────────────────────────────
+ testWidgets('periodic – with flexInterval is accepted', (tester) async {
+ final id = _id('flex');
+
+ final result = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: TaskTrigger.periodic(
+ const Duration(minutes: 15),
+ flexInterval: const Duration(minutes: 5),
+ ),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ );
+
+ expect(
+ result.scheduleResult,
+ ScheduleResult.accepted,
+ reason: 'Periodic with flexInterval must be accepted',
+ );
+
+ await NativeWorkManager.cancel(taskId: id);
+ });
+
+ // ──────────────────────────────────────────────────────────
+ // initialDelay alone (no runImmediately override)
+ // ──────────────────────────────────────────────────────────
+ testWidgets('periodic – with initialDelay is accepted', (tester) async {
+ final id = _id('initial_delay');
+
+ final result = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: TaskTrigger.periodic(
+ const Duration(minutes: 15),
+ initialDelay: const Duration(minutes: 1),
+ ),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ );
+
+ expect(
+ result.scheduleResult,
+ ScheduleResult.accepted,
+ reason: 'Periodic with initialDelay must be accepted',
+ );
+
+ await NativeWorkManager.cancel(taskId: id);
+ });
+
+ // ──────────────────────────────────────────────────────────
+ // runImmediately: true (explicit, should behave same as default)
+ // ──────────────────────────────────────────────────────────
+ testWidgets('periodic – with runImmediately:true is accepted',
+ (tester) async {
+ final id = _id('run_immediately');
+
+ final result = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: TaskTrigger.periodic(
+ const Duration(minutes: 15),
+ runImmediately: true,
+ ),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ );
+
+ expect(
+ result.scheduleResult,
+ ScheduleResult.accepted,
+ reason: 'Periodic with runImmediately:true must be accepted',
+ );
+
+ await NativeWorkManager.cancel(taskId: id);
+ });
+
+ // ──────────────────────────────────────────────────────────
+ // All params combined: interval + flex + initialDelay + runImmediately:false
+ // This is the most complete form and must not be blocked by any assert.
+ // ──────────────────────────────────────────────────────────
+ testWidgets(
+ 'issue_26: periodic with all params (interval+flex+initialDelay+runImmediately:false) is accepted',
+ (tester) async {
+ final id = _id('all_params');
+
+ final result = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: TaskTrigger.periodic(
+ const Duration(minutes: 15),
+ flexInterval: const Duration(minutes: 5),
+ initialDelay: const Duration(minutes: 1),
+ runImmediately: false,
+ ),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ );
+
+ expect(
+ result.scheduleResult,
+ ScheduleResult.accepted,
+ reason:
+ 'issue_26: periodic with all params must be accepted without error',
+ );
+
+ final status = await NativeWorkManager.getTaskStatus(taskId: id);
+ expect(status, isNotNull,
+ reason: 'Task must be tracked after full-param enqueue');
+
+ await NativeWorkManager.cancel(taskId: id);
+ },
+ );
+
+ // ──────────────────────────────────────────────────────────
+ // Cancel verification: task must disappear from tracking after cancel.
+ // Protects against state leaks between test runs.
+ // ──────────────────────────────────────────────────────────
+ testWidgets('periodic – cancel removes task from tracking', (tester) async {
+ final id = _id('cancel_verify');
+
+ final result = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: const TaskTrigger.periodic(Duration(minutes: 15)),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ );
+
+ expect(result.scheduleResult, ScheduleResult.accepted);
+
+ await NativeWorkManager.cancel(taskId: id);
+
+ // After cancel, task must not be in a runnable/pending state.
+ final status = await NativeWorkManager.getTaskStatus(taskId: id);
+ expect(
+ status,
+ anyOf(isNull, equals(TaskStatus.cancelled)),
+ reason: 'Cancelled periodic task must not remain pending',
+ );
+ });
+
+ // ──────────────────────────────────────────────────────────
+ // ExistingPolicy.replace: re-enqueueing the same ID replaces it.
+ // ──────────────────────────────────────────────────────────
+ testWidgets('periodic – REPLACE policy updates existing task',
+ (tester) async {
+ final id = _id('policy_replace');
+
+ // First enqueue
+ final r1 = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: const TaskTrigger.periodic(Duration(minutes: 15)),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ existingPolicy: ExistingTaskPolicy.keep,
+ );
+ expect(r1.scheduleResult, ScheduleResult.accepted);
+
+ // Replace with a different flex window
+ final r2 = await NativeWorkManager.enqueue(
+ taskId: id,
+ trigger: TaskTrigger.periodic(
+ const Duration(minutes: 15),
+ flexInterval: const Duration(minutes: 5),
+ ),
+ worker: NativeWorker.httpSync(url: 'https://example.com/sync'),
+ existingPolicy: ExistingTaskPolicy.replace,
+ );
+
+ expect(
+ r2.scheduleResult,
+ ScheduleResult.accepted,
+ reason: 'REPLACE on an existing periodic task must be accepted',
+ );
+
+ await NativeWorkManager.cancel(taskId: id);
+ });
+ });
+}
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index 1dc6cf7..391a902 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -20,7 +20,5 @@
????
CFBundleVersion
1.0
- MinimumOSVersion
- 13.0
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 47d771e..7b98781 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -2,7 +2,7 @@ PODS:
- Flutter (1.0.0)
- integration_test (0.0.1):
- Flutter
- - native_workmanager (1.2.1):
+ - native_workmanager (1.2.4):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
@@ -32,7 +32,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
- native_workmanager: caf01cc52808bde88cc17e0fa96d85cddb15c4d4
+ native_workmanager: 80ddfef9e4b768fbc19a4540c5f1a4290959c4a0
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
diff --git a/ios/native_workmanager/Sources/native_workmanager/engine/FlutterEngineManager.swift b/ios/native_workmanager/Sources/native_workmanager/engine/FlutterEngineManager.swift
index 71ebd67..f4d996a 100644
--- a/ios/native_workmanager/Sources/native_workmanager/engine/FlutterEngineManager.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/engine/FlutterEngineManager.swift
@@ -121,9 +121,9 @@ class FlutterEngineManager {
let initTime = Date().timeIntervalSince(startTime)
if !wasEngineAlive {
- print("FlutterEngineManager: Cold start - Engine initialized in \(Int(initTime * 1000))ms")
+ NativeLogger.d("FlutterEngineManager: Cold start - Engine initialized in \(Int(initTime * 1000))ms")
} else {
- print("FlutterEngineManager: Warm start - Engine already alive")
+ NativeLogger.d("FlutterEngineManager: Warm start - Engine already alive")
}
// Execute callback with timeout
@@ -148,10 +148,10 @@ class FlutterEngineManager {
group.cancelAll()
let totalTime = Date().timeIntervalSince(startTime)
- print("FlutterEngineManager: Callback (handle: \(callbackHandle)) completed in \(Int(totalTime * 1000))ms")
+ NativeLogger.d("FlutterEngineManager: Callback (handle: \(callbackHandle)) completed in \(Int(totalTime * 1000))ms")
if disposeImmediately {
- print("FlutterEngineManager: Aggressively disposing engine to free RAM")
+ NativeLogger.d("FlutterEngineManager: Aggressively disposing engine to free RAM")
dispose()
} else {
// Original behavior: Keep engine alive for 5 minutes
@@ -168,7 +168,7 @@ class FlutterEngineManager {
// Dart isolate is hung — dispose the engine so the next task gets a fresh start.
// Without this, isInitialized stays true and subsequent DartWorker tasks inherit
// a hung MethodChannel whose invokeMethod replies will never arrive.
- print("FlutterEngineManager: Callback timed out — disposing hung engine")
+ NativeLogger.d("FlutterEngineManager: Callback timed out — disposing hung engine")
dispose()
throw FlutterEngineError.timeout
}
@@ -184,7 +184,7 @@ class FlutterEngineManager {
/// Dispose internals. Must be called with `queue` already held — never acquires the lock itself.
private func _disposeInternal() {
- print("FlutterEngineManager: Disposing engine...")
+ NativeLogger.d("FlutterEngineManager: Disposing engine...")
methodChannel?.setMethodCallHandler(nil)
methodChannel = nil
engine = nil
@@ -192,7 +192,7 @@ class FlutterEngineManager {
disposalWorkItem?.cancel()
disposalWorkItem = nil
lastUsedTimestamp = nil
- print("FlutterEngineManager: Engine disposed")
+ NativeLogger.d("FlutterEngineManager: Engine disposed")
}
// MARK: - Private Methods
@@ -210,7 +210,7 @@ class FlutterEngineManager {
let saved = UserDefaults.standard.object(forKey: FlutterEngineManager.callbackHandleKey) as? Int64
if let s = saved {
self.callbackHandle = s
- print("FlutterEngineManager: Restored callback handle from storage: \(s)")
+ NativeLogger.d("FlutterEngineManager: Restored callback handle from storage: \(s)")
}
}
}
@@ -250,7 +250,7 @@ class FlutterEngineManager {
return
}
- print("FlutterEngineManager: Initializing Flutter Engine...")
+ NativeLogger.d("FlutterEngineManager: Initializing Flutter Engine...")
// Create engine
let engine = FlutterEngine(name: "native_workmanager_background")
@@ -316,7 +316,7 @@ class FlutterEngineManager {
if call.method == "dartReady" {
result(nil)
- print("FlutterEngineManager: Dart side ready")
+ NativeLogger.d("FlutterEngineManager: Dart side ready")
// All writes to isReady AND continuation-resume happen on self.queue (serial).
self.queue.async {
@@ -355,7 +355,7 @@ class FlutterEngineManager {
guard let self = self else { return }
if !isReady {
- print("FlutterEngineManager: Timeout waiting for Dart ready signal")
+ NativeLogger.d("FlutterEngineManager: Timeout waiting for Dart ready signal")
self.completeInitialization(error: FlutterEngineError.dartReadyTimeout)
}
}
@@ -399,7 +399,7 @@ class FlutterEngineManager {
]
channel.invokeMethod("executeCallback", arguments: args) { result in
if let error = result as? FlutterError {
- print("FlutterEngineManager: Callback error: \(error.message ?? "unknown")")
+ NativeLogger.e("FlutterEngineManager: Callback error: \(error.message ?? "unknown")")
continuation.resume(returning: false)
} else if let success = result as? Bool {
continuation.resume(returning: success)
@@ -455,7 +455,7 @@ class FlutterEngineManager {
let idleTime = Date().timeIntervalSince(lastUsed)
if idleTime >= FlutterEngineManager.idleTimeoutSeconds {
- print("FlutterEngineManager: Auto-disposing after \(Int(idleTime))s idle")
+ NativeLogger.d("FlutterEngineManager: Auto-disposing after \(Int(idleTime))s idle")
// Call _disposeInternal() directly — queue is already held here,
// so calling dispose() (which does queue.sync) would deadlock.
self._disposeInternal()
diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/DartCallbackWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/DartCallbackWorker.swift
index 3daf8b8..691f9c1 100644
--- a/ios/native_workmanager/Sources/native_workmanager/workers/DartCallbackWorker.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/workers/DartCallbackWorker.swift
@@ -60,13 +60,13 @@ class DartCallbackWorker: IosWorker {
func doWork(input: String?, env: KMPWorkManager.WorkerEnvironment) async throws -> WorkerResult {
guard let input = input, !input.isEmpty else {
- print("DartCallbackWorker: Error - Empty or null input")
+ NativeLogger.e("DartCallbackWorker: Error - Empty or null input")
return WorkerResult.failure(message: "Empty or null input")
}
// Parse configuration
guard let data = input.data(using: .utf8) else {
- print("DartCallbackWorker: Error - Invalid UTF-8 encoding")
+ NativeLogger.e("DartCallbackWorker: Error - Invalid UTF-8 encoding")
return WorkerResult.failure(message: "Invalid input encoding")
}
@@ -74,11 +74,11 @@ class DartCallbackWorker: IosWorker {
do {
config = try JSONDecoder().decode(Config.self, from: data)
} catch {
- print("DartCallbackWorker: Error parsing JSON config: \(error)")
+ NativeLogger.e("DartCallbackWorker: Error parsing JSON config: \(error)")
return WorkerResult.failure(message: "Error parsing JSON config: \(error.localizedDescription)")
}
- print("DartCallbackWorker: Executing callback '\(config.callbackId)' (handle: \(config.callbackHandle), autoDispose: \(config.shouldAutoDispose))")
+ NativeLogger.d("DartCallbackWorker: Executing callback '\(config.callbackId)' (handle: \(config.callbackHandle), autoDispose: \(config.shouldAutoDispose))")
let startTime = Date()
let engineWasAlive = FlutterEngineManager.shared.isEngineAlive
@@ -97,18 +97,18 @@ class DartCallbackWorker: IosWorker {
if result {
if engineWasAlive {
- print("DartCallbackWorker: Success (warm start) - \(Int(executionTime * 1000))ms")
+ NativeLogger.d("DartCallbackWorker: Success (warm start) - \(Int(executionTime * 1000))ms")
} else {
- print("DartCallbackWorker: Success (cold start) - \(Int(executionTime * 1000))ms")
+ NativeLogger.d("DartCallbackWorker: Success (cold start) - \(Int(executionTime * 1000))ms")
}
return WorkerResult.success(message: "Callback returned true")
} else {
- print("DartCallbackWorker: Failed - Callback returned false")
+ NativeLogger.e("DartCallbackWorker: Failed - Callback returned false")
return WorkerResult.failure(message: "Callback returned false")
}
} catch {
let executionTime = Date().timeIntervalSince(startTime)
- print("DartCallbackWorker: Error - \(error.localizedDescription) (\(Int(executionTime * 1000))ms)")
+ NativeLogger.e("DartCallbackWorker: Error - \(error.localizedDescription) (\(Int(executionTime * 1000))ms)")
return WorkerResult.failure(message: "Execution error: \(error.localizedDescription)")
}
}
diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/HttpDownloadWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/HttpDownloadWorker.swift
index e7acc5b..5b6c39c 100644
--- a/ios/native_workmanager/Sources/native_workmanager/workers/HttpDownloadWorker.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/workers/HttpDownloadWorker.swift
@@ -48,7 +48,7 @@ class HttpDownloadWorker: IosWorker {
func stop() {
isStopped = true
currentDownloadTask?.cancel()
- print("HttpDownloadWorker: Stop signal received, download cancelled.")
+ NativeLogger.d("HttpDownloadWorker: Stop signal received, download cancelled.")
}
struct Config: Codable {
@@ -62,22 +62,15 @@ class HttpDownloadWorker: IosWorker {
let useBackgroundSession: Bool?
let skipExisting: Bool?
- // Sprint 1 - Feature 5: Advanced file handling
let onDuplicate: String? // "overwrite" (default) | "rename" | "skip"
let moveToPublicDownloads: Bool? // Copy to iOS Downloads folder after download
let saveToGallery: Bool? // Save image/video to Photos library after download
let extractAfterDownload: Bool? // Extract archive after download
let extractPath: String? // Destination directory for extraction
let deleteArchiveAfterExtract: Bool? // Delete archive after successful extraction
-
- // Sprint 3 - Cookie support
let cookies: [String: String]?
-
- // Sprint 3 - Auth layer
let authToken: String?
let authHeaderTemplate: String? // Default: "Bearer {accessToken}"
-
- // T3-6 - Bandwidth throttling (bytes/s; nil = no limit)
let bandwidthLimitBytesPerSecond: Int64?
var timeout: TimeInterval {
@@ -107,13 +100,13 @@ class HttpDownloadWorker: IosWorker {
func doWork(input: String?, env: KMPWorkManager.WorkerEnvironment) async throws -> WorkerResult {
guard let input = input, !input.isEmpty else {
- print("HttpDownloadWorker: Error - Empty or null input")
+ NativeLogger.e("HttpDownloadWorker: Error - Empty or null input")
return .failure(message: "Empty or null input")
}
// Parse configuration
guard let data = input.data(using: .utf8) else {
- print("HttpDownloadWorker: Error - Invalid UTF-8 encoding")
+ NativeLogger.e("HttpDownloadWorker: Error - Invalid UTF-8 encoding")
return .failure(message: "Invalid input encoding")
}
@@ -127,14 +120,14 @@ class HttpDownloadWorker: IosWorker {
do {
config = try JSONDecoder().decode(Config.self, from: data)
} catch {
- print("HttpDownloadWorker: Error parsing JSON config: \(error)")
+ NativeLogger.e("HttpDownloadWorker: Error parsing JSON config: \(error)")
return .failure(message: "Invalid JSON config: \(error.localizedDescription)")
}
// Skip download if destination already exists and skipExisting is enabled
// (only for non-directory paths where we already know the final filename)
if config.shouldSkipExisting && !config.isDirectory && FileManager.default.fileExists(atPath: config.savePath) {
- print("HttpDownloadWorker: skipExisting=true and file already exists — skipping")
+ NativeLogger.w("HttpDownloadWorker: skipExisting=true and file already exists — skipping")
let size = (try? FileManager.default.attributesOfItem(atPath: config.savePath))?[.size] as? Int64 ?? 0
return .success(
message: "File already exists, download skipped",
@@ -146,7 +139,7 @@ class HttpDownloadWorker: IosWorker {
if !config.isDirectory {
let duplicate = config.effectiveOnDuplicate
if duplicate == "skip" && FileManager.default.fileExists(atPath: config.savePath) {
- print("HttpDownloadWorker: onDuplicate=skip and file exists — skipping")
+ NativeLogger.w("HttpDownloadWorker: onDuplicate=skip and file exists — skipping")
let size = (try? FileManager.default.attributesOfItem(atPath: config.savePath))?[.size] as? Int64 ?? 0
return .success(
message: "File already exists, download skipped (onDuplicate=skip)",
@@ -163,13 +156,13 @@ class HttpDownloadWorker: IosWorker {
// Validate URL scheme (prevent file://, ftp://, etc.)
guard let url = SecurityValidator.validateURL(config.url) else {
- print("HttpDownloadWorker: Error - Invalid or unsafe URL")
+ NativeLogger.e("HttpDownloadWorker: Error - Invalid or unsafe URL")
return .failure(message: "Invalid or unsafe URL")
}
// Validate file path is within app sandbox
guard SecurityValidator.validateFilePath(config.savePath) else {
- print("HttpDownloadWorker: Error - File path outside app sandbox")
+ NativeLogger.e("HttpDownloadWorker: Error - File path outside app sandbox")
return .failure(message: "File path outside app sandbox")
}
@@ -192,9 +185,9 @@ class HttpDownloadWorker: IosWorker {
do {
try FileManager.default.createDirectory(at: parentDir,
withIntermediateDirectories: true)
- print("HttpDownloadWorker: Created directory: \(parentDir.path)")
+ NativeLogger.d("HttpDownloadWorker: Created directory: \(parentDir.path)")
} catch {
- print("HttpDownloadWorker: Error creating directory: \(error)")
+ NativeLogger.e("HttpDownloadWorker: Error creating directory: \(error)")
return .failure(message: "Failed to create directory: \(error.localizedDescription)")
}
}
@@ -214,13 +207,13 @@ class HttpDownloadWorker: IosWorker {
let attributes = try FileManager.default.attributesOfItem(atPath: tempURL.path)
if let fileSize = attributes[.size] as? Int64, fileSize > 0 {
existingBytes = fileSize
- print("HttpDownloadWorker: Found existing partial download: \(existingBytes) bytes")
+ NativeLogger.d("HttpDownloadWorker: Found existing partial download: \(existingBytes) bytes")
} else {
// Delete empty temp file
try? FileManager.default.removeItem(at: tempURL)
}
} catch {
- print("HttpDownloadWorker: Error reading temp file: \(error)")
+ NativeLogger.e("HttpDownloadWorker: Error reading temp file: \(error)")
try? FileManager.default.removeItem(at: tempURL)
}
} else if FileManager.default.fileExists(atPath: tempURL.path) {
@@ -230,8 +223,8 @@ class HttpDownloadWorker: IosWorker {
// Sanitize URL for logging
let sanitizedURL = SecurityValidator.sanitizedURL(config.url)
- print("HttpDownloadWorker: Downloading \(sanitizedURL)")
- print(" Save to: \(destinationURL.lastPathComponent)")
+ NativeLogger.d("HttpDownloadWorker: Downloading \(sanitizedURL)")
+ NativeLogger.d(" Save to: \(destinationURL.lastPathComponent)")
// Build request
var request = URLRequest(url: url)
@@ -249,7 +242,7 @@ class HttpDownloadWorker: IosWorker {
request.setValue(stored.trimmingCharacters(in: .whitespacesAndNewlines),
forHTTPHeaderField: HttpConstants.headerIfRange)
}
- print("HttpDownloadWorker: Resuming download from byte \(existingBytes)")
+ NativeLogger.d("HttpDownloadWorker: Resuming download from byte \(existingBytes)")
}
// Add custom headers
@@ -258,27 +251,19 @@ class HttpDownloadWorker: IosWorker {
request.setValue(value, forHTTPHeaderField: key)
}
}
-
- // Sprint 3: Cookie support
if let cookies = config.cookies, !cookies.isEmpty {
let cookieHeader = cookies.map { "\($0.key)=\($0.value)" }.joined(separator: "; ")
request.addValue(cookieHeader, forHTTPHeaderField: HttpConstants.headerCookie)
}
-
- // Sprint 3: Auth layer
if let authToken = config.authToken {
let headerValue = config.effectiveAuthHeaderTemplate
.replacingOccurrences(of: "{accessToken}", with: authToken)
request.addValue(headerValue, forHTTPHeaderField: HttpConstants.headerAuthorization)
}
-
- // T3-7: HMAC-SHA256 request signing
let rawDictForSigning = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])
if let signingCfg = RequestSigner.Config.from(rawDictForSigning?["requestSigning"] as? [String: Any]) {
RequestSigner.sign(request: &request, config: signingCfg)
}
-
- // Sprint 2: per-host concurrency — block here until a permit is available.
// The permit is released after the download completes (success, failure, or skip).
let host = URL(string: config.url)?.host ?? config.url
HostConcurrencyManager.shared.acquire(host: host)
@@ -296,8 +281,6 @@ class HttpDownloadWorker: IosWorker {
existingBytes: existingBytes
)
}
-
- // T3-6: Throttled foreground download (iOS 15+) when bandwidth limit is configured
if #available(iOS 15.0, *),
let bwLimit = config.bandwidthLimitBytesPerSecond, bwLimit > 0 {
return await throttledForegroundDownload(
@@ -310,7 +293,7 @@ class HttpDownloadWorker: IosWorker {
taskIdForProgress: taskIdForProgress
)
} else if let bwLimit = config.bandwidthLimitBytesPerSecond, bwLimit > 0 {
- print("HttpDownloadWorker: bandwidth throttling requires iOS 15+, proceeding unthrottled")
+ NativeLogger.d("HttpDownloadWorker: bandwidth throttling requires iOS 15+, proceeding unthrottled")
}
// Execute download using foreground session (iOS 13+ compatible)
@@ -333,7 +316,7 @@ class HttpDownloadWorker: IosWorker {
// Handle errors
if let error = error {
- print("HttpDownloadWorker: Error - \(error.localizedDescription)")
+ NativeLogger.e("HttpDownloadWorker: Error - \(error.localizedDescription)")
try? FileManager.default.removeItem(at: tempURL)
continuation.resume(returning: .failure(message: error.localizedDescription))
return
@@ -341,7 +324,7 @@ class HttpDownloadWorker: IosWorker {
guard let location = location,
let httpResponse = response as? HTTPURLResponse else {
- print("HttpDownloadWorker: Error - Invalid response")
+ NativeLogger.e("HttpDownloadWorker: Error - Invalid response")
continuation.resume(returning: .failure(message: "Invalid response"))
return
}
@@ -355,7 +338,7 @@ class HttpDownloadWorker: IosWorker {
// 416 Range Not Satisfiable: our .tmp is stale (server file changed). Delete and signal retry.
if statusCode == HttpConstants.rangeNotSatisfiable {
- print("HttpDownloadWorker: 416 Range Not Satisfiable — deleting stale .tmp, restart on retry")
+ NativeLogger.d("HttpDownloadWorker: 416 Range Not Satisfiable — deleting stale .tmp, restart on retry")
try? FileManager.default.removeItem(at: tempURL)
try? FileManager.default.removeItem(atPath: tempURL.path + HttpConstants.etagSidecarSuffix)
continuation.resume(returning: .failure(
@@ -365,7 +348,7 @@ class HttpDownloadWorker: IosWorker {
}
if !isPartialContent && !isFullContent {
- print("HttpDownloadWorker: Failed - Status \(statusCode)")
+ NativeLogger.e("HttpDownloadWorker: Failed - Status \(statusCode)")
try? FileManager.default.removeItem(at: location)
continuation.resume(returning: .failure(message: "HTTP \(statusCode)"))
return
@@ -375,7 +358,7 @@ class HttpDownloadWorker: IosWorker {
let contentLength = httpResponse.expectedContentLength
if contentLength > 0 {
if !SecurityValidator.validateContentLength(contentLength) {
- print("HttpDownloadWorker: Error - Content too large")
+ NativeLogger.e("HttpDownloadWorker: Error - Content too large")
try? FileManager.default.removeItem(at: location)
continuation.resume(returning: .failure(message: "Download size exceeds limit"))
return
@@ -383,7 +366,7 @@ class HttpDownloadWorker: IosWorker {
// Check disk space
if !SecurityValidator.hasEnoughDiskSpace(requiredBytes: contentLength, targetURL: destinationURL) {
- print("HttpDownloadWorker: Error - Insufficient disk space")
+ NativeLogger.e("HttpDownloadWorker: Error - Insufficient disk space")
try? FileManager.default.removeItem(at: location)
continuation.resume(returning: .failure(message: "Insufficient disk space"))
return
@@ -392,14 +375,12 @@ class HttpDownloadWorker: IosWorker {
// Log resume status
if isResumingDownload {
- print("HttpDownloadWorker: Resume confirmed - Server sent 206 Partial Content")
+ NativeLogger.d("HttpDownloadWorker: Resume confirmed - Server sent 206 Partial Content")
} else if existingBytes > 0 && statusCode == HttpConstants.httpOk {
- print("HttpDownloadWorker: Server doesn't support resume - Starting from beginning")
+ NativeLogger.d("HttpDownloadWorker: Server doesn't support resume - Starting from beginning")
try? FileManager.default.removeItem(at: tempURL)
try? FileManager.default.removeItem(atPath: tempURL.path + HttpConstants.etagSidecarSuffix)
}
-
- // Feature 4: Resolve filename from Content-Disposition or URL when savePath is a directory
let cdHeader = httpResponse.value(forHTTPHeaderField: "Content-Disposition")
let serverSuggestedName: String? = self.parseFilenameFromContentDisposition(cdHeader)
if config.isDirectory {
@@ -410,11 +391,11 @@ class HttpDownloadWorker: IosWorker {
// LOGIC-001: keep tempURL as the sentinel file in directory mode so that partial
// download data is preserved across retries. Only destinationURL changes here;
// the final rename moves tempURL (sentinel) → destinationURL (resolved name).
- print("HttpDownloadWorker: Directory mode — resolved filename: \(name)")
+ NativeLogger.d("HttpDownloadWorker: Directory mode — resolved filename: \(name)")
// skipExisting check for directory mode (now we know actual path)
if config.shouldSkipExisting && FileManager.default.fileExists(atPath: destinationURL.path) {
- print("HttpDownloadWorker: skipExisting=true and resolved file exists — skipping")
+ NativeLogger.w("HttpDownloadWorker: skipExisting=true and resolved file exists — skipping")
let size = (try? FileManager.default.attributesOfItem(atPath: destinationURL.path))?[.size] as? Int64 ?? 0
try? FileManager.default.removeItem(at: location)
var skipData: [String: Any] = ["filePath": destinationURL.path, "fileSize": size, "skipped": true]
@@ -477,26 +458,26 @@ class HttpDownloadWorker: IosWorker {
// Verify checksum if expected checksum is provided
if let expectedChecksum = config.expectedChecksum {
- print("HttpDownloadWorker: Verifying checksum with \(config.effectiveChecksumAlgorithm)...")
+ NativeLogger.d("HttpDownloadWorker: Verifying checksum with \(config.effectiveChecksumAlgorithm)...")
guard let actualChecksum = self.calculateChecksum(fileURL: tempURL, algorithm: config.effectiveChecksumAlgorithm, taskId: taskIdForProgress) else {
- print("HttpDownloadWorker: Error - Failed to calculate checksum")
+ NativeLogger.e("HttpDownloadWorker: Error - Failed to calculate checksum")
try? FileManager.default.removeItem(at: tempURL)
continuation.resume(returning: .failure(message: "Failed to calculate checksum"))
return
}
if actualChecksum.caseInsensitiveCompare(expectedChecksum) != .orderedSame {
- print("HttpDownloadWorker: Checksum verification failed!")
- print(" Expected: \(expectedChecksum)")
- print(" Actual: \(actualChecksum)")
- print(" Algorithm: \(config.effectiveChecksumAlgorithm)")
+ NativeLogger.e("HttpDownloadWorker: Checksum verification failed!")
+ NativeLogger.d(" Expected: \(expectedChecksum)")
+ NativeLogger.d(" Actual: \(actualChecksum)")
+ NativeLogger.d(" Algorithm: \(config.effectiveChecksumAlgorithm)")
try? FileManager.default.removeItem(at: tempURL)
continuation.resume(returning: .failure(message: "Checksum verification failed (expected: \(expectedChecksum), actual: \(actualChecksum))"))
return
}
- print("HttpDownloadWorker: Checksum verified: \(actualChecksum)")
+ NativeLogger.d("HttpDownloadWorker: Checksum verified: \(actualChecksum)")
}
// Capture content type and final URL
@@ -517,8 +498,8 @@ class HttpDownloadWorker: IosWorker {
// Clean up ETag sidecar after successful download
try? FileManager.default.removeItem(atPath: tempURL.path + HttpConstants.etagSidecarSuffix)
- print("HttpDownloadWorker: Success - Downloaded \(finalFileSize) bytes")
- print("HttpDownloadWorker: Saved to: \(destinationURL.path)")
+ NativeLogger.d("HttpDownloadWorker: Success - Downloaded \(finalFileSize) bytes")
+ NativeLogger.d("HttpDownloadWorker: Saved to: \(destinationURL.path)")
// Post-download actions
self.performPostDownloadActions(config: config, filePath: destinationURL.path)
@@ -537,7 +518,7 @@ class HttpDownloadWorker: IosWorker {
data: resultData
))
} catch {
- print("HttpDownloadWorker: Error moving file - \(error.localizedDescription)")
+ NativeLogger.e("HttpDownloadWorker: Error moving file - \(error.localizedDescription)")
try? FileManager.default.removeItem(at: tempURL)
try? FileManager.default.removeItem(at: location)
continuation.resume(returning: .failure(message: "Failed to move file: \(error.localizedDescription)"))
@@ -765,26 +746,26 @@ class HttpDownloadWorker: IosWorker {
// Verify checksum if expected checksum is provided
if let expectedChecksum = config.expectedChecksum {
- print("HttpDownloadWorker: Verifying checksum with \(config.effectiveChecksumAlgorithm)...")
+ NativeLogger.d("HttpDownloadWorker: Verifying checksum with \(config.effectiveChecksumAlgorithm)...")
guard let actualChecksum = self.calculateChecksum(fileURL: location, algorithm: config.effectiveChecksumAlgorithm, taskId: nil) else {
- print("HttpDownloadWorker: Error - Failed to calculate checksum")
+ NativeLogger.e("HttpDownloadWorker: Error - Failed to calculate checksum")
try? FileManager.default.removeItem(at: location)
continuation.resume(returning: .failure(message: "Failed to calculate checksum"))
return
}
if actualChecksum.lowercased() != expectedChecksum.lowercased() {
- print("HttpDownloadWorker: Checksum verification failed!")
- print(" Expected: \(expectedChecksum)")
- print(" Actual: \(actualChecksum)")
- print(" Algorithm: \(config.effectiveChecksumAlgorithm)")
+ NativeLogger.e("HttpDownloadWorker: Checksum verification failed!")
+ NativeLogger.d(" Expected: \(expectedChecksum)")
+ NativeLogger.d(" Actual: \(actualChecksum)")
+ NativeLogger.d(" Algorithm: \(config.effectiveChecksumAlgorithm)")
try? FileManager.default.removeItem(at: location)
continuation.resume(returning: .failure(message: "Checksum verification failed (expected: \(expectedChecksum), actual: \(actualChecksum))"))
return
}
- print("HttpDownloadWorker: Checksum verified: \(actualChecksum)")
+ NativeLogger.d("HttpDownloadWorker: Checksum verified: \(actualChecksum)")
}
// Remove destination if exists
@@ -793,8 +774,8 @@ class HttpDownloadWorker: IosWorker {
// Move to final destination
try FileManager.default.moveItem(at: location, to: destinationURL)
- print("HttpDownloadWorker: Background download success - Downloaded \(finalFileSize) bytes")
- print("HttpDownloadWorker: Saved to: \(config.savePath)")
+ NativeLogger.d("HttpDownloadWorker: Background download success - Downloaded \(finalFileSize) bytes")
+ NativeLogger.d("HttpDownloadWorker: Saved to: \(config.savePath)")
// Return success with rich data
continuation.resume(returning: .success(
@@ -808,13 +789,13 @@ class HttpDownloadWorker: IosWorker {
]
))
} catch {
- print("HttpDownloadWorker: Error moving file - \(error.localizedDescription)")
+ NativeLogger.e("HttpDownloadWorker: Error moving file - \(error.localizedDescription)")
try? FileManager.default.removeItem(at: location)
continuation.resume(returning: .failure(message: "Failed to move file: \(error.localizedDescription)"))
}
case .failure(let error):
- print("HttpDownloadWorker: Background download failed - \(error.localizedDescription)")
+ NativeLogger.e("HttpDownloadWorker: Background download failed - \(error.localizedDescription)")
continuation.resume(returning: .failure(message: "Background download failed: \(error.localizedDescription)"))
}
}
@@ -859,9 +840,9 @@ class HttpDownloadWorker: IosWorker {
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: fileURL)
}, completionHandler: { success, error in
if let error = error {
- print("HttpDownloadWorker: saveToGallery image error: \(error.localizedDescription)")
+ NativeLogger.e("HttpDownloadWorker: saveToGallery image error: \(error.localizedDescription)")
} else if success {
- print("HttpDownloadWorker: Image saved to gallery")
+ NativeLogger.d("HttpDownloadWorker: Image saved to gallery")
}
})
} else if ["mp4", "mov", "m4v"].contains(ext) {
@@ -869,9 +850,9 @@ class HttpDownloadWorker: IosWorker {
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: fileURL)
}, completionHandler: { success, error in
if let error = error {
- print("HttpDownloadWorker: saveToGallery video error: \(error.localizedDescription)")
+ NativeLogger.e("HttpDownloadWorker: saveToGallery video error: \(error.localizedDescription)")
} else if success {
- print("HttpDownloadWorker: Video saved to gallery")
+ NativeLogger.d("HttpDownloadWorker: Video saved to gallery")
}
})
}
@@ -887,16 +868,16 @@ class HttpDownloadWorker: IosWorker {
try FileManager.default.removeItem(at: destURL)
}
try FileManager.default.copyItem(at: fileURL, to: destURL)
- print("HttpDownloadWorker: Copied to public Downloads: \(destURL.path)")
+ NativeLogger.d("HttpDownloadWorker: Copied to public Downloads: \(destURL.path)")
} catch {
- print("HttpDownloadWorker: moveToPublicDownloads error: \(error.localizedDescription)")
+ NativeLogger.e("HttpDownloadWorker: moveToPublicDownloads error: \(error.localizedDescription)")
}
}
}
// extractAfterDownload: disabled in v1.1.0 for Zero Dependencies
if config.effectiveExtractAfterDownload {
- print("HttpDownloadWorker: extractAfterDownload is disabled in v1.1.0 to achieve Zero Dependencies. Please use the Dart 'archive' package.")
+ NativeLogger.d("HttpDownloadWorker: extractAfterDownload is disabled in v1.1.0 to achieve Zero Dependencies. Please use the Dart 'archive' package.")
}
}
@@ -988,12 +969,12 @@ class HttpDownloadWorker: IosWorker {
return digest.map { String(format: "%02x", $0) }.joined()
default:
- print("HttpDownloadWorker: Unsupported checksum algorithm: \(algorithm)")
+ NativeLogger.d("HttpDownloadWorker: Unsupported checksum algorithm: \(algorithm)")
return nil
}
} else {
// Fallback for iOS < 13 (CryptoKit not available)
- print("HttpDownloadWorker: CryptoKit requires iOS 13+")
+ NativeLogger.d("HttpDownloadWorker: CryptoKit requires iOS 13+")
return nil
}
}
diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/HttpUploadWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/HttpUploadWorker.swift
index e27dc94..f968bfd 100644
--- a/ios/native_workmanager/Sources/native_workmanager/workers/HttpUploadWorker.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/workers/HttpUploadWorker.swift
@@ -146,13 +146,13 @@ class HttpUploadWorker: IosWorker {
func doWork(input: String?, env: KMPWorkManager.WorkerEnvironment) async throws -> WorkerResult {
guard let input = input, !input.isEmpty else {
- print("HttpUploadWorker: Error - Empty or null input")
+ NativeLogger.e("HttpUploadWorker: Error - Empty or null input")
return .failure(message: "Empty or null input")
}
// Parse configuration
guard let data = input.data(using: .utf8) else {
- print("HttpUploadWorker: Error - Invalid UTF-8 encoding")
+ NativeLogger.e("HttpUploadWorker: Error - Invalid UTF-8 encoding")
return .failure(message: "Invalid input encoding")
}
@@ -160,13 +160,13 @@ class HttpUploadWorker: IosWorker {
do {
config = try JSONDecoder().decode(Config.self, from: data)
} catch {
- print("HttpUploadWorker: Error parsing JSON config: \(error)")
+ NativeLogger.e("HttpUploadWorker: Error parsing JSON config: \(error)")
return .failure(message: "Invalid JSON config: \(error.localizedDescription)")
}
// Validate URL scheme (prevent file://, ftp://, etc.)
guard let url = SecurityValidator.validateURL(config.url) else {
- print("HttpUploadWorker: Error - Invalid or unsafe URL")
+ NativeLogger.e("HttpUploadWorker: Error - Invalid or unsafe URL")
return .failure(message: "Invalid or unsafe URL")
}
@@ -176,12 +176,12 @@ class HttpUploadWorker: IosWorker {
// Validate upload mode
if isRawBodyUpload && !fileConfigs.isEmpty {
- print("HttpUploadWorker: Error - Cannot mix raw body and file upload")
+ NativeLogger.e("HttpUploadWorker: Error - Cannot mix raw body and file upload")
return .failure(message: "Cannot use both body/bodyBytes and filePath/files")
}
if !isRawBodyUpload && fileConfigs.isEmpty {
- print("HttpUploadWorker: Error - No data to upload")
+ NativeLogger.e("HttpUploadWorker: Error - No data to upload")
return .failure(message: "No data to upload (provide body/bodyBytes or filePath/files)")
}
@@ -197,19 +197,19 @@ class HttpUploadWorker: IosWorker {
for fileConfig in fileConfigs {
// Validate file path
guard SecurityValidator.validateFilePath(fileConfig.filePath) else {
- print("HttpUploadWorker: Error - File path outside sandbox: \(fileConfig.filePath)")
+ NativeLogger.e("HttpUploadWorker: Error - File path outside sandbox: \(fileConfig.filePath)")
return .failure(message: "File path outside sandbox")
}
let fileURL = URL(fileURLWithPath: fileConfig.filePath)
guard FileManager.default.fileExists(atPath: fileConfig.filePath) else {
- print("HttpUploadWorker: Error - File not found: \(fileConfig.filePath)")
+ NativeLogger.e("HttpUploadWorker: Error - File not found: \(fileConfig.filePath)")
return .failure(message: "File not found: \(fileURL.lastPathComponent)")
}
// Validate file size
guard SecurityValidator.validateFileSize(fileURL) else {
- print("HttpUploadWorker: Error - File too large: \(fileConfig.filePath)")
+ NativeLogger.e("HttpUploadWorker: Error - File too large: \(fileConfig.filePath)")
return .failure(message: "File size exceeds limit")
}
@@ -220,7 +220,7 @@ class HttpUploadWorker: IosWorker {
totalSize += fileSize
}
} catch {
- print("HttpUploadWorker: Error reading file: \(error)")
+ NativeLogger.e("HttpUploadWorker: Error reading file: \(error)")
return .failure(message: "Failed to read file: \(fileURL.lastPathComponent)")
}
@@ -233,12 +233,12 @@ class HttpUploadWorker: IosWorker {
// Sanitize logging
let sanitizedURL = SecurityValidator.sanitizedURL(config.url)
- print("HttpUploadWorker: Uploading to \(sanitizedURL)")
- print(" Files: \(validatedFiles.count), Total Size: \(totalSize) bytes")
+ NativeLogger.d("HttpUploadWorker: Uploading to \(sanitizedURL)")
+ NativeLogger.d(" Files: \(validatedFiles.count), Total Size: \(totalSize) bytes")
for (index, (fileURL, fileName, mimeType)) in validatedFiles.enumerated() {
let fileAttributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path)
let fileSize = fileAttributes?[.size] as? Int64 ?? 0
- print(" [\(index)] \(fileName) (\(fileSize) bytes, \(mimeType))")
+ NativeLogger.d(" [\(index)] \(fileName) (\(fileSize) bytes, \(mimeType))")
}
// 🚀 Use background session if enabled (v2.3.0+)
@@ -276,12 +276,10 @@ class HttpUploadWorker: IosWorker {
do {
bodyTempURL = try buildMultipartBodyToFile(boundary: boundary, fields: config.fields, files: fileTuples)
} catch {
- print("HttpUploadWorker: Error building multipart body: \(error)")
+ NativeLogger.e("HttpUploadWorker: Error building multipart body: \(error)")
return .failure(message: "Failed to build upload body: \(error.localizedDescription)")
}
defer { try? FileManager.default.removeItem(at: bodyTempURL) }
-
- // T3-7: HMAC-SHA256 request signing
let uploadRawDict = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
if let signingDict = uploadRawDict?["requestSigning"] as? [String: Any],
let signingCfg: RequestSigner.Config = RequestSigner.Config.from(signingDict) {
@@ -299,13 +297,13 @@ class HttpUploadWorker: IosWorker {
let (data, response) = try await uploadSession.upload(for: request, fromFile: bodyTempURL)
guard let httpResponse = response as? HTTPURLResponse else {
- print("HttpUploadWorker: Error - Invalid response type")
+ NativeLogger.e("HttpUploadWorker: Error - Invalid response type")
return .failure(message: "Invalid response type")
}
// Validate response body size
guard SecurityValidator.validateResponseSize(data) else {
- print("HttpUploadWorker: Error - Response body too large")
+ NativeLogger.e("HttpUploadWorker: Error - Response body too large")
return .failure(message: "Response body too large")
}
@@ -316,8 +314,8 @@ class HttpUploadWorker: IosWorker {
if success {
// Truncate response for logging
let truncatedResponse = SecurityValidator.truncateForLogging(responseBody, maxLength: 200)
- print("HttpUploadWorker: Success - Status \(statusCode)")
- print("HttpUploadWorker: Response: \(truncatedResponse)")
+ NativeLogger.d("HttpUploadWorker: Success - Status \(statusCode)")
+ NativeLogger.d("HttpUploadWorker: Response: \(truncatedResponse)")
return .success(
message: "Uploaded \(validatedFiles.count) file(s), \(totalSize) bytes",
@@ -332,8 +330,8 @@ class HttpUploadWorker: IosWorker {
} else {
// Truncate error body for logging
let truncatedError = SecurityValidator.truncateForLogging(responseBody, maxLength: 200)
- print("HttpUploadWorker: Failed - Status \(statusCode)")
- print("HttpUploadWorker: Error: \(truncatedError)")
+ NativeLogger.e("HttpUploadWorker: Failed - Status \(statusCode)")
+ NativeLogger.e("HttpUploadWorker: Error: \(truncatedError)")
// 401 + token refresh: attempt refresh and retry once
if statusCode == 401, let refreshConfig = uploadTokenRefreshConfig {
@@ -363,7 +361,7 @@ class HttpUploadWorker: IosWorker {
return .failure(message: "HTTP \(statusCode)", shouldRetry: statusCode >= 500)
}
} catch {
- print("HttpUploadWorker: Error - \(error.localizedDescription)")
+ NativeLogger.e("HttpUploadWorker: Error - \(error.localizedDescription)")
return .failure(message: error.localizedDescription, shouldRetry: true)
}
}
@@ -372,28 +370,28 @@ class HttpUploadWorker: IosWorker {
private func handleRawBodyUpload(config: Config, url: URL, rawInputData: Data) async -> WorkerResult {
// Validate content type is provided
guard let contentType = config.contentType, !contentType.isEmpty else {
- print("HttpUploadWorker: Error - contentType is required for raw body upload")
+ NativeLogger.e("HttpUploadWorker: Error - contentType is required for raw body upload")
return .failure(message: "contentType is required for raw body upload")
}
let sanitizedURL = SecurityValidator.sanitizedURL(config.url)
- print("HttpUploadWorker: Uploading raw body to \(sanitizedURL)")
- print(" Content-Type: \(contentType)")
+ NativeLogger.d("HttpUploadWorker: Uploading raw body to \(sanitizedURL)")
+ NativeLogger.d(" Content-Type: \(contentType)")
// Build request body
let requestBody: Data
if let body = config.body {
requestBody = body.data(using: .utf8) ?? Data()
- print(" Body: \(body.count) characters (\(requestBody.count) bytes)")
+ NativeLogger.d(" Body: \(body.count) characters (\(requestBody.count) bytes)")
} else if let bodyBytes = config.bodyBytes {
guard let decodedData = Data(base64Encoded: bodyBytes) else {
- print("HttpUploadWorker: Error - Failed to decode base64 bodyBytes")
+ NativeLogger.e("HttpUploadWorker: Error - Failed to decode base64 bodyBytes")
return .failure(message: "Invalid base64 bodyBytes")
}
requestBody = decodedData
- print(" Body: \(requestBody.count) bytes (from base64)")
+ NativeLogger.d(" Body: \(requestBody.count) bytes (from base64)")
} else {
- print("HttpUploadWorker: Error - No body or bodyBytes provided")
+ NativeLogger.e("HttpUploadWorker: Error - No body or bodyBytes provided")
return .failure(message: "No body or bodyBytes provided")
}
@@ -409,8 +407,6 @@ class HttpUploadWorker: IosWorker {
request.setValue(value, forHTTPHeaderField: key)
}
}
-
- // T3-7: HMAC-SHA256 request signing (raw body path)
let rawBodyDict = (try? JSONSerialization.jsonObject(with: rawInputData)) as? [String: Any]
if let signingDict = rawBodyDict?["requestSigning"] as? [String: Any],
let signingCfg: RequestSigner.Config = RequestSigner.Config.from(signingDict) {
@@ -428,13 +424,13 @@ class HttpUploadWorker: IosWorker {
let (data, response) = try await rawBodySession.upload(for: request, from: requestBody)
guard let httpResponse = response as? HTTPURLResponse else {
- print("HttpUploadWorker: Error - Invalid response type")
+ NativeLogger.e("HttpUploadWorker: Error - Invalid response type")
return .failure(message: "Invalid response type")
}
// Validate response body size
guard SecurityValidator.validateResponseSize(data) else {
- print("HttpUploadWorker: Error - Response body too large")
+ NativeLogger.e("HttpUploadWorker: Error - Response body too large")
return .failure(message: "Response body too large")
}
@@ -445,8 +441,8 @@ class HttpUploadWorker: IosWorker {
if success {
// Truncate response for logging
let truncatedResponse = SecurityValidator.truncateForLogging(responseBody, maxLength: 200)
- print("HttpUploadWorker: Success - Status \(statusCode)")
- print("HttpUploadWorker: Response: \(truncatedResponse)")
+ NativeLogger.d("HttpUploadWorker: Success - Status \(statusCode)")
+ NativeLogger.d("HttpUploadWorker: Response: \(truncatedResponse)")
return .success(
message: "Uploaded raw body",
@@ -460,8 +456,8 @@ class HttpUploadWorker: IosWorker {
} else {
// Truncate error body for logging
let truncatedError = SecurityValidator.truncateForLogging(responseBody, maxLength: 200)
- print("HttpUploadWorker: Failed - Status \(statusCode)")
- print("HttpUploadWorker: Error: \(truncatedError)")
+ NativeLogger.e("HttpUploadWorker: Failed - Status \(statusCode)")
+ NativeLogger.e("HttpUploadWorker: Error: \(truncatedError)")
// 401 + token refresh: attempt refresh and retry once
if statusCode == 401, let refreshConfig = rawBodyTokenRefreshConfig {
@@ -490,7 +486,7 @@ class HttpUploadWorker: IosWorker {
return .failure(message: "HTTP \(statusCode)", shouldRetry: statusCode >= 500)
}
} catch {
- print("HttpUploadWorker: Error - \(error.localizedDescription)")
+ NativeLogger.e("HttpUploadWorker: Error - \(error.localizedDescription)")
return .failure(message: error.localizedDescription, shouldRetry: true)
}
}
@@ -513,7 +509,7 @@ class HttpUploadWorker: IosWorker {
validatedFiles: [(url: URL, fileName: String, mimeType: String)],
totalSize: Int64
) async -> WorkerResult {
- print("HttpUploadWorker: Using background URLSession for upload")
+ NativeLogger.d("HttpUploadWorker: Using background URLSession for upload")
// NET-001/NET-004: Build multipart body by streaming files to a temp file (avoids OOM).
let fileConfigs = config.getFileConfigs()
@@ -524,7 +520,7 @@ class HttpUploadWorker: IosWorker {
do {
tempFileURL = try buildMultipartBodyToFile(boundary: boundary, fields: config.fields, files: fileTuplesBg)
} catch {
- print("HttpUploadWorker: Error building multipart body: \(error)")
+ NativeLogger.e("HttpUploadWorker: Error building multipart body: \(error)")
return .failure(message: "Failed to build upload body: \(error.localizedDescription)")
}
defer { try? FileManager.default.removeItem(at: tempFileURL) }
@@ -554,7 +550,7 @@ class HttpUploadWorker: IosWorker {
let success = (200..<300).contains(statusCode)
if success {
- print("HttpUploadWorker: Background upload succeeded - Status \(statusCode)")
+ NativeLogger.d("HttpUploadWorker: Background upload succeeded - Status \(statusCode)")
continuation.resume(returning: .success(
message: "Uploaded \(validatedFiles.count) file(s) via background session",
@@ -567,12 +563,12 @@ class HttpUploadWorker: IosWorker {
]
))
} else {
- print("HttpUploadWorker: Background upload failed - Status \(statusCode)")
+ NativeLogger.e("HttpUploadWorker: Background upload failed - Status \(statusCode)")
continuation.resume(returning: .failure(message: "HTTP \(statusCode)"))
}
case .failure(let error):
- print("HttpUploadWorker: Background upload failed: \(error.localizedDescription)")
+ NativeLogger.e("HttpUploadWorker: Background upload failed: \(error.localizedDescription)")
continuation.resume(returning: .failure(message: "Background upload failed: \(error.localizedDescription)"))
}
}
diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/ImageProcessWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/ImageProcessWorker.swift
index 87424fe..fd371de 100644
--- a/ios/native_workmanager/Sources/native_workmanager/workers/ImageProcessWorker.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/workers/ImageProcessWorker.swift
@@ -74,7 +74,7 @@ class ImageProcessWorker: IosWorker {
// Parse configuration
guard let data = input.data(using: .utf8) else {
- print("ImageProcessWorker: Error - Invalid UTF-8 encoding")
+ NativeLogger.e("ImageProcessWorker: Error - Invalid UTF-8 encoding")
return .failure(message: "Invalid input encoding")
}
@@ -136,7 +136,7 @@ class ImageProcessWorker: IosWorker {
return .failure(message: "Failed to crop image")
}
processedImage = croppedImage
- print("ImageProcessWorker: Cropped to: \(Int(processedImage.size.width))x\(Int(processedImage.size.height))")
+ NativeLogger.d("ImageProcessWorker: Cropped to: \(Int(processedImage.size.width))x\(Int(processedImage.size.height))")
}
// Resize if needed
@@ -149,7 +149,7 @@ class ImageProcessWorker: IosWorker {
maxHeight: CGFloat(maxHeight),
maintainAspectRatio: config.shouldMaintainAspectRatio
)
- print("ImageProcessWorker: Resized to: \(Int(processedImage.size.width))x\(Int(processedImage.size.height))")
+ NativeLogger.d("ImageProcessWorker: Resized to: \(Int(processedImage.size.width))x\(Int(processedImage.size.height))")
}
}
@@ -194,12 +194,12 @@ class ImageProcessWorker: IosWorker {
let processedSize = Int64(data.count)
let compressionRatio = originalSize > 0 ? String(format: "%.1f", (Float(processedSize) / Float(originalSize)) * 100) : "N/A"
- print("ImageProcessWorker: Processed image saved: \(processedSize) bytes (\(compressionRatio)% of original)")
+ NativeLogger.d("ImageProcessWorker: Processed image saved: \(processedSize) bytes (\(compressionRatio)% of original)")
// Delete original if requested
if config.shouldDeleteOriginal && inputURL.path != outputURL.path {
try? FileManager.default.removeItem(at: inputURL)
- print("ImageProcessWorker: Deleted original file")
+ NativeLogger.d("ImageProcessWorker: Deleted original file")
}
return .success(
@@ -234,7 +234,7 @@ class ImageProcessWorker: IosWorker {
let imageBounds = CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)
let intersection = cropRect.intersection(imageBounds)
guard !intersection.isEmpty else {
- print("ImageProcessWorker: Invalid crop rectangle")
+ NativeLogger.d("ImageProcessWorker: Invalid crop rectangle")
return nil
}
diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/IosWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/IosWorker.swift
index f11b5f8..c33ea29 100644
--- a/ios/native_workmanager/Sources/native_workmanager/workers/IosWorker.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/workers/IosWorker.swift
@@ -115,7 +115,7 @@ public class IosWorkerFactory {
case "PdfWorker":
return PdfWorker()
default:
- print("IosWorkerFactory: Unknown worker class: \(className)")
+ NativeLogger.d("IosWorkerFactory: Unknown worker class: \(className)")
return nil
}
}
diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/MoveToSharedStorageWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/MoveToSharedStorageWorker.swift
index 0832a7e..8fe2bc1 100644
--- a/ios/native_workmanager/Sources/native_workmanager/workers/MoveToSharedStorageWorker.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/workers/MoveToSharedStorageWorker.swift
@@ -70,14 +70,14 @@ class MoveToSharedStorageWorker: IosWorker {
request.addResource(with: .photo, fileURL: sourceURL, options: nil)
}) { success, error in
if success {
- print("MoveToSharedStorageWorker: Saved to Photo Library: \(fileName)")
+ NativeLogger.d("MoveToSharedStorageWorker: Saved to Photo Library: \(fileName)")
continuation.resume(returning: .success(
message: "Saved to Photo Library",
data: ["fileName": fileName, "storageType": "photos"]
))
} else {
let msg = error?.localizedDescription ?? "Unknown error"
- print("MoveToSharedStorageWorker: Photo Library error: \(msg)")
+ NativeLogger.e("MoveToSharedStorageWorker: Photo Library error: \(msg)")
continuation.resume(returning: .failure(message: "Failed to save to Photo Library: \(msg)"))
}
}
@@ -123,13 +123,13 @@ class MoveToSharedStorageWorker: IosWorker {
}
try FileManager.default.copyItem(at: sourceURL, to: destURL)
- print("MoveToSharedStorageWorker: Copied to Documents: \(destURL.path)")
+ NativeLogger.d("MoveToSharedStorageWorker: Copied to Documents: \(destURL.path)")
return .success(
message: "Saved to Documents",
data: ["filePath": destURL.path, "fileName": fileName, "storageType": "documents"]
)
} catch {
- print("MoveToSharedStorageWorker: Documents copy failed: \(error.localizedDescription)")
+ NativeLogger.e("MoveToSharedStorageWorker: Documents copy failed: \(error.localizedDescription)")
return .failure(message: "Failed to copy to Documents: \(error.localizedDescription)")
}
}
diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/ParallelHttpDownloadWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/ParallelHttpDownloadWorker.swift
index cd3b441..23bef84 100644
--- a/ios/native_workmanager/Sources/native_workmanager/workers/ParallelHttpDownloadWorker.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/workers/ParallelHttpDownloadWorker.swift
@@ -78,7 +78,7 @@ class ParallelHttpDownloadWorker: IosWorker {
// Skip download if destination already exists and skipExisting is enabled
if (config.skipExisting ?? false) && FileManager.default.fileExists(atPath: config.savePath) {
- print("ParallelHttpDownloadWorker: skipExisting=true and file already exists — skipping")
+ NativeLogger.w("ParallelHttpDownloadWorker: skipExisting=true and file already exists — skipping")
let size = (try? FileManager.default.attributesOfItem(atPath: config.savePath))?[.size] as? Int64 ?? 0
return .success(
message: "File already exists, download skipped",
@@ -108,14 +108,14 @@ class ParallelHttpDownloadWorker: IosWorker {
let ar = http.value(forHTTPHeaderField: "Accept-Ranges")?.lowercased() == "bytes"
return (cl, ar)
} catch {
- print("ParallelHttpDownloadWorker: HEAD failed, fallback: \(error.localizedDescription)")
+ NativeLogger.e("ParallelHttpDownloadWorker: HEAD failed, fallback: \(error.localizedDescription)")
return (-1, false)
}
}()
// ── Step 2: Fallback if server does not support range requests ────────
if !acceptsRanges || contentLength <= 0 {
- print("ParallelHttpDownloadWorker: No range support or unknown size — sequential fallback")
+ NativeLogger.w("ParallelHttpDownloadWorker: No range support or unknown size — sequential fallback")
return await downloadSequential(
session: session, url: url, config: config,
destinationURL: destinationURL, taskId: taskId,
@@ -124,7 +124,7 @@ class ParallelHttpDownloadWorker: IosWorker {
)
}
- print("ParallelHttpDownloadWorker: Content-Length=\(contentLength) chunks=\(config.effectiveNumChunks)")
+ NativeLogger.d("ParallelHttpDownloadWorker: Content-Length=\(contentLength) chunks=\(config.effectiveNumChunks)")
// ── Step 3: Compute byte ranges ───────────────────────────────────────
let numChunks = config.effectiveNumChunks
@@ -175,7 +175,7 @@ class ParallelHttpDownloadWorker: IosWorker {
// ── Step 7: Merge parts → temp file ──────────────────────────────────
let tempURL = URL(fileURLWithPath: config.savePath + ".tmp")
- print("ParallelHttpDownloadWorker: Merging \(numChunks) chunks")
+ NativeLogger.d("ParallelHttpDownloadWorker: Merging \(numChunks) chunks")
do {
// Resolve symlinks before writing (iOS /var → /private/var)
@@ -217,14 +217,14 @@ class ParallelHttpDownloadWorker: IosWorker {
// ── Step 8: Checksum ───────────────────────────────────────────
if let expected = config.expectedChecksum {
- print("ParallelHttpDownloadWorker: Verifying checksum (\(config.effectiveChecksumAlgorithm))...")
+ NativeLogger.d("ParallelHttpDownloadWorker: Verifying checksum (\(config.effectiveChecksumAlgorithm))...")
guard let actual = calculateChecksum(fileURL: resolvedTemp, algorithm: config.effectiveChecksumAlgorithm) else {
return .failure(message: "Failed to calculate checksum")
}
if actual.caseInsensitiveCompare(expected) != .orderedSame {
return .failure(message: "Checksum mismatch (expected: \(expected), actual: \(actual))")
}
- print("ParallelHttpDownloadWorker: Checksum OK: \(actual)")
+ NativeLogger.d("ParallelHttpDownloadWorker: Checksum OK: \(actual)")
}
// ── Step 9: Atomic rename ──────────────────────────────────────
@@ -239,7 +239,7 @@ class ParallelHttpDownloadWorker: IosWorker {
let finalSize = (try? FileManager.default.attributesOfItem(atPath: resolvedDest.path))?[.size] as? Int64 ?? 0
reportProgress(taskId: taskId, progress: 100, message: "Download complete",
bytesDownloaded: finalSize, totalBytes: totalBytes)
- print("ParallelHttpDownloadWorker: Success — \(finalSize) bytes at \(config.savePath)")
+ NativeLogger.d("ParallelHttpDownloadWorker: Success — \(finalSize) bytes at \(config.savePath)")
return .success(
message: "Downloaded \(finalSize) bytes (\(numChunks) parallel chunks)",
@@ -252,7 +252,7 @@ class ParallelHttpDownloadWorker: IosWorker {
]
)
} catch {
- print("ParallelHttpDownloadWorker: Merge failed: \(error.localizedDescription)")
+ NativeLogger.e("ParallelHttpDownloadWorker: Merge failed: \(error.localizedDescription)")
return .failure(message: "Merge failed: \(error.localizedDescription)")
}
}
@@ -279,7 +279,7 @@ class ParallelHttpDownloadWorker: IosWorker {
// Resume: skip if already complete
let existingSize = (try? FileManager.default.attributesOfItem(atPath: partURL.path))?[.size] as? Int64 ?? 0
if existingSize >= expectedSize {
- print("ParallelHttpDownloadWorker: Chunk \(index) already complete, skipping")
+ NativeLogger.w("ParallelHttpDownloadWorker: Chunk \(index) already complete, skipping")
downloadedAtomic.add(existingSize)
return true
}
@@ -290,7 +290,7 @@ class ParallelHttpDownloadWorker: IosWorker {
config.headers?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
if let sc = signingConfig { RequestSigner.sign(request: &request, config: sc) }
- print("ParallelHttpDownloadWorker: Chunk \(index) — bytes=\(resumeFrom)-\(rangeEnd)")
+ NativeLogger.d("ParallelHttpDownloadWorker: Chunk \(index) — bytes=\(resumeFrom)-\(rangeEnd)")
do {
var (data, response) = try await session.data(for: request)
@@ -311,7 +311,7 @@ class ParallelHttpDownloadWorker: IosWorker {
guard let http = response as? HTTPURLResponse,
(200 ..< 300).contains(http.statusCode) else {
let code = (response as? HTTPURLResponse)?.statusCode ?? -1
- print("ParallelHttpDownloadWorker: Chunk \(index) HTTP \(code)")
+ NativeLogger.d("ParallelHttpDownloadWorker: Chunk \(index) HTTP \(code)")
return false
}
@@ -341,10 +341,10 @@ class ParallelHttpDownloadWorker: IosWorker {
networkSpeed: speed, timeRemainingMs: etaMs
)
}
- print("ParallelHttpDownloadWorker: Chunk \(index) done (\(data.count) bytes)")
+ NativeLogger.d("ParallelHttpDownloadWorker: Chunk \(index) done (\(data.count) bytes)")
return true
} catch {
- print("ParallelHttpDownloadWorker: Chunk \(index) error: \(error.localizedDescription)")
+ NativeLogger.e("ParallelHttpDownloadWorker: Chunk \(index) error: \(error.localizedDescription)")
return false
}
}
@@ -509,7 +509,7 @@ class ParallelHttpDownloadWorker: IosWorker {
}) {}
return h.finalize().map { String(format: "%02x", $0) }.joined()
default:
- print("ParallelHttpDownloadWorker: Unsupported algorithm: \(algorithm)")
+ NativeLogger.d("ParallelHttpDownloadWorker: Unsupported algorithm: \(algorithm)")
return nil
}
}
diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/ParallelHttpUploadWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/ParallelHttpUploadWorker.swift
index 9de2818..bdb2256 100644
--- a/ios/native_workmanager/Sources/native_workmanager/workers/ParallelHttpUploadWorker.swift
+++ b/ios/native_workmanager/Sources/native_workmanager/workers/ParallelHttpUploadWorker.swift
@@ -115,7 +115,7 @@ class ParallelHttpUploadWorker: IosWorker {
))
}
- print("ParallelHttpUploadWorker: Uploading \(resolvedFiles.count) files to \(host)" +
+ NativeLogger.d("ParallelHttpUploadWorker: Uploading \(resolvedFiles.count) files to \(host)" +
" maxConcurrent=\(config.effectiveMaxConcurrent) maxRetries=\(config.effectiveMaxRetries)")
let sessionConfig = URLSessionConfiguration.default
@@ -147,7 +147,7 @@ class ParallelHttpUploadWorker: IosWorker {
while attempt <= config.effectiveMaxRetries {
if attempt > 0 {
- print("ParallelHttpUploadWorker: Retry \(attempt)/\(config.effectiveMaxRetries) for \(rf.resolvedName)")
+ NativeLogger.d("ParallelHttpUploadWorker: Retry \(attempt)/\(config.effectiveMaxRetries) for \(rf.resolvedName)")
}
// Per-host concurrency gate (DispatchSemaphore.wait — OK on cooperative pool thread).
@@ -183,7 +183,7 @@ class ParallelHttpUploadWorker: IosWorker {
let done = uploadedCount
progressLock.unlock()
- print("ParallelHttpUploadWorker: [\(i)] \(rf.resolvedName) uploaded (\(done)/\(resolvedFiles.count))")
+ NativeLogger.d("ParallelHttpUploadWorker: [\(i)] \(rf.resolvedName) uploaded (\(done)/\(resolvedFiles.count))")
if let taskId = taskId {
let pct = Int(min(99, Double(done) / Double(resolvedFiles.count) * 100))
@@ -208,7 +208,7 @@ class ParallelHttpUploadWorker: IosWorker {
attempt += 1
}
- print("ParallelHttpUploadWorker: [\(i)] \(rf.resolvedName) failed after \(attempt) attempt(s): \(lastError)")
+ NativeLogger.e("ParallelHttpUploadWorker: [\(i)] \(rf.resolvedName) failed after \(attempt) attempt(s): \(lastError)")
return (i, [
"fileName": rf.resolvedName,
"filePath": rf.fileURL.path,
@@ -236,7 +236,7 @@ class ParallelHttpUploadWorker: IosWorker {
ProgressReporter.shared.clearTask(taskId)
}
- print("ParallelHttpUploadWorker: Done — \(succeeded) succeeded, \(failed) failed, \(totalBytes) bytes total")
+ NativeLogger.e("ParallelHttpUploadWorker: Done — \(succeeded) succeeded, \(failed) failed, \(totalBytes) bytes total")
if succeeded == 0 {
return .failure(message: "All \(fileResults.count) file uploads failed")
diff --git a/native_workmanager_gen/CHANGELOG.md b/native_workmanager_gen/CHANGELOG.md
index 144e79f..5fa500d 100644
--- a/native_workmanager_gen/CHANGELOG.md
+++ b/native_workmanager_gen/CHANGELOG.md
@@ -1,48 +1,68 @@
-## 1.2.5 - 2026-05-06
+# Changelog
-- Bump version to 1.2.5 to synchronize with the `native_workmanager` package release.
+All notable changes to this project will be documented in this file.
-## 1.2.3 - 2026-04-24
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
-- Bump version to 1.2.3 to synchronize with the `native_workmanager` package release.
+---
-## 1.0.4 - 2026-04-20
+## [1.2.5] - 2026-05-06
-- Widen `analyzer` constraint from `^12.0.0` to `>=10.0.0 <13.0.0` so that
- Flutter projects whose SDK pins `meta` to `1.17.0` (e.g. Flutter 3.41.x)
- can resolve the dependency without conflict — `analyzer 10.x` requires
- `meta ^1.15.0` which satisfies the pin.
-- Fix deprecation: replace `getDisplayString(withNullability: false)` with
- `getDisplayString()` (the `withNullability` parameter was deprecated across
- all supported analyzer versions).
+### Changed
+- Version bump to match `native_workmanager` 1.2.5 release.
-## 1.0.3
+---
-- Require analyzer `>=12.0.0` and Dart SDK `>=3.9.0` to match pub.dev analysis
- environment and gain access to stable analyzer 12.x APIs.
-- Replace `FunctionElement` (removed in analyzer 12.x / Dart 3.11+) with
- `ElementKind.FUNCTION` check and `TopLevelFunctionElement` cast.
-- Replace `element.parameters` (renamed to `formalParameters` in analyzer 12.x)
- with `fn.formalParameters` throughout validation logic.
-- Replace `element.name` (now `String?` in analyzer 12.x) with
- `element.displayName` (always non-null `String`) throughout.
-- Drop redundant `enclosingElement is! LibraryElement` guard — annotated
- top-level functions always satisfy `kind == ElementKind.FUNCTION`.
+## [1.2.3] - 2026-04-24
-## 1.0.2
+### Changed
+- Version bump to match `native_workmanager` 1.2.3 release.
-- Replace `TypeChecker.fromRuntime` (removed in source_gen 4.x) with `TypeChecker.fromUrl`
- — removes `dart:mirrors` dependency and fixes static analysis on pub.dev.
-- Remove `native_workmanager` from runtime dependencies (only needed at build time via URI).
+---
-## 1.0.1
+## [1.0.4] - 2026-04-20
-- Widen dependency constraints: `build <5`, `source_gen <5`, `analyzer <13`, `build_runner <4`.
-- Add dartdoc to `workerCallbackBuilder` and `WorkerCallbackGenerator` constructor.
-- Add example demonstrating codegen setup.
+### Fixed
+- Widened `analyzer` constraint from `^12.0.0` to `>=10.0.0 <13.0.0` to resolve
+ `meta` version conflict on Flutter 3.41.x (`analyzer 10.x` requires `meta ^1.15.0`).
+- Replaced deprecated `getDisplayString(withNullability: false)` with `getDisplayString()`.
-## 1.0.0
+---
+## [1.0.3]
+
+### Changed
+- Require `analyzer >=12.0.0` and Dart SDK `>=3.9.0`.
+- Replaced `FunctionElement` (removed in analyzer 12.x) with `ElementKind.FUNCTION` check
+ and `TopLevelFunctionElement` cast.
+- Replaced `element.parameters` with `fn.formalParameters` (renamed in analyzer 12.x).
+- Replaced `element.name` with `element.displayName` (now `String?` in analyzer 12.x).
+
+---
+
+## [1.0.2]
+
+### Fixed
+- Replaced `TypeChecker.fromRuntime` (removed in source_gen 4.x) with `TypeChecker.fromUrl` —
+ removes `dart:mirrors` dependency and fixes static analysis on pub.dev.
+- Removed `native_workmanager` from runtime dependencies (only needed at build time via URI).
+
+---
+
+## [1.0.1]
+
+### Changed
+- Widened dependency constraints: `build <5`, `source_gen <5`, `analyzer <13`, `build_runner <4`.
+
+### Added
+- Dartdoc to `workerCallbackBuilder` and `WorkerCallbackGenerator` constructor.
+- Example demonstrating codegen setup.
+
+---
+
+## [1.0.0]
+
+### Added
- Initial release: `@WorkerCallback` annotation code generator for `native_workmanager`.
- Generates type-safe callback IDs and worker registry from annotated top-level functions.
- Validates callback signature (`Future` return type, `String?` parameter).
diff --git a/native_workmanager_gen/README.md b/native_workmanager_gen/README.md
index 3fda6cf..4b9c33d 100644
--- a/native_workmanager_gen/README.md
+++ b/native_workmanager_gen/README.md
@@ -8,7 +8,7 @@ Generates type-safe Dart callback IDs and a worker registry from `@WorkerCallbac
```yaml
dev_dependencies:
- native_workmanager_gen: ^1.0.4
+ native_workmanager_gen: ^1.2.5
build_runner: ^2.4.0
```
From 0aa79bf89529d874fdc34d1fe20c1e96c705f18c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nguye=CC=82=CC=83n=20Tua=CC=82=CC=81n=20Vie=CC=A3=CC=82t?=
Date: Thu, 7 May 2026 01:21:24 +0700
Subject: [PATCH 5/5] style: fix dart format in test files
---
.../bridge_parameter_passthrough_test.dart | 25 ++++++++++---------
test/unit/task_trigger_test.dart | 19 +++++++++-----
2 files changed, 26 insertions(+), 18 deletions(-)
diff --git a/test/unit/bridge_parameter_passthrough_test.dart b/test/unit/bridge_parameter_passthrough_test.dart
index 3f393c4..5042bdb 100644
--- a/test/unit/bridge_parameter_passthrough_test.dart
+++ b/test/unit/bridge_parameter_passthrough_test.dart
@@ -43,7 +43,8 @@ void main() {
test('intervalMs is int (milliseconds)', () {
final map = TaskTrigger.periodic(const Duration(hours: 2)).toMap();
expect(map['intervalMs'], isA());
- expect(map['intervalMs'], equals(const Duration(hours: 2).inMilliseconds));
+ expect(
+ map['intervalMs'], equals(const Duration(hours: 2).inMilliseconds));
});
test('flexMs is int when set, null when absent', () {
@@ -52,7 +53,8 @@ void main() {
flexInterval: const Duration(minutes: 20),
).toMap();
expect(withFlex['flexMs'], isA());
- expect(withFlex['flexMs'], equals(const Duration(minutes: 20).inMilliseconds));
+ expect(withFlex['flexMs'],
+ equals(const Duration(minutes: 20).inMilliseconds));
final noFlex = TaskTrigger.periodic(const Duration(hours: 1)).toMap();
expect(noFlex['flexMs'], isNull);
@@ -72,8 +74,7 @@ void main() {
});
test('runImmediately is bool', () {
- final trueMap =
- TaskTrigger.periodic(const Duration(hours: 1)).toMap();
+ final trueMap = TaskTrigger.periodic(const Duration(hours: 1)).toMap();
expect(trueMap['runImmediately'], isA());
expect(trueMap['runImmediately'], isTrue);
@@ -88,8 +89,7 @@ void main() {
group('OneTimeTrigger map keys and types', () {
test('all expected keys are present', () {
- final map =
- TaskTrigger.oneTime(const Duration(minutes: 5)).toMap();
+ final map = TaskTrigger.oneTime(const Duration(minutes: 5)).toMap();
expect(map.containsKey('type'), isTrue);
expect(map.containsKey('initialDelayMs'), isTrue);
});
@@ -100,11 +100,10 @@ void main() {
});
test('initialDelayMs is int (milliseconds)', () {
- final map =
- TaskTrigger.oneTime(const Duration(minutes: 10)).toMap();
+ final map = TaskTrigger.oneTime(const Duration(minutes: 10)).toMap();
expect(map['initialDelayMs'], isA());
- expect(
- map['initialDelayMs'], equals(const Duration(minutes: 10).inMilliseconds));
+ expect(map['initialDelayMs'],
+ equals(const Duration(minutes: 10).inMilliseconds));
});
test('zero delay emits 0 not null', () {
@@ -159,8 +158,10 @@ void main() {
).toMap();
expect(map['earliestMs'], isA());
expect(map['latestMs'], isA());
- expect(map['earliestMs'], equals(const Duration(hours: 1).inMilliseconds));
- expect(map['latestMs'], equals(const Duration(hours: 3).inMilliseconds));
+ expect(
+ map['earliestMs'], equals(const Duration(hours: 1).inMilliseconds));
+ expect(
+ map['latestMs'], equals(const Duration(hours: 3).inMilliseconds));
});
});
});
diff --git a/test/unit/task_trigger_test.dart b/test/unit/task_trigger_test.dart
index c0704cc..8a33fb2 100644
--- a/test/unit/task_trigger_test.dart
+++ b/test/unit/task_trigger_test.dart
@@ -242,7 +242,8 @@ void main() {
).toMap();
expect(map['intervalMs'], const Duration(hours: 1).inMilliseconds);
expect(map['flexMs'], isNull);
- expect(map['initialDelayMs'], const Duration(minutes: 5).inMilliseconds);
+ expect(
+ map['initialDelayMs'], const Duration(minutes: 5).inMilliseconds);
expect(map['runImmediately'], isTrue);
});
@@ -265,7 +266,8 @@ void main() {
).toMap();
expect(map['intervalMs'], const Duration(hours: 6).inMilliseconds);
expect(map['flexMs'], const Duration(minutes: 30).inMilliseconds);
- expect(map['initialDelayMs'], const Duration(minutes: 10).inMilliseconds);
+ expect(map['initialDelayMs'],
+ const Duration(minutes: 10).inMilliseconds);
expect(map['runImmediately'], isTrue);
});
@@ -282,7 +284,8 @@ void main() {
});
// issue_26: this combination was blocked by an assert in v1.2.4
- test('combo: interval + initialDelay + runImmediately:false [issue_26]', () {
+ test('combo: interval + initialDelay + runImmediately:false [issue_26]',
+ () {
final map = TaskTrigger.periodic(
const Duration(hours: 1),
initialDelay: const Duration(minutes: 30),
@@ -290,13 +293,16 @@ void main() {
).toMap();
expect(map['intervalMs'], const Duration(hours: 1).inMilliseconds);
expect(map['flexMs'], isNull);
- expect(map['initialDelayMs'], const Duration(minutes: 30).inMilliseconds);
+ expect(map['initialDelayMs'],
+ const Duration(minutes: 30).inMilliseconds);
// Dart emits false; native bridge overrides to true when initialDelayMs>0
// to satisfy KMP library. See KMPSchedulerBridge.swift and +Enqueue.kt.
expect(map['runImmediately'], isFalse);
});
- test('combo: all four — interval + flex + initialDelay + runImmediately:false', () {
+ test(
+ 'combo: all four — interval + flex + initialDelay + runImmediately:false',
+ () {
final map = TaskTrigger.periodic(
const Duration(hours: 6),
flexInterval: const Duration(minutes: 30),
@@ -305,7 +311,8 @@ void main() {
).toMap();
expect(map['intervalMs'], const Duration(hours: 6).inMilliseconds);
expect(map['flexMs'], const Duration(minutes: 30).inMilliseconds);
- expect(map['initialDelayMs'], const Duration(minutes: 10).inMilliseconds);
+ expect(map['initialDelayMs'],
+ const Duration(minutes: 10).inMilliseconds);
expect(map['runImmediately'], isFalse);
});
});