diff --git a/CLAUDE.md b/CLAUDE.md index 5c3e74c5..0ac07987 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,87 +1,18 @@ -## Project Workflow -- Project uses GitHub Actions -- Use `ktlint -F .` in root folder to format Kotlin code -- Use SwiftLint for code formatting -- Always resolve formatting and analyzer errors before completing a task -- **CRITICAL**: Always run `ktlint -F .` after modifying any Kotlin files before committing - -## Pigeon Code Generation -- Pigeon configuration is in `workmanager_platform_interface/pigeons/workmanager_api.dart` -- **MUST use melos to regenerate Pigeon files**: `melos run generate:pigeon` -- ⚠️ **DO NOT** run pigeon directly - always use the melos script for consistency -- Generated files: - - Dart: `workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart` - - Kotlin: `workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt` - - Swift: `workmanager_apple/ios/Classes/pigeon/WorkmanagerApi.g.swift` -- Do not manually edit generated files (*.g.* files) -- Generated files may have different formatting than dart format - this is expected and handled by exclusion patterns - -## Code Formatting Configuration -- `.editorconfig` in root folder configures ktlint to ignore Pigeon-generated Kotlin files -- `.swiftlint.yml` in root folder excludes Pigeon-generated Swift files from linting - -## GitHub Actions Configuration -- Format checks: `.github/workflows/format.yml` - - Runs dart format, ktlint, and SwiftLint -- Tests: `.github/workflows/test.yml` - - `test`: Runs Dart unit tests - - `native_ios_tests`: Runs iOS native tests with xcodebuild - - `native_android_tests`: Runs Android native tests with Gradle - - `drive_ios`: Runs Flutter integration tests on iOS simulator - - `drive_android`: Runs Flutter integration tests on Android emulator - -## Testing Strategy & Preferences -- **Focus on business logic**: Test unique platform implementation logic, not Pigeon plumbing -- **Trust third-party components**: Consider Pigeon a trusted component - don't test its internals -- **Platform-specific behavior**: Test what makes each platform unique (Android WorkManager vs iOS BGTaskScheduler) -- **Avoid channel mocking**: Don't mock platform channels unless absolutely necessary -- **Test unsupported operations**: Verify platform-specific UnsupportedError throwing -- **Integration over unit**: Prefer integration tests for complete platform behavior validation - -## Test Execution -- Run all tests: `flutter test` (from root or individual package) -- Android tests: `cd workmanager_android && flutter test` -- Apple tests: `cd workmanager_apple && flutter test` -- Native Android tests: `cd example/android && ./gradlew :workmanager_android:test` -- Native iOS tests: `cd example/ios && xcodebuild test -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest'` -- Always build example app before completing: `cd example && flutter build apk --debug && flutter build ios --debug --no-codesign` - -## Pigeon Migration Status -- ✅ Migration to Pigeon v22.7.4 completed successfully -- ✅ All platforms (Android, iOS) migrated from MethodChannel to Pigeon -- ✅ Unit tests refactored to focus on platform-specific business logic -- ✅ Code formatting and linting properly configured for generated files -- ✅ All tests passing: Dart unit tests, native Android tests, native iOS tests -- ✅ Example app builds successfully for both Android APK and iOS app - -## Documentation Preferences -- Keep summaries concise - don't repeat completed tasks in status updates -- Focus on current progress and next steps -- Document decisions and architectural choices - -## CHANGELOG Management -- Document improvements in CHANGELOG.md files immediately when implemented -- Use "Future" as the version header for unreleased changes (standard open source practice) -- Keep entries brief and focused on user-facing impact -- Relevant files: workmanager/CHANGELOG.md, workmanager_android/CHANGELOG.md, workmanager_apple/CHANGELOG.md - -## GitHub Actions - Package Analysis -- The `analysis.yml` workflow runs package analysis for all packages -- It performs `flutter analyze` and `dart pub publish --dry-run` for each package -- The dry-run validates that packages are ready for publishing -- Common issues that cause failures: - - Uncommitted changes in git (packages should be published from clean state) - - Files ignored by .gitignore but checked into git (use .pubignore if needed) - - Modified files that haven't been committed -- Always ensure all changes are committed before pushing to avoid CI failures - -## GitHub Actions - Formatting Issues -- The `format.yml` workflow runs formatting checks -- ❌ **Important Discovery**: `analysis_options.yml formatter.exclude` does NOT prevent `dart format` from formatting files -- ✅ **FIXED**: Updated CI workflow to use `find` command to exclude .g.dart files: - ```bash - find . -name "*.dart" ! -name "*.g.dart" ! -path "*/.*" -print0 | xargs -0 dart format --set-exit-if-changed - ``` -- **Root Issue**: `dart format` ignores analysis_options.yml exclusions and will always format ALL Dart files -- **Solution**: Filter files before passing to dart format to exclude generated files -- The `analysis_options.yml` exclusions only affect static analysis, not formatting \ No newline at end of file +## Pre-Commit Requirements +**CRITICAL**: Always run from project root before ANY commit: +1. `ktlint -F .` +2. `find . -name "*.dart" ! -name "*.g.dart" ! -path "*/.*" -print0 | xargs -0 dart format --set-exit-if-changed` +3. `flutter test` (all Dart tests) +4. `cd example/android && ./gradlew :workmanager_android:test` (Android native tests) + +## Code Generation +- Regenerate Pigeon files: `melos run generate:pigeon` +- Do not manually edit *.g.* files + +## Test Quality Requirements +- **NEVER create useless tests**: No `assert(true)`, `expect(true, true)`, or compilation-only tests +- **Test real logic**: Exercise actual methods with real inputs and verify meaningful outputs +- **Test edge cases**: null inputs, error conditions, boundary values + +## Complex Component Testing +- **BackgroundWorker**: Cannot be unit tested due to Flutter engine dependencies - use integration tests \ No newline at end of file diff --git a/workmanager/CHANGELOG.md b/workmanager/CHANGELOG.md index 1b2b1097..6feff105 100644 --- a/workmanager/CHANGELOG.md +++ b/workmanager/CHANGELOG.md @@ -1,6 +1,7 @@ # Future ## Bug Fixes & Improvements +* Fix null cast to map bug in executeTask when inputData contains null keys or values (thanks to @Dr-wgy) * Internal improvements to development and testing infrastructure # 0.8.0 diff --git a/workmanager/lib/src/workmanager_impl.dart b/workmanager/lib/src/workmanager_impl.dart index e51ac784..b77a8b45 100644 --- a/workmanager/lib/src/workmanager_impl.dart +++ b/workmanager/lib/src/workmanager_impl.dart @@ -295,22 +295,30 @@ class Workmanager { Future printScheduledTasks() async => _platform.printScheduledTasks(); } +/// Converts inputData from Pigeon format, filtering out null keys +@visibleForTesting +Map? convertPigeonInputData(Map? inputData) { + Map? convertedInputData; + if (inputData != null) { + convertedInputData = {}; + for (final entry in inputData.entries) { + if (entry.key != null) { + convertedInputData[entry.key!] = entry.value; + } + } + } + return convertedInputData; +} + /// Implementation of WorkmanagerFlutterApi for handling background task execution class _WorkmanagerFlutterApiImpl extends WorkmanagerFlutterApi { @override - Future backgroundChannelInitialized() async { - // This is called by the native side to indicate it's ready - // We don't need to do anything special here - } + Future backgroundChannelInitialized() async {} @override Future executeTask( String taskName, Map? inputData) async { - // Convert the input data to the expected format - final Map? convertedInputData = - inputData?.cast(); - - // Call the user's background task handler + final convertedInputData = convertPigeonInputData(inputData); final result = await Workmanager._backgroundTaskHandler ?.call(taskName, convertedInputData); return result ?? false; diff --git a/workmanager/test/pigeon_input_data_conversion_test.dart b/workmanager/test/pigeon_input_data_conversion_test.dart new file mode 100644 index 00000000..a3135bfe --- /dev/null +++ b/workmanager/test/pigeon_input_data_conversion_test.dart @@ -0,0 +1,95 @@ +import 'package:test/test.dart'; +import 'package:workmanager/src/workmanager_impl.dart'; + +void main() { + group('convertPigeonInputData', () { + test('handles null inputData', () { + final result = convertPigeonInputData(null); + expect(result, null); + }); + + test('filters null keys while preserving null values', () { + final Map inputData = { + 'validKey': 'validValue', + 'nullValueKey': null, + null: 'shouldBeFilteredOut', + 'numberKey': 42, + 'boolKey': true, + 'listKey': ['item1', 'item2'], + }; + + final result = convertPigeonInputData(inputData); + + expect(result, isNotNull); + expect(result, isA>()); + expect(result!.length, 5); + + expect(result['validKey'], 'validValue'); + expect(result['nullValueKey'], null); + expect(result['numberKey'], 42); + expect(result['boolKey'], true); + expect(result['listKey'], ['item1', 'item2']); + expect(result.containsKey(null), false); + }); + + test('handles empty inputData', () { + final result = convertPigeonInputData({}); + + expect(result, isNotNull); + expect(result, isEmpty); + expect(result, isA>()); + }); + + test('handles inputData with only null keys', () { + final result = convertPigeonInputData({null: 'value1'}); + + expect(result, isNotNull); + expect(result, isEmpty); + expect(result, isA>()); + }); + + test('handles mixed valid and invalid keys', () { + final Map mixedData = { + 'key1': 'value1', + null: 'nullKeyValue', + 'key2': null, + '': 'emptyStringKey', + 'key3': 123, + }; + + final result = convertPigeonInputData(mixedData); + + expect(result!.length, 4); + expect(result['key1'], 'value1'); + expect(result['key2'], null); + expect(result[''], 'emptyStringKey'); + expect(result['key3'], 123); + expect(result.containsKey(null), false); + }); + + test('preserves complex nested data structures', () { + final Map complexData = { + 'mapKey': {'nested': 'value'}, + 'listKey': [ + 1, + 2, + {'nested': 'list'} + ], + 'nullKey': null, + null: 'filtered', + }; + + final result = convertPigeonInputData(complexData); + + expect(result!.length, 3); + expect(result['mapKey'], {'nested': 'value'}); + expect(result['listKey'], [ + 1, + 2, + {'nested': 'list'} + ]); + expect(result['nullKey'], null); + expect(result.containsKey(null), false); + }); + }); +} diff --git a/workmanager_android/CHANGELOG.md b/workmanager_android/CHANGELOG.md index d174af9e..5f877790 100644 --- a/workmanager_android/CHANGELOG.md +++ b/workmanager_android/CHANGELOG.md @@ -1,5 +1,8 @@ ## Future +### Bug Fixes +* Fix null callback crash in BackgroundWorker when FlutterCallbackInformation is null (thanks to @jonathanduke, @Muneeza-PT) + ### Improvements * Improve SharedPreferenceHelper callback handling - now calls callback immediately when preferences are already loaded diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt index 66776501..8913c9c6 100644 --- a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt +++ b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt @@ -83,6 +83,13 @@ class BackgroundWorker( ) { val callbackHandle = SharedPreferenceHelper.getCallbackHandle(applicationContext) val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) + + if (callbackInfo == null) { + Log.e(TAG, "Failed to resolve Dart callback for handle $callbackHandle.") + completer?.set(Result.failure()) + return@ensureInitializationCompleteAsync + } + val dartBundlePath = flutterLoader.findAppBundlePath() if (isInDebug) {