├── ios ├── Assets │ └── .gitkeep ├── .gitignore ├── Classes │ ├── internal │ │ ├── BGTaskHandler.h │ │ ├── utils.m │ │ ├── BGTaskMgrDelegate.h │ │ ├── utils.h │ │ ├── BGTaskHandler.m │ │ └── BGTaskMgrDelegate.m │ ├── FltWorkerPlugin.h │ └── FltWorkerPlugin.m └── flt_worker.podspec ├── android ├── settings.gradle ├── .gitignore ├── gradle.properties ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── dev │ │ │ └── thinkng │ │ │ └── flt_worker │ │ │ ├── internal │ │ │ ├── BackgroundWorker.java │ │ │ ├── MethodCallFuture.java │ │ │ ├── BackgroundWorkerPlugin.java │ │ │ ├── WorkRequests.java │ │ │ └── AbsWorkerPlugin.java │ │ │ └── FltWorkerPlugin.java │ └── test │ │ └── java │ │ └── dev │ │ └── thinkng │ │ └── flt_worker │ │ └── internal │ │ └── WorkRequestsTest.java ├── .idea │ ├── codeStyles │ │ ├── codeStyleConfig.xml │ │ └── Project.xml │ └── inspectionProfiles │ │ └── Project_Default.xml ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── build.gradle ├── analysis_options.yaml ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── AppDelegate.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── main.m │ │ ├── AppDelegate.swift │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ ├── Podfile.lock │ └── Podfile ├── android │ ├── gradle.properties │ ├── .gitignore │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ └── values │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── java │ │ │ │ │ └── dev │ │ │ │ │ │ └── thinkng │ │ │ │ │ │ └── flt_worker_example │ │ │ │ │ │ └── MainActivity.java │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── .gitignore ├── test │ └── widget_test.dart ├── lib │ ├── worker.dart │ ├── counter_file.dart │ ├── btc_price_file.dart │ ├── background_tasks_counter.dart │ ├── counter.dart │ ├── work_manager_counter.dart │ ├── background_tasks_btc_prices.dart │ ├── work_manager_btc_prices.dart │ ├── btc_prices.dart │ ├── rest.dart │ └── main.dart ├── pubspec.yaml ├── README.md └── pubspec.lock ├── CHANGELOG.md ├── test ├── flt_worker_test.dart ├── ios_delegate_test.dart └── android_delegate_test.dart ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── dictionaries │ └── ywu.xml └── runConfigurations │ └── example_lib_main_dart.xml ├── lib ├── android.dart ├── ios.dart ├── src │ ├── utils.dart │ ├── background_tasks │ │ ├── background_tasks.dart │ │ ├── delegate.dart │ │ └── models.dart │ ├── work_manager │ │ ├── delegate.dart │ │ ├── work_manager.dart │ │ └── models.dart │ ├── functions.dart │ ├── callback_dispatcher.dart │ ├── constraints.dart │ └── models.dart └── flt_worker.dart ├── .metadata ├── .gitignore ├── pubspec.yaml ├── .github └── workflows │ ├── publish.yaml │ └── check.yaml ├── flt_worker.iml ├── LICENSE ├── pubspec.lock └── README.md /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flt_worker' 2 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | :point_right: [Release History](https://github.com/xinthink/flt_worker/releases) 2 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /test/flt_worker_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | void main() { 4 | test('temaple', () { 5 | expect(1, 1); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /android/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /.idea/dictionaries/ywu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | backoff 5 | cupertino 6 | unmetered 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/android.dart: -------------------------------------------------------------------------------- 1 | /// The low level api specific for the Android platform, mapping to the `WorkManager` library. 2 | library work_manager; 3 | 4 | export 'flt_worker.dart' show initializeWorker; 5 | export 'src/work_manager/work_manager.dart'; 6 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/ios.dart: -------------------------------------------------------------------------------- 1 | /// The low level api specific for the iOS platform, mapping to the `BackgroundTasks` framework. 2 | library background_tasks; 3 | 4 | export 'flt_worker.dart' show initializeWorker; 5 | export 'src/background_tasks/background_tasks.dart'; 6 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 6 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations/example_lib_main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 67826bdce54505760fe83b7ead70bdb5af6fe9f2 8 | channel: dev 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 67826bdce54505760fe83b7ead70bdb5af6fe9f2 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Classes/internal/BGTaskHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // BGTaskHandler.h 3 | // Pods 4 | // 5 | // Created by Yingxin Wu on 2020/3/13. 6 | // 7 | 8 | #ifndef BGTaskHandler_h 9 | #define BGTaskHandler_h 10 | 11 | #import "utils.h" 12 | #import 13 | 14 | @interface BGTaskHandler : NSObject 15 | 16 | @property (class, readonly) BGTaskHandler * _Nonnull instance; 17 | @property (class, nonatomic) FuncRegisterPlugins _Nullable registerPlugins; 18 | 19 | - (void)handleBGTask:(BGTask * _Nonnull)task API_AVAILABLE(ios(13.0)); 20 | 21 | @end 22 | 23 | #endif /* BGTaskHandler_h */ 24 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.3' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /ios/Classes/FltWorkerPlugin.h: -------------------------------------------------------------------------------- 1 | #import "utils.h" 2 | #import 3 | 4 | 5 | @interface FltWorkerPlugin : NSObject 6 | 7 | /** 8 | * Provides a callback to register needed plugins for the headless isolate. 9 | * 10 | * Example: 11 | * ``` 12 | * - (BOOL)application:(UIApplication *)application 13 | * didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 14 | * FltWorkerPlugin.registerPlugins = ^(NSObject *registry) { 15 | * [GeneratedPluginRegistrant registerWithRegistry:registry]; 16 | * }; 17 | * } 18 | * ``` 19 | */ 20 | @property (class, nonatomic) FuncRegisterPlugins registerPlugins; 21 | @end 22 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Classes/internal/utils.m: -------------------------------------------------------------------------------- 1 | // 2 | // utils.m 3 | // flt_worker 4 | // 5 | // Created by Yingxin Wu on 2020/3/13. 6 | // 7 | 8 | #import "utils.h" 9 | 10 | const NSString *_lock = @""; 11 | 12 | NSUserDefaults *_workerDefaults = nil; 13 | NSUserDefaults* workerDefaults() { 14 | @synchronized (_lock) { 15 | if (_workerDefaults == nil) { 16 | _workerDefaults = [[NSUserDefaults alloc] init]; 17 | } 18 | return _workerDefaults; 19 | } 20 | } 21 | 22 | int64_t dispatcherHandle() { 23 | return [[workerDefaults() objectForKey:@DISPATCHER_KEY] longValue] ?: 0; 24 | } 25 | 26 | int64_t workerHandle() { 27 | return [[workerDefaults() objectForKey:@WORKER_KEY] longValue] ?: 0; 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | doc/ 9 | 10 | # IntelliJ related 11 | *.iml 12 | *.ipr 13 | *.iws 14 | # .idea/ 15 | **/.idea/.name 16 | **/.idea/assetWizardSettings.xml 17 | **/.idea/caches 18 | # .idea/dictionaries 19 | **/.idea/gradle.xml 20 | **/.idea/jarRepositories.xml 21 | **/.idea/jsLibraryMappings.xml 22 | **/.idea/libraries 23 | **/.idea/misc.xml 24 | **/.idea/modules.xml 25 | **/.idea/navEditor.xml 26 | **/.idea/runConfigurations.xml 27 | **/.idea/tasks.xml 28 | **/.idea/vcs.xml 29 | **/.idea/workspace.xml 30 | 31 | # The .vscode folder contains launch configuration and tasks you configure in 32 | # VS Code which you may wish to be included in version control, so this line 33 | # is commented out by default. 34 | .vscode/ 35 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flt_worker 2 | description: The flt_worker plugin allows you to schedule and execute Dart background tasks, based on the WorkManager and the BackgroundTasks APIs, for Android and iOS 13+ respectively. 3 | version: 0.1.0 4 | homepage: https://github.com/xinthink/flt_worker/ 5 | repository: https://github.com/xinthink/flt_worker/ 6 | 7 | environment: 8 | sdk: ">=2.3.0 <3.0.0" 9 | flutter: "^1.10.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | pedantic: ^1.8.0 19 | 20 | flutter: 21 | plugin: 22 | platforms: 23 | android: 24 | package: dev.thinkng.flt_worker 25 | pluginClass: FltWorkerPlugin 26 | ios: 27 | pluginClass: FltWorkerPlugin 28 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/services.dart'; 4 | 5 | import 'models.dart'; 6 | 7 | const CHANNEL_NAME = 'dev.thinkng.flt_worker'; 8 | const METHOD_PREFIX = 'FltWorkerPlugin'; 9 | 10 | /// Typedef of a worker function. 11 | typedef WorkerFn = Future Function(WorkPayload payload); 12 | 13 | /// The shared method channel for api calls 14 | const apiChannel = MethodChannel(CHANNEL_NAME); 15 | 16 | /// Returns the raw handle of the [callback] function, throws if it doesn't exist. 17 | int ensureRawHandle(Function callback) { 18 | final handle = PluginUtilities.getCallbackHandle(callback)?.toRawHandle(); 19 | if (handle == null) { 20 | throw Exception('CallbackHandle not found for the specified function. Make sure to use a top level function'); 21 | } 22 | return handle; 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: published 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: subosito/flutter-action@v1 13 | - name: Format 14 | run: | 15 | $FLUTTER_HOME/bin/cache/dart-sdk/bin/dartfmt -l 80 -w . 16 | - name: Publish 17 | run: | 18 | mkdir ~/.pub-cache/ 19 | echo "${{ secrets.PUB_CREDENTIALS }}" | base64 --decode > ~/.pub-cache/credentials.json 20 | pub publish --force 21 | - name: notification 22 | if: cancelled() == false 23 | uses: xinthink/action-telegram@v1.1 24 | with: 25 | botToken: ${{ secrets.TelegramBotToken }} 26 | chatId: ${{ secrets.TelegramTarget }} 27 | jobStatus: ${{ job.status }} 28 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Exceptions to above rules. 40 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 41 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/dev/thinkng/flt_worker_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package dev.thinkng.flt_worker_example; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import dev.thinkng.flt_worker.FltWorkerPlugin; 6 | import io.flutter.embedding.android.FlutterActivity; 7 | import io.flutter.embedding.engine.FlutterEngine; 8 | import io.flutter.plugins.GeneratedPluginRegistrant; 9 | 10 | public class MainActivity extends FlutterActivity { 11 | @Override 12 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { 13 | GeneratedPluginRegistrant.registerWith(flutterEngine); 14 | FltWorkerPlugin.registerPluginsForWorkers = registry -> { 15 | io.flutter.plugins.pathprovider.PathProviderPlugin.registerWith( 16 | registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin")); 17 | return null; 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/src/main/java/dev/thinkng/flt_worker/internal/BackgroundWorker.java: -------------------------------------------------------------------------------- 1 | package dev.thinkng.flt_worker.internal; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.Keep; 7 | import androidx.annotation.NonNull; 8 | import androidx.work.Worker; 9 | import androidx.work.WorkerParameters; 10 | 11 | @Keep 12 | public class BackgroundWorker extends Worker { 13 | 14 | public BackgroundWorker(@NonNull Context context, @NonNull WorkerParameters params) { 15 | super(context, params); 16 | } 17 | 18 | @NonNull 19 | @Override 20 | public Result doWork() { 21 | try { 22 | BackgroundWorkerPlugin.getInstance(getApplicationContext()) 23 | .doWork(this) 24 | .get(); 25 | return Result.success(); 26 | } catch (Throwable e) { 27 | Log.e(AbsWorkerPlugin.TAG, "worker execution failure", e); 28 | return Result.failure(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'dev.thinkng.flt_worker' 2 | version '1.0' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.5.3' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 28 26 | 27 | defaultConfig { 28 | minSdkVersion 16 29 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 30 | } 31 | lintOptions { 32 | disable 'InvalidPackage' 33 | } 34 | } 35 | 36 | dependencies { 37 | def work_version = "2.3.2" 38 | 39 | implementation "androidx.work:work-runtime:$work_version" 40 | // implementation "androidx.work:work-rxjava2:$work_version" 41 | 42 | testCompile 'junit:junit:4.12' 43 | } 44 | -------------------------------------------------------------------------------- /ios/Classes/internal/BGTaskMgrDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // BGTaskMgrDelegate.h 3 | // flt_worker 4 | // 5 | // Created by Yingxin Wu on 2020/3/12. 6 | // 7 | 8 | #ifndef BGTaskMgrDelegate_h 9 | #define BGTaskMgrDelegate_h 10 | 11 | #import 12 | 13 | @interface BGTaskMgrDelegate : NSObject 14 | 15 | @property (readonly, nonatomic) FlutterMethodChannel *methodChannel; 16 | 17 | /** Register background task indentifiers. */ 18 | + (void)registerBGTaskHandler; 19 | 20 | - (instancetype)initWithRegistrar:(NSObject*)registrar; 21 | 22 | - (instancetype)initWithEngine:(FlutterEngine*)engine; 23 | 24 | - (BOOL)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; 25 | 26 | /** Save dispatcher & worker handles for later use */ 27 | - (void)saveHandles:(NSArray*)args; 28 | 29 | /** Makes a payload dict used as input of the dart worker */ 30 | - (NSDictionary*)packPayloadForTask:(NSString*)identifier; 31 | @end 32 | 33 | #endif /* BGTaskMgrDelegate_h */ 34 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "GeneratedPluginRegistrant.h" 3 | #import 4 | #import 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application 9 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 10 | 11 | // set a callback to register all plugins to a headless engine instance 12 | FltWorkerPlugin.registerPlugins = ^(NSObject *registry) { 13 | [FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]]; 14 | }; 15 | 16 | [GeneratedPluginRegistrant registerWithRegistry:self]; 17 | // Override point for customization after application launch. 18 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 19 | } 20 | 21 | - (void)applicationDidEnterBackground:(UIApplication *)application { 22 | NSLog(@"app did enter background…"); 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flt_worker_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => widget is Text && 22 | widget.data.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: subosito/flutter-action@v1 11 | - name: Check 12 | run: | 13 | cd example && flutter analyze && cd .. 14 | flutter test 15 | - name: notification 16 | if: cancelled() == false 17 | uses: xinthink/action-telegram@v1.1 18 | with: 19 | botToken: ${{ secrets.TelegramBotToken }} 20 | chatId: ${{ secrets.TelegramTarget }} 21 | jobStatus: ${{ job.status }} 22 | 23 | # coverage: 24 | # runs-on: ubuntu-latest 25 | # container: 26 | # image: google/dart:dev 27 | # steps: 28 | # - uses: actions/checkout@v1 29 | # - run: pub get 30 | # - name: Code Coverage 31 | # run: | 32 | # pub global activate test_coverage 33 | # pub global run test_coverage 34 | # - uses: codecov/codecov-action@v1.0.0 35 | # with: 36 | # token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /ios/flt_worker.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint flt_worker.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'flt_worker' 7 | s.version = '0.0.1' 8 | s.summary = 'A new flutter plugin project.' 9 | s.description = <<-DESC 10 | A new flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.public_header_files = 'Classes/**/*.h' 18 | s.dependency 'Flutter' 19 | s.weak_framework = 'BackgroundTasks' 20 | s.platform = :ios, '8.0' 21 | 22 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. 23 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 24 | end 25 | -------------------------------------------------------------------------------- /lib/src/background_tasks/background_tasks.dart: -------------------------------------------------------------------------------- 1 | /// The low level api specific for the iOS platform, mapping to the `BackgroundTasks` framework. 2 | library background_tasks; 3 | 4 | import 'models.dart'; 5 | import '../utils.dart'; 6 | 7 | export 'models.dart'; 8 | 9 | /// Schedules a previously registered background task for execution. 10 | Future submitTaskRequest(BGTaskRequest request) 11 | => apiChannel.invokeMethod('$METHOD_PREFIX#submitTaskRequest', request.toJson()); 12 | 13 | /// Cancels a scheduled task request with the [identifier]. 14 | Future cancelTaskRequest(String identifier) 15 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelTaskRequest', identifier); 16 | 17 | /// Cancels all scheduled task requests. 18 | Future cancelAllTaskRequests() 19 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelAllTaskRequests'); 20 | 21 | /// Simulate launch BGTask with the given [identifier], **debugging only** 22 | Future simulateLaunchTask(String identifier) 23 | => apiChannel.invokeMethod('$METHOD_PREFIX#simulateLaunchTask', identifier); 24 | -------------------------------------------------------------------------------- /lib/src/work_manager/delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import 'work_manager.dart'; 4 | import '../models.dart'; 5 | 6 | /// Enqueues a request to work in the background. 7 | Future enqueueWorkIntent(WorkIntent intent) => 8 | enqueueWorkRequest(parseWorkIntent(intent)); 9 | 10 | Future cancelWork(String id) => cancelAllWorkByTag(id); 11 | 12 | Future wmCancelAllWork() => cancelAllWork(); 13 | 14 | @visibleForTesting 15 | WorkRequest parseWorkIntent(WorkIntent intent) { 16 | final tags = [intent.identifier] + (intent.tags ?? []); 17 | 18 | return intent.repeatInterval != null 19 | ? PeriodicWorkRequest( 20 | tags: tags, 21 | input: intent.input, 22 | initialDelay: intent.initialDelay, 23 | constraints: intent.constraints, 24 | repeatInterval: intent.repeatInterval, 25 | flexInterval: intent.flexInterval, 26 | ) 27 | : OneTimeWorkRequest( 28 | tags: tags, 29 | input: intent.input, 30 | initialDelay: intent.initialDelay, 31 | constraints: intent.constraints, 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/functions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'background_tasks/delegate.dart' as bg; 5 | import 'models.dart'; 6 | import 'work_manager/delegate.dart' as wm; 7 | 8 | /// Enqueues a request to work in the background. 9 | final Future Function(WorkIntent intent) enqueueWorkIntent = 10 | Platform.isAndroid ? wm.enqueueWorkIntent : bg.enqueueWorkIntent; 11 | 12 | /// Cancels all unfinished work with the given [identifier]. 13 | /// 14 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run. 15 | final Future Function(String id) cancelWork = 16 | Platform.isAndroid ? wm.cancelWork : bg.cancelWork; 17 | 18 | /// Cancels all unfinished work. 19 | /// 20 | /// **Use this method with extreme caution!** 21 | /// By invoking it, you will potentially affect other modules or libraries in your codebase. 22 | /// It is strongly recommended that you use one of the other cancellation methods at your disposal. 23 | final Future Function() cancelAllWork = 24 | Platform.isAndroid ? wm.wmCancelAllWork : bg.cancelAllWork; 25 | -------------------------------------------------------------------------------- /ios/Classes/internal/utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // utils.h 3 | // Pods 4 | // 5 | // Created by Yingxin Wu on 2020/3/13. 6 | // 7 | 8 | #ifndef utils_h 9 | #define utils_h 10 | 11 | #import 12 | 13 | #define PLUGIN_PKG "dev.thinkng.flt_worker" 14 | #define API_METHOD(NAME) "FltWorkerPlugin#"#NAME 15 | #define DISPATCHER_KEY "dev.thinkng.flt_worker/callback_dispatcher_handle" 16 | #define WORKER_KEY "dev.thinkng.flt_worker/worker_handle" 17 | #define IS_NONNULL(V) V && ![NSNull.null isEqual:V] 18 | #define TASK_KEY(id) [NSString stringWithFormat:@"dev.thinkng.flt_worker/tasks/%@", id] 19 | #define WORKER_DEFAULTS_LONG(K) [[workerDefaults() objectForKey:@K] longValue] ?: 0 20 | 21 | /** Retrieves the `UserDefaults` instance for FltWorkerPlugin. */ 22 | NSUserDefaults* workerDefaults(void); 23 | 24 | /** Returns raw function handle of the callback dispatcher. */ 25 | int64_t dispatcherHandle(void); 26 | 27 | /** Returns raw handle of the worker function. */ 28 | int64_t workerHandle(void); 29 | 30 | typedef void (^FuncRegisterPlugins)(NSObject*registry); 31 | 32 | #endif /* utils_h */ 33 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - flt_worker (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - path_provider (0.0.1): 6 | - Flutter 7 | - path_provider_macos (0.0.1): 8 | - Flutter 9 | 10 | DEPENDENCIES: 11 | - flt_worker (from `.symlinks/plugins/flt_worker/ios`) 12 | - Flutter (from `Flutter`) 13 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 14 | - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) 15 | 16 | EXTERNAL SOURCES: 17 | flt_worker: 18 | :path: ".symlinks/plugins/flt_worker/ios" 19 | Flutter: 20 | :path: Flutter 21 | path_provider: 22 | :path: ".symlinks/plugins/path_provider/ios" 23 | path_provider_macos: 24 | :path: ".symlinks/plugins/path_provider_macos/ios" 25 | 26 | SPEC CHECKSUMS: 27 | flt_worker: 09a85dc9bfc12106faa15f7c00b26f85cbccb3e6 28 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 29 | path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d 30 | path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 31 | 32 | PODFILE CHECKSUM: 083258d7f5e80b42ea9bfee905fe93049bc04c64 33 | 34 | COCOAPODS: 1.8.4 35 | -------------------------------------------------------------------------------- /flt_worker.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 xinthink 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/callback_dispatcher.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'models.dart'; 8 | import 'utils.dart'; 9 | 10 | /// Callback dispatcher, which is the entry of the isolate running background workers. 11 | void callbackDispatcher() { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | // final channel = MethodChannel('$CHANNEL_NAME'); 14 | // channel.setMethodCallHandler(_executeBackgroundTask); 15 | apiChannel.setMethodCallHandler(_executeBackgroundTask); 16 | } 17 | 18 | /// Run the specified function in the background isoloate. 19 | Future _executeBackgroundTask(MethodCall call) { 20 | final args = call.arguments; 21 | WorkerFn callback; 22 | Map payload; 23 | 24 | if (args.isNotEmpty) { 25 | final handle = CallbackHandle.fromRawHandle(args[0]); 26 | payload = Map.castFrom(args[1]); 27 | if (handle != null) { 28 | callback = PluginUtilities.getCallbackFromHandle(handle); 29 | } 30 | } 31 | 32 | if (callback != null) { 33 | return callback(WorkPayload.fromJson(payload)); 34 | } 35 | 36 | debugPrint('Callback not found for method=${call.method} args=$args'); 37 | return Future.value(); 38 | } 39 | -------------------------------------------------------------------------------- /example/lib/worker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flt_worker/flt_worker.dart'; 4 | 5 | import 'btc_price_file.dart'; 6 | import 'counter_file.dart'; 7 | 8 | /// Worker callback running in the background isolate. 9 | Future worker(WorkPayload payload) { 10 | if (payload.tags.contains(kTagCounterWork)) { 11 | return _increaseCounter(payload.input); 12 | } else if (payload.tags.contains(kTagBtcPricesWork)) { 13 | return _fetchBtcPrice(); 14 | } else { 15 | return Future.value(); 16 | } 17 | } 18 | 19 | /// The worker increasing the counter. 20 | Future _increaseCounter(Map input) => 21 | writeCounter((input['counter'] ?? 0) + 1); 22 | 23 | /// Fetches the latest BTC price via CoinBase rest api. 24 | Future _fetchBtcPrice() async { 25 | try { 26 | await fetchBtcPrice(); 27 | } finally { 28 | if (Platform.isIOS) { 29 | // periodic work is not supported natively on iOS, 30 | // so we have to schedule it again after the current one is marked as complete 31 | Future.delayed(Duration(milliseconds: 50), () => 32 | enqueueWorkIntent(const WorkIntent( 33 | identifier: kTagBtcPricesWork, 34 | initialDelay: Duration(seconds: 60), 35 | )) 36 | ); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/background_tasks/delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import 'background_tasks.dart'; 4 | import '../models.dart'; 5 | 6 | /// Enqueues a request to work in the background. 7 | Future enqueueWorkIntent(WorkIntent intent) => 8 | submitTaskRequest(parseWorkIntent(intent)); 9 | 10 | Future cancelWork(String id) => cancelTaskRequest(id).then((_) => true); 11 | 12 | Future cancelAllWork() => cancelAllTaskRequests().then((_) => true); 13 | 14 | @visibleForTesting 15 | BGTaskRequest parseWorkIntent(WorkIntent intent) { 16 | bool network; 17 | if (intent.constraints?.networkType != null) { 18 | network = intent.constraints.networkType != NetworkType.notRequired; 19 | } 20 | 21 | final earliestBeginDate = intent.initialDelay != null 22 | ? DateTime.now().add(intent.initialDelay) : null; 23 | 24 | return intent.isProcessingTask == true 25 | ? BGProcessingTaskRequest( 26 | intent.identifier, 27 | input: intent.input, 28 | earliestBeginDate: earliestBeginDate, 29 | requiresExternalPower: intent.constraints?.charging, 30 | requiresNetworkConnectivity: network, 31 | ) 32 | : BGAppRefreshTaskRequest( 33 | intent.identifier, 34 | input: intent.input, 35 | earliestBeginDate: earliestBeginDate, 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /example/lib/counter_file.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:watcher/watcher.dart'; 6 | 7 | const kTagCounterWork = 'com.example.counter_task'; 8 | 9 | /// Returns the counter file path. 10 | Future counterFile() async { 11 | final dir = (await getTemporaryDirectory()).path; 12 | final file = File('$dir/counter.txt'); 13 | if (!(await file.exists())) { 14 | await file.writeAsString('0'); 15 | } 16 | return file; 17 | } 18 | 19 | /// A stream of the updated counter values. 20 | Stream counterStream() async* { 21 | // yield the initial value 22 | yield await readCounter(); 23 | 24 | // yield a value whenever the file is modified 25 | final path = (await counterFile()).path; 26 | final updates = (Platform.isAndroid 27 | ? PollingFileWatcher(path) : FileWatcher(path)).events; 28 | await for (final _ in updates) { 29 | yield await readCounter(); 30 | } 31 | } 32 | 33 | /// Reads counter from a file. 34 | Future readCounter() async { 35 | try { 36 | final counterStr = await (await counterFile()).readAsString(); 37 | return counterStr.isNotEmpty ? int.parse(counterStr) : 0; 38 | } catch (e) { 39 | debugPrint('read counter file failed: $e'); 40 | return 0; 41 | } 42 | } 43 | 44 | /// The worker working on the counter. 45 | Future writeCounter(int count) async { 46 | debugPrint('--- updating counter file => $count'); 47 | await (await counterFile()).writeAsString('$count'); 48 | } 49 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/lib/btc_price_file.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | import 'package:watcher/watcher.dart'; 7 | 8 | import 'rest.dart'; 9 | 10 | const kTagBtcPricesWork = 'com.example.btc_prices_task'; 11 | 12 | /// Returns the BTC price file path. 13 | Future btcPriceFile() async { 14 | final dir = (await getTemporaryDirectory()).path; 15 | final file = File('$dir/btc_price.json'); 16 | if (!(await file.exists())) { 17 | await file.writeAsString('{}', flush: true); 18 | } 19 | return file; 20 | } 21 | 22 | /// A stream of updated BTC prices. 23 | Stream btcPriceStream() async* { 24 | // yield the initial value 25 | yield await readBtcPrice(); 26 | 27 | // yield a value whenever the file is modified 28 | final path = (await btcPriceFile()).path; 29 | final updates = (Platform.isAndroid 30 | ? PollingFileWatcher(path) : FileWatcher(path)).events; 31 | await for (final _ in updates) { 32 | yield await readBtcPrice(); 33 | } 34 | } 35 | 36 | /// Reads the price from a data file. 37 | Future readBtcPrice() async { 38 | try { 39 | final json = jsonDecode(await (await btcPriceFile()).readAsString()); 40 | return json['amount'] != null ? json : null; 41 | } catch (e) { 42 | debugPrint('read data file failed: $e'); 43 | return null; 44 | } 45 | } 46 | 47 | /// Fetches the latest BTC price via CoinBase rest api. 48 | Future fetchBtcPrice() async { 49 | debugPrint('--- fetching BTC price'); 50 | final resp = await getJson('https://api.coinbase.com/v2/prices/spot?currency=USD'); 51 | await (await btcPriceFile()).writeAsString('''{ 52 | "amount": ${resp['data']['amount']}, 53 | "time": ${DateTime.now().millisecondsSinceEpoch} 54 | }'''); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/work_manager/work_manager.dart: -------------------------------------------------------------------------------- 1 | /// The low level api specific for the Android platform, mapping to the `WorkManager` library. 2 | library work_manager; 3 | 4 | import 'models.dart'; 5 | import '../utils.dart'; 6 | 7 | export 'models.dart'; 8 | 9 | /// Enqueues one item for background processing. 10 | Future enqueueWorkRequest(WorkRequest request) 11 | => enqueueWorkRequests([request]); 12 | 13 | /// Enqueues one or more items for background processing. 14 | Future enqueueWorkRequests(Iterable requests) 15 | => apiChannel.invokeMethod('$METHOD_PREFIX#enqueue', 16 | requests.map((r) => r.toJson()).toList(growable: false) 17 | ); 18 | 19 | /// Cancels all unfinished work with the given [tag]. 20 | /// 21 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run. 22 | Future cancelAllWorkByTag(String tag) 23 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelAllWorkByTag', tag); 24 | 25 | /// Cancels all unfinished work in the work chain with the given [name]. 26 | /// 27 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run. 28 | Future cancelUniqueWork(String name) 29 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelUniqueWork', name); 30 | 31 | /// Cancels work with the given [uuid] if it isn't finished. 32 | /// 33 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run. 34 | Future cancelWorkById(String uuid) 35 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelWorkById', uuid); 36 | 37 | /// Cancels all unfinished work. 38 | /// 39 | /// **Use this method with extreme caution!** 40 | /// By invoking it, you will potentially affect other modules or libraries in your codebase. 41 | /// It is strongly recommended that you use one of the other cancellation methods at your disposal. 42 | Future cancelAllWork() 43 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelAllWork'); 44 | -------------------------------------------------------------------------------- /example/lib/background_tasks_counter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flt_worker/ios.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'counter_file.dart'; 5 | 6 | /// BackgroundTasks api example for the iOS platform. 7 | class BackgroundTasksCounter extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) => Scaffold( 10 | appBar: AppBar( 11 | title: const Text('Counter (BackgroundTasks)'), 12 | ), 13 | body: SingleChildScrollView( 14 | child: Container( 15 | alignment: Alignment.center, 16 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48), 17 | child: _buildCounter(), 18 | ), 19 | ), 20 | ); 21 | 22 | /// Renders the latest counter by watching a data file. 23 | Widget _buildCounter() => StreamBuilder( 24 | stream: counterStream(), 25 | builder: (_, snapshot) => Column( 26 | children: [ 27 | RichText( 28 | textAlign: TextAlign.center, 29 | text: TextSpan( 30 | text: 'Increases the counter via a processing task\n', 31 | style: const TextStyle( 32 | color: Colors.black87, 33 | fontSize: 16, 34 | ), 35 | children: [ 36 | TextSpan( 37 | text: snapshot.hasData ? '${snapshot.data}' : '', 38 | style: const TextStyle( 39 | color: Colors.blueAccent, 40 | fontSize: 48, 41 | height: 1.618, 42 | ), 43 | ), 44 | ], 45 | ), 46 | ), 47 | RaisedButton( 48 | child: const Text('Count'), 49 | onPressed: () => _increaseCounter(snapshot.data), 50 | ), 51 | ], 52 | ), 53 | ); 54 | 55 | /// Submit a background task to update the counter. 56 | void _increaseCounter(int counter) { 57 | submitTaskRequest(BGProcessingTaskRequest(kTagCounterWork, 58 | input: { 59 | 'counter': counter, 60 | }, 61 | )); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/lib/counter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flt_worker/flt_worker.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'counter_file.dart'; 5 | 6 | /// WorkManager api example for the Android platform. 7 | class Counter extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) => Scaffold( 10 | appBar: AppBar( 11 | title: const Text('Counter'), 12 | ), 13 | body: SingleChildScrollView( 14 | child: Container( 15 | alignment: Alignment.center, 16 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48), 17 | child: _buildCounter(), 18 | ), 19 | ), 20 | ); 21 | 22 | /// Renders the latest counter by watching a data file. 23 | Widget _buildCounter() => StreamBuilder( 24 | stream: counterStream(), 25 | builder: (_, snapshot) => Column( 26 | children: [ 27 | RichText( 28 | textAlign: TextAlign.center, 29 | text: TextSpan( 30 | text: 'Increases the counter via an unified API on both platforms.\n', 31 | style: const TextStyle( 32 | color: Colors.black87, 33 | fontSize: 16, 34 | ), 35 | children: [ 36 | TextSpan( 37 | text: snapshot.hasData ? '${snapshot.data}' : '', 38 | style: const TextStyle( 39 | color: Colors.blueAccent, 40 | fontSize: 48, 41 | height: 1.618, 42 | ), 43 | ), 44 | ], 45 | ), 46 | ), 47 | RaisedButton( 48 | child: const Text('Count'), 49 | onPressed: () => _increaseCounter(snapshot.data), 50 | ), 51 | ], 52 | ) 53 | ); 54 | 55 | /// Enqueues a work request to update the counter. 56 | void _increaseCounter(int counter) { 57 | enqueueWorkIntent(WorkIntent( 58 | identifier: kTagCounterWork, 59 | input: { 60 | 'counter': counter, 61 | }, 62 | isProcessingTask: true, 63 | )); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "dev.thinkng.flt_worker_example" 37 | minSdkVersion 16 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'androidx.test:runner:1.1.1' 60 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 61 | } 62 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BGTaskSchedulerPermittedIdentifiers 6 | 7 | com.example.task1 8 | dev.example.task2 9 | com.example.counter_task 10 | com.example.btc_prices_task 11 | 12 | CFBundleDevelopmentRegion 13 | $(DEVELOPMENT_LANGUAGE) 14 | CFBundleExecutable 15 | $(EXECUTABLE_NAME) 16 | CFBundleIdentifier 17 | $(PRODUCT_BUNDLE_IDENTIFIER) 18 | CFBundleInfoDictionaryVersion 19 | 6.0 20 | CFBundleName 21 | flt_worker_example 22 | CFBundlePackageType 23 | APPL 24 | CFBundleShortVersionString 25 | $(FLUTTER_BUILD_NAME) 26 | CFBundleSignature 27 | ???? 28 | CFBundleVersion 29 | $(FLUTTER_BUILD_NUMBER) 30 | LSRequiresIPhoneOS 31 | 32 | UIBackgroundModes 33 | 34 | fetch 35 | processing 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /example/lib/work_manager_counter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flt_worker/android.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'counter_file.dart'; 5 | 6 | /// WorkManager api example for the Android platform. 7 | class WorkManagerCounter extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) => Scaffold( 10 | appBar: AppBar( 11 | title: const Text('Counter (WorkManager)'), 12 | ), 13 | body: SingleChildScrollView( 14 | child: Container( 15 | alignment: Alignment.center, 16 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48), 17 | child: _buildCounter(), 18 | ), 19 | ), 20 | ); 21 | 22 | /// Renders the latest counter by watching a data file. 23 | Widget _buildCounter() => StreamBuilder( 24 | stream: counterStream(), 25 | builder: (_, snapshot) => Column( 26 | children: [ 27 | RichText( 28 | textAlign: TextAlign.center, 29 | text: TextSpan( 30 | text: 'Increases the counter via an one-off work\n', 31 | style: const TextStyle( 32 | color: Colors.black87, 33 | fontSize: 16, 34 | ), 35 | children: [ 36 | TextSpan( 37 | text: snapshot.hasData ? '${snapshot.data}' : '', 38 | style: const TextStyle( 39 | color: Colors.blueAccent, 40 | fontSize: 48, 41 | height: 1.618, 42 | ), 43 | ), 44 | ], 45 | ), 46 | ), 47 | RaisedButton( 48 | child: const Text('Count'), 49 | onPressed: () => _increaseCounter(snapshot.data), 50 | ), 51 | ], 52 | ) 53 | ); 54 | 55 | /// Enqueues a work request to update the counter. 56 | void _increaseCounter(int counter) { 57 | enqueueWorkRequest(OneTimeWorkRequest( 58 | tags: [kTagCounterWork], 59 | constraints: WorkConstraints( 60 | networkType: NetworkType.notRequired, 61 | ), 62 | input: { 63 | 'counter': counter, 64 | }, 65 | )); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/ios_delegate_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flt_worker/flt_worker.dart'; 2 | import 'package:flt_worker/ios.dart'; 3 | import 'package:flt_worker/src/background_tasks/delegate.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | test('tranform unified model to BackgroundTasks terms', () { 8 | var intent = WorkIntent( 9 | identifier: 'work1', 10 | ); 11 | var req = parseWorkIntent(intent); 12 | expect(req, isA()); 13 | expect(req.identifier, 'work1'); 14 | expect(req.earliestBeginDate, null); 15 | expect(req.input, null); 16 | 17 | intent = WorkIntent( 18 | identifier: 'work2', 19 | isProcessingTask: true, 20 | ); 21 | req = parseWorkIntent(intent); 22 | expect(req, isA()); 23 | expect(req.identifier, 'work2'); 24 | expect((req as BGProcessingTaskRequest).requiresNetworkConnectivity, isNull); 25 | expect((req as BGProcessingTaskRequest).requiresExternalPower, isNull); 26 | }); 27 | 28 | test('parse BGProcessingTaskRequest with constraints', () { 29 | final now = DateTime.now(); 30 | var intent = WorkIntent( 31 | identifier: 'work1', 32 | initialDelay: Duration(minutes: 11), 33 | isProcessingTask: true, 34 | constraints: WorkConstraints( 35 | networkType: NetworkType.notRoaming, 36 | charging: true, 37 | ), 38 | ); 39 | var req = parseWorkIntent(intent) as BGProcessingTaskRequest; 40 | expect( 41 | (req.earliestBeginDate.millisecondsSinceEpoch - now.add(Duration(minutes: 11)).millisecondsSinceEpoch).abs(), 42 | lessThanOrEqualTo(1000)); 43 | expect(req.requiresNetworkConnectivity, isTrue); 44 | expect(req.requiresExternalPower, isTrue); 45 | 46 | intent = WorkIntent( 47 | identifier: 'work1', 48 | isProcessingTask: true, 49 | constraints: WorkConstraints( 50 | networkType: NetworkType.notRequired, 51 | charging: false, 52 | ), 53 | ); 54 | req = parseWorkIntent(intent) as BGProcessingTaskRequest; 55 | expect(req.requiresNetworkConnectivity, isFalse); 56 | expect(req.requiresExternalPower, isFalse); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flt_worker_example 2 | description: Demonstrates how to use the flt_worker plugin. 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.3.0 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | # The following adds the Cupertino Icons font to your application. 14 | # Use with the CupertinoIcons class for iOS style icons. 15 | cupertino_icons: ^0.1.2 16 | 17 | path_provider: ^1.6.5 18 | watcher: ^0.9.7+14 19 | http: ^0.12.0+4 20 | intl: ^0.16.1 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | 26 | flt_worker: 27 | path: ../ 28 | 29 | # For information on the generic Dart part of this file, see the 30 | # following page: https://dart.dev/tools/pub/pubspec 31 | 32 | # The following section is specific to Flutter. 33 | flutter: 34 | 35 | # The following line ensures that the Material Icons font is 36 | # included with your application, so that you can use the icons in 37 | # the material Icons class. 38 | uses-material-design: true 39 | 40 | # To add assets to your application, add an assets section, like this: 41 | # assets: 42 | # - images/a_dot_burr.jpeg 43 | # - images/a_dot_ham.jpeg 44 | 45 | # An image asset can refer to one or more resolution-specific "variants", see 46 | # https://flutter.dev/assets-and-images/#resolution-aware. 47 | 48 | # For details regarding adding assets from package dependencies, see 49 | # https://flutter.dev/assets-and-images/#from-packages 50 | 51 | # To add custom fonts to your application, add a fonts section here, 52 | # in this "flutter" section. Each entry in this list should have a 53 | # "family" key with the font family name, and a "fonts" key with a 54 | # list giving the asset and other descriptors for the font. For 55 | # example: 56 | # fonts: 57 | # - family: Schyler 58 | # fonts: 59 | # - asset: fonts/Schyler-Regular.ttf 60 | # - asset: fonts/Schyler-Italic.ttf 61 | # style: italic 62 | # - family: Trajan Pro 63 | # fonts: 64 | # - asset: fonts/TrajanPro.ttf 65 | # - asset: fonts/TrajanPro_Bold.ttf 66 | # weight: 700 67 | # 68 | # For details regarding fonts from package dependencies, 69 | # see https://flutter.dev/custom-fonts/#from-packages 70 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flt_worker_example 2 | 3 | Demonstrates how to use the flt_worker plugin. 4 | 5 | ## Examples 6 | 7 | High-level API examples: 8 | - [counter.dart]: Counter demo the worker way 9 | - [btc_prices.dart]: refreshing Bitcoin price periodically in the background 10 | - [worker.dart]: the worker dispather example 11 | 12 | Android low-level API examples: 13 | - [work_manager_counter.dart]: Counter using an `OneTimeWorkRequest` 14 | - [work_manager_btc_prices.dart]: refreshing Bitcoin price periodically using a `PeriodicWorkRequest` 15 | 16 | iOS low-level API examples: 17 | - [background_tasks_counter.dart]: Counter using a `BGProcessingTaskRequest` 18 | - [background_tasks_btc_prices.dart]: refreshing Bitcoin price periodically using `BGAppRefreshTaskRequest`s 19 | 20 | ## Debugging on iOS 21 | 22 | For debugging your worker on an iOS device, you may want to force launch a `BGTaskRequest`. 23 | 24 | Please follow these steps: 25 | 1. Run your app using Xcode 26 | 2. Set a breakpoint at the last line of the `handleMethodCall` method in `flt_worker/ios/Classes/FltWorkerPlugin.m` 27 | 3. When the app pauses (after submission of a `BGTaskRequest`), execute the following line in the debugger, substituting your task identifier for `TASK_IDENTIFIER`, and resume the app. 28 | 29 | ``` 30 | e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"] 31 | ``` 32 | 33 | Please find more details here: [Starting and Terminating Tasks During Development]. 34 | 35 | 36 | [worker.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/worker.dart 37 | [counter.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/counter.dart 38 | [btc_prices.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/btc_prices.dart 39 | [background_tasks_btc_prices.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/background_tasks_btc_prices.dart 40 | [background_tasks_counter.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/background_tasks_counter.dart 41 | [work_manager_btc_prices.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/work_manager_btc_prices.dart 42 | [work_manager_counter.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/work_manager_counter.dart 43 | [Starting and Terminating Tasks During Development]: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development 44 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 23 | 27 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /android/src/main/java/dev/thinkng/flt_worker/internal/MethodCallFuture.java: -------------------------------------------------------------------------------- 1 | package dev.thinkng.flt_worker.internal; 2 | 3 | import androidx.annotation.Nullable; 4 | import androidx.annotation.UiThread; 5 | 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.concurrent.Future; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.concurrent.TimeoutException; 10 | 11 | import io.flutter.plugin.common.MethodChannel; 12 | 13 | @SuppressWarnings("unchecked") 14 | public class MethodCallFuture implements MethodChannel.Result, Future { 15 | private final byte[] lock = new byte[0]; 16 | private volatile boolean isComplete; 17 | private volatile Object result; 18 | private volatile Exception error; 19 | 20 | @UiThread 21 | @Override 22 | public void success(Object o) { 23 | synchronized (lock) { 24 | result = o; 25 | isComplete = true; 26 | lock.notifyAll(); 27 | } 28 | } 29 | 30 | @UiThread 31 | @Override 32 | public void error(String errorCode, String errorMessage, Object errorDetails) { 33 | synchronized (lock) { 34 | error = new RuntimeException(error + " " + errorMessage + (errorDetails != null ? " " + errorDetails : "")); 35 | isComplete = true; 36 | lock.notifyAll(); 37 | } 38 | } 39 | 40 | @UiThread 41 | @Override 42 | public void notImplemented() { 43 | synchronized (lock) { 44 | isComplete = true; 45 | lock.notifyAll(); 46 | } 47 | } 48 | 49 | @Override 50 | public boolean cancel(boolean b) { 51 | return false; 52 | } 53 | 54 | @Override 55 | public boolean isCancelled() { 56 | return false; 57 | } 58 | 59 | @Override 60 | public boolean isDone() { 61 | return isComplete; 62 | } 63 | 64 | @Nullable 65 | @Override 66 | public T get() throws ExecutionException, InterruptedException { 67 | synchronized (lock) { 68 | while (!isComplete) { 69 | lock.wait(); 70 | } 71 | 72 | if (error != null) { 73 | throw new ExecutionException("", error); 74 | } 75 | return (T) result; 76 | } 77 | } 78 | 79 | @Nullable 80 | @Override 81 | public T get(long timeout, TimeUnit timeUnit) throws ExecutionException, InterruptedException, TimeoutException { 82 | synchronized (lock) { 83 | long timeoutMillis = timeUnit.toMillis(timeout); 84 | long t = System.currentTimeMillis(); 85 | while (!isComplete) { 86 | lock.wait(timeoutMillis); 87 | if (System.currentTimeMillis() - t >= timeoutMillis) { 88 | throw new TimeoutException("timed out waiting for the result to complete"); 89 | } 90 | } 91 | 92 | if (error != null) { 93 | throw new ExecutionException("", error); 94 | } 95 | return (T) result; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/android_delegate_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flt_worker/android.dart'; 2 | import 'package:flt_worker/flt_worker.dart'; 3 | import 'package:flt_worker/src/work_manager/delegate.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | test('tranform unified model to WorkManager terms', () { 8 | var intent = WorkIntent( 9 | identifier: 'work1', 10 | ); 11 | var req = parseWorkIntent(intent); 12 | expect(req, isA()); 13 | expect(req.tags, ['work1']); 14 | expect(req.initialDelay, null); 15 | expect(req.input, null); 16 | expect(req.constraints, null); 17 | expect(req.backoffCriteria, null); 18 | 19 | intent = WorkIntent( 20 | identifier: 'work2', 21 | tags: ['periodic'], 22 | repeatInterval: Duration(hours: 4), 23 | ); 24 | req = parseWorkIntent(intent); 25 | expect(req, isA()); 26 | expect(req.tags, ['work2', 'periodic']); 27 | expect((req as PeriodicWorkRequest).repeatInterval, Duration(hours: 4)); 28 | 29 | intent = WorkIntent( 30 | identifier: 'work2', 31 | tags: ['periodic'], 32 | repeatInterval: Duration(hours: 4), 33 | flexInterval: Duration(minutes: 1), 34 | ); 35 | req = parseWorkIntent(intent); 36 | expect(req, isA()); 37 | expect(req.tags, ['work2', 'periodic']); 38 | expect((req as PeriodicWorkRequest).repeatInterval, Duration(hours: 4)); 39 | expect((req as PeriodicWorkRequest).flexInterval, Duration(minutes: 1)); 40 | }); 41 | 42 | test('model tranformation with constraints', () { 43 | // default constraints (empty) 44 | var intent = WorkIntent( 45 | identifier: 'work1', 46 | initialDelay: Duration(seconds: 59), 47 | constraints: WorkConstraints(), 48 | ); 49 | var req = parseWorkIntent(intent); 50 | var constraints = req.constraints; 51 | expect(req.initialDelay, Duration(seconds: 59)); 52 | expect(constraints, isNotNull); 53 | expect(constraints.batteryNotLow, isNull); 54 | expect(constraints.charging, isNull); 55 | expect(constraints.deviceIdle, isNull); 56 | expect(constraints.networkType, isNull); 57 | expect(constraints.storageNotLow, isNull); 58 | 59 | intent = WorkIntent( 60 | identifier: 'work1', 61 | constraints: WorkConstraints( 62 | batteryNotLow: true, 63 | charging: false, 64 | deviceIdle: true, 65 | networkType: NetworkType.metered, 66 | storageNotLow: false, 67 | ), 68 | ); 69 | req = parseWorkIntent(intent); 70 | constraints = req.constraints; 71 | expect(constraints.batteryNotLow, isTrue); 72 | expect(constraints.charging, isFalse); 73 | expect(constraints.deviceIdle, isTrue); 74 | expect(constraints.networkType, NetworkType.metered); 75 | expect(constraints.storageNotLow, isFalse); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/background_tasks/models.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | /// An abstract class for representing task requests. 6 | @immutable 7 | abstract class BGTaskRequest { 8 | /// The identifier of the task associated with the request. 9 | final String identifier; 10 | 11 | /// The earliest date and time at which to run the task. 12 | /// 13 | /// Specify `null` for no start delay. 14 | /// 15 | /// Setting the property indicates that the background task shouldn’t start any earlier than this date. 16 | /// However, the system doesn't guarantee launching the task at the specified date, but only that it won’t begin sooner. 17 | final DateTime earliestBeginDate; 18 | 19 | /// Input data of the task. 20 | final Map input; 21 | 22 | /// Initializes a [BGTaskRequest] instance with the given [identifier]. 23 | /// 24 | /// Optional properties: 25 | /// - [earliestBeginDate] 26 | /// - [input] 27 | const BGTaskRequest(this.identifier, { 28 | this.earliestBeginDate, 29 | this.input, 30 | }); 31 | 32 | Map toJson() => { 33 | 'type': (this is BGAppRefreshTaskRequest) ? 'AppRefresh' : 'Processing', 34 | 'identifier': identifier, 35 | 'earliestBeginDate': earliestBeginDate?.millisecondsSinceEpoch, 36 | 'input': jsonEncode(input ?? {}), 37 | }; 38 | } 39 | 40 | /// A request to launch your app in the background to execute a short refresh task. 41 | @immutable 42 | class BGAppRefreshTaskRequest extends BGTaskRequest { 43 | /// Instantiates a [BGAppRefreshTaskRequest] with the task [identifier] 44 | /// and an optional [earliestBeginDate]. 45 | const BGAppRefreshTaskRequest(String identifier, { 46 | DateTime earliestBeginDate, 47 | Map input, 48 | }) : super(identifier, earliestBeginDate: earliestBeginDate, input: input); 49 | } 50 | 51 | /// A request to launch your app in the background to execute a processing task that can take minutes to complete. 52 | @immutable 53 | class BGProcessingTaskRequest extends BGTaskRequest { 54 | /// Specifies if the processing task requires a device connected to power. 55 | final bool requiresExternalPower; 56 | 57 | /// Specifies if the processing task requires network connectivity. 58 | final bool requiresNetworkConnectivity; 59 | 60 | /// Instantiates a [BGProcessingTaskRequest] with the task [identifier]. 61 | /// 62 | /// and an optional [earliestBeginDate]. 63 | const BGProcessingTaskRequest(String identifier, { 64 | DateTime earliestBeginDate, 65 | Map input, 66 | this.requiresExternalPower, 67 | this.requiresNetworkConnectivity, 68 | }) : super(identifier, earliestBeginDate: earliestBeginDate, input: input); 69 | 70 | @override 71 | Map toJson() { 72 | final json = super.toJson(); 73 | json['requiresExternalPower'] = requiresExternalPower; 74 | json['requiresNetworkConnectivity'] = requiresNetworkConnectivity; 75 | return json; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ios/Classes/FltWorkerPlugin.m: -------------------------------------------------------------------------------- 1 | #import "FltWorkerPlugin.h" 2 | #import "BGTaskMgrDelegate.h" 3 | #import "BGTaskHandler.h" 4 | 5 | @implementation FltWorkerPlugin { 6 | BGTaskMgrDelegate *_delegate; 7 | // FlutterEngine *_headlessEngine; 8 | // FlutterMethodChannel *_callbackChannel; 9 | // BOOL _isHeadlessEnginRegistered; 10 | // NSUserDefaults *_userDefaults; 11 | // NSDictionary *_workers; 12 | } 13 | 14 | static FltWorkerPlugin *instance = nil; 15 | 16 | + (void)registerWithRegistrar:(NSObject*)registrar { 17 | @synchronized (self) { 18 | if (instance == nil) { 19 | instance = [[FltWorkerPlugin alloc] initWithRegistrar:registrar]; 20 | // [registrar addApplicationDelegate:instance]; 21 | } 22 | } 23 | 24 | // channel for api calls should be registerd for both instances of engine, 25 | // so that it's available in the headless isolate 26 | // FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@PLUGIN_PKG 27 | // binaryMessenger:[registrar messenger]]; 28 | // [registrar addMethodCallDelegate:instance channel:channel]; 29 | } 30 | 31 | + (FuncRegisterPlugins) registerPlugins { 32 | return BGTaskHandler.registerPlugins; 33 | } 34 | 35 | + (void) setRegisterPlugins:(FuncRegisterPlugins)registerPlugins { 36 | BGTaskHandler.registerPlugins = registerPlugins; 37 | } 38 | 39 | - (instancetype)initWithRegistrar:(NSObject*)registrar { 40 | self = [super init]; 41 | if (self) { 42 | _delegate = [[BGTaskMgrDelegate alloc] initWithRegistrar:registrar]; 43 | [registrar addMethodCallDelegate:self channel:_delegate.methodChannel]; 44 | // _userDefaults = [[NSUserDefaults alloc] init]; 45 | 46 | // init a headless engine instance for callback 47 | // _headlessEngine = [[FlutterEngine alloc] initWithName:@"flt_worker_isolate" 48 | // project:nil 49 | // allowHeadlessExecution:YES]; 50 | // 51 | // // channel for callbacks 52 | // FlutterMethodChannel *callbackChannel = [FlutterMethodChannel methodChannelWithName:@PLUGIN_PKG"/callback" 53 | // binaryMessenger:[_headlessEngine binaryMessenger]]; 54 | 55 | // register BGTask identifiers 56 | [BGTaskMgrDelegate registerBGTaskHandler]; 57 | } 58 | return self; 59 | } 60 | 61 | - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 62 | // NSLog(@"--- handling method call: %@ args=%@", call.method, call.arguments); 63 | NSString *method = call.method; 64 | NSArray *args = call.arguments; 65 | if ([@API_METHOD(initialize) isEqualToString:method]) { 66 | [_delegate saveHandles:args]; 67 | result(nil); 68 | } else if ([@API_METHOD(test) isEqualToString:method]) { 69 | // [BGTaskHandler.instance handleBGTask:nil]; 70 | result(nil); 71 | } else if (![_delegate handleMethodCall:call result:result]) { 72 | result(FlutterMethodNotImplemented); 73 | } 74 | } 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /lib/src/constraints.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | /// Constraints for a [WorkIntent]. 4 | @immutable 5 | class WorkConstraints { 6 | /// Whether the work requires a particular [NetworkType] to run. 7 | /// 8 | /// The default value is dependent on the native `WorkManager`, 9 | /// which should be [NetworkType.notRequired] according to 10 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiredNetworkType(androidx.work.NetworkType)). 11 | final NetworkType networkType; 12 | 13 | /// Whether device battery should be at an acceptable level for the work to run. 14 | /// 15 | /// The default value is dependent on the native `WorkManager`, 16 | /// which should be `false` according to 17 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiresBatteryNotLow(boolean)). 18 | final bool batteryNotLow; 19 | 20 | /// Whether device should be charging for the work to run. 21 | /// 22 | /// The default value is dependent on the native `WorkManager`, 23 | /// which should be `false` according to 24 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiresCharging(boolean)). 25 | final bool charging; 26 | 27 | /// Whether device should be idle for the work to run. 28 | /// 29 | /// Requires Android SDK level 23+. 30 | /// The default value is dependent on the native `WorkManager`, 31 | /// which should be `false` according to 32 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiresDeviceIdle(boolean)). 33 | final bool deviceIdle; 34 | 35 | /// Whether the work requires device's storage should be at an acceptable level. 36 | /// 37 | /// The default value is dependent on the native `WorkManager`, 38 | /// which should be `false` according to 39 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiresStorageNotLow(boolean)). 40 | final bool storageNotLow; 41 | 42 | /// Creates constraints for a [WorkRequest]. 43 | const WorkConstraints({ 44 | this.networkType, 45 | this.batteryNotLow, 46 | this.charging, 47 | this.deviceIdle, 48 | this.storageNotLow, 49 | }); 50 | 51 | /// Serializes this constraints into a json object. 52 | Map toJson() => { 53 | 'networkType': networkType?.index, 54 | 'batteryNotLow': batteryNotLow, 55 | 'charging': charging, 56 | 'deviceIdle': deviceIdle, 57 | 'storageNotLow': storageNotLow, 58 | }; 59 | } 60 | 61 | /// An enumeration of various network types that can be used as [WorkConstraints]. 62 | enum NetworkType { 63 | /// Any working network connection is required for this work. 64 | connected, 65 | 66 | /// A metered network connection is required for this work. 67 | metered, 68 | 69 | /// A network is not required for this work. 70 | notRequired, 71 | 72 | /// A non-roaming network connection is required for this work. 73 | notRoaming, 74 | 75 | /// An unmetered network connection is required for this work. 76 | unmetered, 77 | } 78 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/lib/background_tasks_btc_prices.dart: -------------------------------------------------------------------------------- 1 | import 'package:flt_worker/ios.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import 'btc_price_file.dart'; 6 | 7 | /// An example for using low level `WorkManager` api on the Android platform, 8 | /// which polls Bitcoin price periodically every 900 seconds. 9 | class BackgroundTasksBtcPrices extends StatefulWidget { 10 | @override 11 | State createState() => _BtcPricesState(); 12 | } 13 | 14 | class _BtcPricesState extends State { 15 | @override 16 | void initState() { 17 | super.initState(); 18 | _startPolling(); 19 | } 20 | 21 | @override 22 | void dispose() { 23 | // Comments out this line to keep the work running in background 24 | cancelTaskRequest(kTagBtcPricesWork); 25 | super.dispose(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) => Scaffold( 30 | appBar: AppBar( 31 | title: const Text('Bitcoin Price (BackgroundTasks)'), 32 | ), 33 | body: SingleChildScrollView( 34 | child: Container( 35 | alignment: Alignment.center, 36 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48), 37 | child: _buildDashboard(), 38 | ), 39 | ), 40 | ); 41 | 42 | /// Renders the latest Bitcoin price by watching a data file. 43 | Widget _buildDashboard() => StreamBuilder( 44 | stream: btcPriceStream(), 45 | builder: (_, snapshot) => Column( 46 | children: [ 47 | RichText( 48 | textAlign: TextAlign.center, 49 | text: TextSpan( 50 | text: '₿1', 51 | style: TextStyle( 52 | color: Colors.lime.shade700, 53 | fontSize: 56, 54 | height: 1.618, 55 | ), 56 | children: [ 57 | TextSpan( 58 | text: '\n=\n', 59 | style: const TextStyle( 60 | color: Colors.black45, 61 | fontSize: 36, 62 | height: null, 63 | ), 64 | ), 65 | TextSpan( 66 | text: snapshot.hasData 67 | ? NumberFormat.currency(symbol: '\$', decimalDigits: 2) 68 | .format(snapshot.data['amount']) 69 | : '', 70 | style: const TextStyle( 71 | color: Colors.blueAccent, 72 | ), 73 | ), 74 | TextSpan( 75 | text: snapshot.hasData 76 | ? '\nUpdated at: ${DateFormat('hh:mm a, yyyy MMM dd') 77 | .format(DateTime.fromMillisecondsSinceEpoch(snapshot.data['time']))}' 78 | : '', 79 | style: const TextStyle( 80 | color: Colors.black38, 81 | fontSize: 14, 82 | height: null, 83 | ), 84 | ), 85 | ], 86 | ), 87 | ), 88 | ], 89 | ), 90 | ); 91 | 92 | /// Enqueues a work request to poll the price. 93 | void _startPolling() async { 94 | await cancelTaskRequest(kTagBtcPricesWork); // cancel the previous task 95 | await submitTaskRequest(BGAppRefreshTaskRequest(kTagBtcPricesWork)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/flt_worker.dart: -------------------------------------------------------------------------------- 1 | /// A unified and simplified API for scheduling general background tasks. 2 | /// 3 | /// The background tasks scheduler is based on 4 | /// the [BackgroundTasks](https://developer.apple.com/documentation/backgroundtasks) framework on iOS 13+, 5 | /// and the [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager) APIs 6 | /// on Android. 7 | /// 8 | /// For more complex tasks, you may want to use the flatform-specific low-level 9 | /// [work_manager] and [background_tasks] APIs for Android and iOS devices respectively. 10 | library flt_worker; 11 | 12 | import 'dart:async'; 13 | 14 | import 'src/callback_dispatcher.dart'; 15 | import 'src/functions.dart' as impl; 16 | import 'src/models.dart'; 17 | import 'src/utils.dart'; 18 | 19 | export 'src/models.dart'; 20 | 21 | /// Initializes the plugin by registering a [worker] callback. 22 | /// 23 | /// All background work will be dispatched to the [worker] function, 24 | /// which will run in a headless isolate. 25 | /// 26 | /// You may assign different tasks to other functions according to the [input][WorkPayload] of each work. 27 | /// For example: 28 | /// ``` 29 | /// Future worker(WorkPayload payload) { 30 | /// final id = payload.tags.first; 31 | /// switch (id) { 32 | /// case 'task1': 33 | /// return onTask1(); 34 | /// default: 35 | /// return Future.value(); 36 | /// } 37 | /// } 38 | /// 39 | /// ... 40 | /// initializeWorker(worker); 41 | /// ``` 42 | Future initializeWorker(WorkerFn worker) => 43 | apiChannel.invokeMethod( 44 | '$METHOD_PREFIX#initialize', 45 | [ 46 | ensureRawHandle(callbackDispatcher), 47 | ensureRawHandle(worker), 48 | ]); 49 | 50 | /// Enqueues a [intent] to work in the background. 51 | /// 52 | /// You can specify input data and constraints like network or battery status 53 | /// to the background work via the [WorkIntent]. 54 | /// 55 | /// Example: 56 | /// ``` 57 | /// enqueueWorkIntent(WorkIntent( 58 | /// identifier: 'task1', 59 | /// initialDelay: Duration(seconds: 59), 60 | /// constraints: WorkConstraints( 61 | /// networkType: NetworkType.connected, 62 | /// batteryNotLow: true, 63 | /// ), 64 | /// input: { 65 | /// 'counter': counter, 66 | /// }, 67 | /// )); 68 | /// ``` 69 | /// 70 | /// For the iOS platform, all `identifier`s must be registered in the `Info.plist` file, 71 | /// please see the [integration guide](https://github.com/xinthink/flt_worker#integration) for more details. 72 | /// 73 | /// The `identifier` will always be prepended to the `tags` properties, 74 | /// which you can retrieve from the `WorkPayload` when handling the work later. 75 | Future enqueueWorkIntent(WorkIntent intent) => impl.enqueueWorkIntent(intent); 76 | 77 | /// Cancels all unfinished work with the given [identifier]. 78 | /// 79 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run. 80 | Future cancelWork(String identifier) => impl.cancelWork(identifier); 81 | 82 | /// Cancels all unfinished work. 83 | /// 84 | /// **Use this method with extreme caution!** 85 | /// By invoking it, you will potentially affect other modules or libraries in your codebase. 86 | /// It is strongly recommended that you use one of the other cancellation methods at your disposal. 87 | Future cancelAllWork() => impl.cancelAllWork(); 88 | -------------------------------------------------------------------------------- /example/lib/work_manager_btc_prices.dart: -------------------------------------------------------------------------------- 1 | import 'package:flt_worker/android.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import 'btc_price_file.dart'; 6 | 7 | /// An example for using low level `WorkManager` api on the Android platform, 8 | /// which polls Bitcoin price periodically every 900 seconds. 9 | class WorkManagerBtcPrices extends StatefulWidget { 10 | @override 11 | State createState() => _BtcPricesState(); 12 | } 13 | 14 | class _BtcPricesState extends State { 15 | @override 16 | void initState() { 17 | super.initState(); 18 | _startPolling(); 19 | } 20 | 21 | @override 22 | void dispose() { 23 | // Comments out this line to keep the work running in background 24 | cancelAllWorkByTag(kTagBtcPricesWork); 25 | super.dispose(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) => Scaffold( 30 | appBar: AppBar( 31 | title: const Text('Bitcoin Price (WorkManager)'), 32 | ), 33 | body: SingleChildScrollView( 34 | child: Container( 35 | alignment: Alignment.center, 36 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48), 37 | child: _buildDashboard(), 38 | ), 39 | ), 40 | ); 41 | 42 | /// Renders the latest Bitcoin price by watching a data file. 43 | Widget _buildDashboard() => StreamBuilder( 44 | stream: btcPriceStream(), 45 | builder: (_, snapshot) => Column( 46 | children: [ 47 | RichText( 48 | textAlign: TextAlign.center, 49 | text: TextSpan( 50 | text: '₿1', 51 | style: TextStyle( 52 | color: Colors.lime.shade700, 53 | fontSize: 56, 54 | height: 1.618, 55 | ), 56 | children: [ 57 | TextSpan( 58 | text: '\n=\n', 59 | style: const TextStyle( 60 | color: Colors.black45, 61 | fontSize: 36, 62 | height: null, 63 | ), 64 | ), 65 | TextSpan( 66 | text: snapshot.hasData 67 | ? NumberFormat.currency(symbol: '\$', decimalDigits: 2) 68 | .format(snapshot.data['amount']) 69 | : '', 70 | style: const TextStyle( 71 | color: Colors.blueAccent, 72 | ), 73 | ), 74 | TextSpan( 75 | text: snapshot.hasData 76 | ? '\nUpdated at: ${DateFormat('hh:mm a, yyyy MMM dd') 77 | .format(DateTime.fromMillisecondsSinceEpoch(snapshot.data['time']))}' 78 | : '', 79 | style: const TextStyle( 80 | color: Colors.black38, 81 | fontSize: 14, 82 | height: null, 83 | ), 84 | ), 85 | ], 86 | ), 87 | ), 88 | ], 89 | ), 90 | ); 91 | 92 | /// Enqueues a work request to poll the price. 93 | void _startPolling() async { 94 | await cancelAllWorkByTag(kTagBtcPricesWork); // cancel the previous work 95 | await enqueueWorkRequest(const PeriodicWorkRequest( 96 | repeatInterval: Duration(seconds: 30), 97 | tags: [kTagBtcPricesWork], 98 | constraints: WorkConstraints( 99 | networkType: NetworkType.connected, 100 | ), 101 | )); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /example/lib/btc_prices.dart: -------------------------------------------------------------------------------- 1 | import 'package:flt_worker/flt_worker.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import 'btc_price_file.dart'; 6 | 7 | /// An example for using low level `WorkManager` api on the Android platform, 8 | /// which polls Bitcoin price periodically every 900 seconds. 9 | class BtcPrices extends StatefulWidget { 10 | @override 11 | State createState() => _BtcPricesState(); 12 | } 13 | 14 | class _BtcPricesState extends State { 15 | @override 16 | void initState() { 17 | super.initState(); 18 | _startPolling(); 19 | } 20 | 21 | @override 22 | void dispose() { 23 | // Comments out this line to keep the work running in background 24 | cancelWork(kTagBtcPricesWork); 25 | super.dispose(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) => Scaffold( 30 | appBar: AppBar( 31 | title: const Text('Bitcoin Price'), 32 | ), 33 | body: SingleChildScrollView( 34 | child: Container( 35 | alignment: Alignment.center, 36 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48), 37 | child: _buildDashboard(), 38 | ), 39 | ), 40 | ); 41 | 42 | /// Renders the latest Bitcoin price by watching a data file. 43 | Widget _buildDashboard() => StreamBuilder( 44 | stream: btcPriceStream(), 45 | builder: (_, snapshot) => Column( 46 | children: [ 47 | RichText( 48 | textAlign: TextAlign.center, 49 | text: TextSpan( 50 | text: '₿1', 51 | style: TextStyle( 52 | color: Colors.lime.shade700, 53 | fontSize: 56, 54 | height: 1.618, 55 | ), 56 | children: [ 57 | TextSpan( 58 | text: '\n=\n', 59 | style: const TextStyle( 60 | color: Colors.black45, 61 | fontSize: 36, 62 | height: null, 63 | ), 64 | ), 65 | TextSpan( 66 | text: snapshot.hasData 67 | ? NumberFormat.currency(symbol: '\$', decimalDigits: 2) 68 | .format(snapshot.data['amount']) 69 | : '', 70 | style: const TextStyle( 71 | color: Colors.blueAccent, 72 | ), 73 | ), 74 | TextSpan( 75 | text: snapshot.hasData 76 | ? '\nUpdated at: ${DateFormat('hh:mm a, yyyy MMM dd') 77 | .format(DateTime.fromMillisecondsSinceEpoch(snapshot.data['time']))}' 78 | : '', 79 | style: const TextStyle( 80 | color: Colors.black38, 81 | fontSize: 14, 82 | height: null, 83 | ), 84 | ), 85 | ], 86 | ), 87 | ), 88 | ], 89 | ), 90 | ); 91 | 92 | /// Enqueues a work request to poll the price. 93 | void _startPolling() async { 94 | await cancelWork(kTagBtcPricesWork); // cancel the previous work 95 | await enqueueWorkIntent(const WorkIntent( 96 | identifier: kTagBtcPricesWork, 97 | repeatInterval: Duration(seconds: 60), // TODO minimum is 900 on Android 98 | constraints: WorkConstraints( 99 | networkType: NetworkType.connected, 100 | batteryNotLow: true, 101 | ), 102 | )); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /android/src/main/java/dev/thinkng/flt_worker/FltWorkerPlugin.java: -------------------------------------------------------------------------------- 1 | package dev.thinkng.flt_worker; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.Keep; 6 | import androidx.annotation.NonNull; 7 | import androidx.arch.core.util.Function; 8 | 9 | import java.util.List; 10 | 11 | import dev.thinkng.flt_worker.internal.AbsWorkerPlugin; 12 | import dev.thinkng.flt_worker.internal.BackgroundWorkerPlugin; 13 | import io.flutter.plugin.common.MethodCall; 14 | import io.flutter.plugin.common.MethodChannel; 15 | import io.flutter.plugin.common.MethodChannel.Result; 16 | import io.flutter.plugin.common.PluginRegistry; 17 | import io.flutter.plugin.common.PluginRegistry.Registrar; 18 | 19 | /** Main entry of the FltWorkerPlugin, dedicated to main isolate. */ 20 | @Keep 21 | public class FltWorkerPlugin extends AbsWorkerPlugin { 22 | /** 23 | * Provides a callback to register all needed plugins for background workers. 24 | * 25 | * Example: 26 | *
{@code
27 |    * @Override
28 |    * public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
29 |    *   GeneratedPluginRegistrant.registerWith(flutterEngine);
30 |    *   FltWorkerPlugin.registerPluginsForWorkers = registry -> {
31 |    *     if (!registry.hasPlugin("XPlugin")) {
32 |    *       XPlugin.registerWith(registry.registrarFor("XPlugin"));
33 |    *     }
34 |    *     return null;
35 |    *   };
36 |    * }
37 |    * }
38 | *

39 | */ 40 | public static Function registerPluginsForWorkers; 41 | 42 | @SuppressWarnings("unused") 43 | public FltWorkerPlugin() { 44 | super(); 45 | } 46 | 47 | private FltWorkerPlugin(Context context) { 48 | super(context); 49 | } 50 | 51 | // This static function is optional and equivalent to onAttachedToEngine. It supports the old 52 | // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting 53 | // plugin registration via this function while apps migrate to use the new Android APIs 54 | // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. 55 | // 56 | // It is encouraged to share logic between onAttachedToEngine and registerWith to keep 57 | // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called 58 | // depending on the user's project. onAttachedToEngine or registerWith must both be defined 59 | // in the same class. 60 | public static void registerWith(Registrar registrar) { 61 | final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); 62 | channel.setMethodCallHandler(new FltWorkerPlugin(registrar.activity())); 63 | } 64 | 65 | @Override 66 | public boolean handleMethodCall(@NonNull MethodCall call, @NonNull Result result) { 67 | boolean handled = true; 68 | String method = call.method; 69 | if (method.equals(METHOD_PREFIX + "initialize")) { 70 | List args = (List) call.arguments; 71 | if (args.size() > 1) { 72 | Long dispatcherHandler = (Long) args.get(0); 73 | Long workerHandler = (Long) args.get(1); 74 | getPrefs() 75 | .edit() 76 | .putLong("callback_dispatcher_handle", dispatcherHandler) 77 | .putLong("worker_handle", workerHandler) 78 | .apply(); 79 | } 80 | result.success(null); 81 | } else if (method.equals(METHOD_PREFIX + "test")) { 82 | try { 83 | BackgroundWorkerPlugin.getInstance(context).doWork(null); 84 | result.success(null); 85 | } catch (Exception e) { 86 | result.error("E", "worker test failure: " + e.getMessage(), null); 87 | } 88 | } else { 89 | handled = super.handleMethodCall(call, result); 90 | } 91 | return handled; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/src/models.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | import 'constraints.dart'; 6 | 7 | export 'constraints.dart'; 8 | 9 | /// Describes a work request. 10 | /// 11 | /// The name `WorkIntent` is chosen to avoid conflict with the term `WorkRequest` on the Android platform. 12 | @immutable 13 | class WorkIntent { 14 | /// The identifier of the work. 15 | /// 16 | /// Will be prepended to [tags] implicitly, which can be retrieved from [WorkPayload] when handling the work. 17 | /// 18 | /// For the iOS platform, all `identifier`s must be registered in the `Info.plist` file, 19 | /// please see the [integration guide](https://github.com/xinthink/flt_worker#integration) for more details. 20 | final String identifier; 21 | 22 | /// Tags for grouping work. 23 | /// 24 | /// Tags except [identifier] are only available on **Android**. 25 | final Iterable tags; 26 | 27 | /// Input data of the work. 28 | /// 29 | /// Please notice that on iOS, the input data is cached with the key of `identifier`, 30 | /// if you schedule a work before the previous one with the same `identifier` is complete, 31 | /// cached input of the key `identifier` will be overwritten. 32 | final Map input; 33 | 34 | /// The duration of initial delay of the work. 35 | final Duration initialDelay; 36 | 37 | /// Constraints for the work to run. 38 | final WorkConstraints constraints; 39 | 40 | /// **iOS** only, if `true`, requests to schedule a `BGProcessingTaskRequest`, 41 | /// otherwise defaults to `BGAppRefreshTaskRequest`. 42 | final bool isProcessingTask; 43 | 44 | /// **Android** only. The repeat interval of a periodic work request. 45 | final Duration repeatInterval; 46 | 47 | /// **Android** only. The duration for which the work repeats from the end of the [repeatInterval]. 48 | /// 49 | /// Note that flex intervals are ignored for certain Android OS versions (in particular, API 23). 50 | final Duration flexInterval; 51 | 52 | /// Instantiates a [WorkIntent] with an [identifier]. 53 | /// 54 | /// Optional properties include [tags], [input] data and an [initialDelay]. 55 | const WorkIntent({ 56 | @required this.identifier, 57 | this.tags, 58 | this.input, 59 | this.initialDelay, 60 | this.constraints, 61 | this.isProcessingTask, 62 | this.repeatInterval, 63 | this.flexInterval, 64 | }); 65 | } 66 | 67 | /// Payload of a background work. 68 | @immutable 69 | class WorkPayload { 70 | /// Id of the work. 71 | /// 72 | /// It's the BGTask identifier on iOS, and work **UUID** on Android. 73 | /// 74 | /// Please notice that it's **NOT** the `identifier` you specify in the `WorkIntent` 75 | /// when you schedule the work on Android devices. 76 | /// Retrieves the `identifier` from `tags` instead. 77 | final String id; 78 | 79 | /// Tags of the work. 80 | /// 81 | /// Tags except [identifier] are only available on **Android**. 82 | final Iterable tags; 83 | 84 | /// Input of the work. 85 | final Map input; 86 | 87 | /// Instantiates the payload for a work. 88 | const WorkPayload._({this.id, this.tags, this.input}); 89 | 90 | /// Decodes the input json into a [WorkPayload]. 91 | factory WorkPayload.fromJson(Map json) { 92 | Map input = json['input'] ?? {}; 93 | String inputJson = input['data']; 94 | input = inputJson?.isNotEmpty == true ? jsonDecode(inputJson) : {}; 95 | return WorkPayload._( 96 | id: json['id'], 97 | tags: Iterable.castFrom(json['tags'] ?? []), 98 | input: input, 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | generated_key_values = {} 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) do |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | generated_key_values[podname] = podpath 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | end 32 | generated_key_values 33 | end 34 | 35 | target 'Runner' do 36 | use_frameworks! 37 | use_modular_headers! 38 | 39 | # Flutter Pod 40 | 41 | copied_flutter_dir = File.join(__dir__, 'Flutter') 42 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') 43 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') 44 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) 45 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. 46 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. 47 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. 48 | 49 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') 50 | unless File.exist?(generated_xcode_build_settings_path) 51 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" 52 | end 53 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) 54 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; 55 | 56 | unless File.exist?(copied_framework_path) 57 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) 58 | end 59 | unless File.exist?(copied_podspec_path) 60 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) 61 | end 62 | end 63 | 64 | # Keep pod path relative so it can be checked into Podfile.lock. 65 | pod 'Flutter', :path => 'Flutter' 66 | 67 | # Plugin Pods 68 | 69 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 70 | # referring to absolute paths on developers' machines. 71 | system('rm -rf .symlinks') 72 | system('mkdir -p .symlinks/plugins') 73 | plugin_pods = parse_KV_file('../.flutter-plugins') 74 | plugin_pods.each do |name, path| 75 | symlink = File.join('.symlinks', 'plugins', name) 76 | File.symlink(path, symlink) 77 | pod name, :path => File.join(symlink, 'ios') 78 | end 79 | end 80 | 81 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 82 | install! 'cocoapods', :disable_input_output_paths => true 83 | 84 | post_install do |installer| 85 | installer.pods_project.targets.each do |target| 86 | target.build_configurations.each do |config| 87 | config.build_settings['ENABLE_BITCODE'] = 'NO' 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /example/lib/rest.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show HttpException; 2 | import 'dart:convert' show Encoding, jsonDecode, jsonEncode; 3 | 4 | import 'package:meta/meta.dart' show required; 5 | import 'package:http/http.dart' as http; 6 | 7 | typedef ErrorCheck = http.Response Function(http.Response); 8 | typedef BodyParser = T Function(String body); 9 | 10 | /// A simple [BodyParser] doing nothing. 11 | T ignoreBody(String body) => null; 12 | 13 | /// A simple [BodyParser] returning response body as is. 14 | String stringBody(String body) => body; 15 | 16 | /// A [BodyParser] parsing a `JSON` response. 17 | dynamic jsonBody(String body) => jsonDecode(body); 18 | 19 | /// Issues a `HTTP GET` request for [uri]. 20 | /// 21 | /// with optional [headers], 22 | /// customized [bodyParser] to parse the response body, defaults to *ignoring*, 23 | /// customized [errorCheck] handling HTTP status, `2xx` considered successful by default. 24 | Future get(uri, { 25 | Map headers, 26 | BodyParser bodyParser, 27 | ErrorCheck errorCheck, 28 | }) { 29 | print('GET $uri headers=$headers'); 30 | return http.get(uri, headers: headers) 31 | .then(errorCheck ?? _checkHttpError) 32 | .then((resp) => (bodyParser ?? ignoreBody)(resp.body)); 33 | } 34 | 35 | /// `GET` a `JSON` response from [uri]. 36 | /// 37 | /// with optional [headers], 38 | /// customized [errorCheck] handling HTTP status, `2xx` considered successful by default. 39 | Future getJson(uri, { 40 | Map headers, 41 | ErrorCheck errorCheck, 42 | }) => get(uri, headers: headers, bodyParser: jsonBody, errorCheck: errorCheck); 43 | 44 | /// `HTTP POST` [body] to [uri]. 45 | /// 46 | /// with optional body [encoding] and [headers], 47 | /// customized [bodyParser] to parse the response body, defaults to *ignoring*, 48 | /// customized [errorCheck] handling HTTP status, `2xx` considered successful by default. 49 | /// 50 | /// see also: [http.post] 51 | Future post(uri, { 52 | @required dynamic body, 53 | Encoding encoding, 54 | Map headers, 55 | BodyParser bodyParser, 56 | ErrorCheck errorCheck, 57 | }) { 58 | print('POST $uri headers=$headers'); 59 | return http.post(uri, 60 | body: body, 61 | encoding: encoding, 62 | headers: headers, 63 | ) 64 | .then(errorCheck ?? _checkHttpError) 65 | .then((resp) => (bodyParser ?? ignoreBody)(resp.body)); 66 | } 67 | 68 | /// `HTTP POST` a JSON [body] to [uri]. 69 | /// 70 | /// with optional body [encoding] and [headers], 71 | /// customized [bodyParser] to parse the response body, defaults to *ignoring*, 72 | /// customized [errorCheck] handling HTTP status, `2xx` considered successful by default. 73 | /// 74 | /// see also: [http.post] 75 | Future postJson(uri, { 76 | @required dynamic body, 77 | Encoding encoding, 78 | Map headers, 79 | ErrorCheck errorCheck, 80 | }) => post( 81 | uri, 82 | body: jsonEncode(body), 83 | encoding: encoding, 84 | headers: _mergeHeaders({ 85 | 'Content-type': 'application/json', 86 | }, headers), 87 | bodyParser: jsonBody, 88 | errorCheck: errorCheck, 89 | ); 90 | 91 | /// Checking HTTP status code for failures 92 | http.Response _checkHttpError(http.Response resp) { 93 | if (resp.statusCode < 200 || resp.statusCode >= 300) { 94 | throw HttpException('${resp.request.method} ${resp.request.url} failed: ${resp.statusCode}'); 95 | } 96 | return resp; 97 | } 98 | 99 | /// Merge [extra] headers into the [base] one 100 | Map _mergeHeaders(Map base, Map extra) { 101 | final headers = {}; 102 | if (base != null) headers.addAll(base); 103 | if (extra != null) headers.addAll(extra); 104 | return headers; 105 | } 106 | -------------------------------------------------------------------------------- /ios/Classes/internal/BGTaskHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // BGTaskHandler.m 3 | // flt_worker 4 | // 5 | // Created by Yingxin Wu on 2020/3/13. 6 | // 7 | 8 | #import "BGTaskHandler.h" 9 | #import "BGTaskMgrDelegate.h" 10 | 11 | static BGTaskHandler *_instance = nil; 12 | static FuncRegisterPlugins _registerPlugins = nil; 13 | 14 | @implementation BGTaskHandler { 15 | BGTaskMgrDelegate *_delegate; 16 | FlutterEngine *_headlessEngine; 17 | BOOL _headlessEngineStarted; 18 | } 19 | 20 | + (FuncRegisterPlugins) registerPlugins { 21 | return _registerPlugins; 22 | } 23 | 24 | + (void) setRegisterPlugins:(FuncRegisterPlugins)registerPlugins { 25 | _registerPlugins = registerPlugins; 26 | } 27 | 28 | + (void)registerWithRegistrar:(nonnull NSObject *)registrar { 29 | } 30 | 31 | + (BGTaskHandler*) instance { 32 | @synchronized (self) { 33 | if (_instance == nil) { 34 | _instance = [[BGTaskHandler alloc] init]; 35 | } 36 | } 37 | return _instance; 38 | } 39 | 40 | - (instancetype)init { 41 | self = [super init]; 42 | if (self) { 43 | // init a headless engine instance for callback 44 | _headlessEngine = [[FlutterEngine alloc] initWithName:@"flt_worker_isolate" 45 | project:nil 46 | allowHeadlessExecution:YES]; 47 | [self startCallbackDispatcher]; 48 | 49 | // methodCallHandler must be set on a running engine 50 | _delegate = [[BGTaskMgrDelegate alloc] initWithEngine:_headlessEngine]; 51 | [_delegate.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) { 52 | [self handleMethodCall:call result:result]; 53 | }]; 54 | } 55 | return self; 56 | } 57 | 58 | - (void)handleMethodCall:(FlutterMethodCall * _Nonnull)call result:(FlutterResult _Nonnull)result { 59 | if (![_delegate handleMethodCall:call result:result]) { 60 | result(FlutterMethodNotImplemented); 61 | } 62 | } 63 | 64 | /** Start a headless engine instance with the entry handle */ 65 | - (void)startCallbackDispatcher { 66 | @synchronized (self) { 67 | if (!_headlessEngineStarted) { 68 | int64_t handle = dispatcherHandle(); 69 | FlutterCallbackInformation *cbInfo = [FlutterCallbackCache lookupCallbackInformation:handle]; 70 | if (cbInfo == nil) { 71 | NSLog(@"callback not found for handle: %lld", handle); 72 | return; 73 | } 74 | 75 | [_headlessEngine runWithEntrypoint:cbInfo.callbackName libraryURI:cbInfo.callbackLibraryPath]; 76 | _registerPlugins(_headlessEngine); 77 | _headlessEngineStarted = YES; 78 | } 79 | } 80 | } 81 | 82 | - (void)handleBGTask:(BGTask * _Nonnull)task { 83 | NSString *identifier = task.identifier; 84 | NSLog(@"Handling BGTask id=%@", identifier); 85 | 86 | int64_t handle = workerHandle(); 87 | [_delegate.methodChannel invokeMethod:@API_METHOD(dispatch) 88 | arguments:@[ 89 | @(handle), 90 | [_delegate packPayloadForTask:identifier], 91 | ] 92 | result:^(id _Nullable result) { 93 | if ([result isKindOfClass:[FlutterError class]]) { 94 | NSLog(@"BGTask '%@' execution failed: %@ %@", identifier, ((FlutterError*) result).code, ((FlutterError*) result).message); 95 | [task setTaskCompletedWithSuccess:NO]; 96 | } else if (result == FlutterMethodNotImplemented) { 97 | NSLog(@"Dart worker for BGTask '%@' is NOT implemented", identifier); 98 | [task setTaskCompletedWithSuccess:NO]; 99 | } else { 100 | NSLog(@"BGTask '%@' done. result=%@", identifier, result); 101 | [task setTaskCompletedWithSuccess:YES]; 102 | } 103 | }]; 104 | 105 | task.expirationHandler = ^{ 106 | NSLog(@"WARN: BGTask expired. id=%@", identifier); 107 | }; 108 | } 109 | 110 | @end 111 | -------------------------------------------------------------------------------- /android/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | xmlns:android 21 | 22 | ^$ 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 | xmlns:.* 32 | 33 | ^$ 34 | 35 | 36 | BY_NAME 37 | 38 |
39 |
40 | 41 | 42 | 43 | .*:id 44 | 45 | http://schemas.android.com/apk/res/android 46 | 47 | 48 | 49 |
50 |
51 | 52 | 53 | 54 | .*:name 55 | 56 | http://schemas.android.com/apk/res/android 57 | 58 | 59 | 60 |
61 |
62 | 63 | 64 | 65 | name 66 | 67 | ^$ 68 | 69 | 70 | 71 |
72 |
73 | 74 | 75 | 76 | style 77 | 78 | ^$ 79 | 80 | 81 | 82 |
83 |
84 | 85 | 86 | 87 | .* 88 | 89 | ^$ 90 | 91 | 92 | BY_NAME 93 | 94 |
95 |
96 | 97 | 98 | 99 | .* 100 | 101 | http://schemas.android.com/apk/res/android 102 | 103 | 104 | ANDROID_ATTRIBUTE_ORDER 105 | 106 |
107 |
108 | 109 | 110 | 111 | .* 112 | 113 | .* 114 | 115 | 116 | BY_NAME 117 | 118 |
119 |
120 |
121 |
122 |
123 |
-------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flt_worker/flt_worker.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | import 'background_tasks_btc_prices.dart'; 7 | import 'background_tasks_counter.dart'; 8 | import 'btc_prices.dart'; 9 | import 'counter.dart'; 10 | import 'work_manager_btc_prices.dart'; 11 | import 'work_manager_counter.dart'; 12 | import 'worker.dart'; 13 | 14 | /// Background processing examples. 15 | /// 16 | /// Force start iOS background tasks: 17 | /// ``` 18 | /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.btc_prices_task"] 19 | /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.counter_task"] 20 | /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.task1"] 21 | /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"dev.example.task2"] 22 | /// ``` 23 | /// 24 | /// > See [iOS documentations](https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development) 25 | void main() { 26 | runApp(MyApp()); 27 | initializeWorker(worker); 28 | } 29 | 30 | class MyApp extends StatelessWidget { 31 | @override 32 | Widget build(BuildContext context) => MaterialApp( 33 | home: Scaffold( 34 | appBar: AppBar( 35 | title: const Text('Worker Examples'), 36 | ), 37 | body: Builder( 38 | builder: (context) => SingleChildScrollView( 39 | child: Container( 40 | padding: const EdgeInsets.symmetric(vertical: 48, horizontal: 32), 41 | child: DefaultTextStyle( 42 | textAlign: TextAlign.center, 43 | style: const TextStyle(), 44 | child: Column( 45 | mainAxisSize: MainAxisSize.min, 46 | crossAxisAlignment: CrossAxisAlignment.stretch, 47 | children: [ 48 | const Text('High level API examples', 49 | style: TextStyle( 50 | fontSize: 14, 51 | color: Colors.black45, 52 | ), 53 | ), 54 | const SizedBox(height: 16), 55 | RaisedButton( 56 | child: const Text('Counter'), 57 | onPressed: () => Navigator.push(context, MaterialPageRoute( 58 | builder: (_) => Counter(), 59 | )), 60 | ), 61 | RaisedButton( 62 | child: const Text('Bitcoin price polling'), 63 | onPressed: () => Navigator.push(context, MaterialPageRoute( 64 | builder: (_) => BtcPrices(), 65 | )), 66 | ), 67 | Padding( 68 | padding: const EdgeInsets.only(top: 48, bottom: 16), 69 | child: const Text('Low level platform-specific API examples', 70 | style: TextStyle( 71 | fontSize: 14, 72 | color: Colors.black45, 73 | ), 74 | ), 75 | ), 76 | ...(Platform.isAndroid ? _workManagerExamples(context) : _bgTasksExamples(context)), 77 | ], 78 | ), 79 | ), 80 | ), 81 | ), 82 | ), 83 | ), 84 | ); 85 | } 86 | 87 | List _workManagerExamples(BuildContext context) => [ 88 | RaisedButton( 89 | child: const Text('Counter\n(OneTimeWorkRequest)'), 90 | onPressed: () => Navigator.push(context, MaterialPageRoute( 91 | builder: (_) => WorkManagerCounter(), 92 | )), 93 | ), 94 | RaisedButton( 95 | child: const Text('Bitcoin price polling\n(PeriodicWorkRequest)'), 96 | onPressed: () => Navigator.push(context, MaterialPageRoute( 97 | builder: (_) => WorkManagerBtcPrices(), 98 | )), 99 | ), 100 | ]; 101 | 102 | List _bgTasksExamples(BuildContext context) => [ 103 | RaisedButton( 104 | child: const Text('Counter\n(BGProcessingTaskRequest)'), 105 | onPressed: () { 106 | Navigator.push(context, MaterialPageRoute( 107 | builder: (_) => BackgroundTasksCounter(), 108 | )); 109 | }, 110 | ), 111 | RaisedButton( 112 | child: const Text('Bitcoin price polling\n(BGAppRefreshTaskRequest)'), 113 | onPressed: () { 114 | Navigator.push(context, MaterialPageRoute( 115 | builder: (_) => BackgroundTasksBtcPrices(), 116 | )); 117 | }, 118 | ), 119 | ]; 120 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.1.3" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | image: 71 | dependency: transitive 72 | description: 73 | name: image 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "2.1.4" 77 | matcher: 78 | dependency: transitive 79 | description: 80 | name: matcher 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "0.12.6" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "1.1.8" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.flutter-io.cn" 96 | source: hosted 97 | version: "1.6.4" 98 | pedantic: 99 | dependency: "direct dev" 100 | description: 101 | name: pedantic 102 | url: "https://pub.flutter-io.cn" 103 | source: hosted 104 | version: "1.9.0" 105 | petitparser: 106 | dependency: transitive 107 | description: 108 | name: petitparser 109 | url: "https://pub.flutter-io.cn" 110 | source: hosted 111 | version: "2.4.0" 112 | quiver: 113 | dependency: transitive 114 | description: 115 | name: quiver 116 | url: "https://pub.flutter-io.cn" 117 | source: hosted 118 | version: "2.0.5" 119 | sky_engine: 120 | dependency: transitive 121 | description: flutter 122 | source: sdk 123 | version: "0.0.99" 124 | source_span: 125 | dependency: transitive 126 | description: 127 | name: source_span 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "1.5.5" 131 | stack_trace: 132 | dependency: transitive 133 | description: 134 | name: stack_trace 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.9.3" 138 | stream_channel: 139 | dependency: transitive 140 | description: 141 | name: stream_channel 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "2.0.0" 145 | string_scanner: 146 | dependency: transitive 147 | description: 148 | name: string_scanner 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "1.0.5" 152 | term_glyph: 153 | dependency: transitive 154 | description: 155 | name: term_glyph 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "1.1.0" 159 | test_api: 160 | dependency: transitive 161 | description: 162 | name: test_api 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "0.2.15" 166 | typed_data: 167 | dependency: transitive 168 | description: 169 | name: typed_data 170 | url: "https://pub.flutter-io.cn" 171 | source: hosted 172 | version: "1.1.6" 173 | vector_math: 174 | dependency: transitive 175 | description: 176 | name: vector_math 177 | url: "https://pub.flutter-io.cn" 178 | source: hosted 179 | version: "2.0.8" 180 | xml: 181 | dependency: transitive 182 | description: 183 | name: xml 184 | url: "https://pub.flutter-io.cn" 185 | source: hosted 186 | version: "3.5.0" 187 | sdks: 188 | dart: ">=2.4.0 <3.0.0" 189 | flutter: ">=1.10.0 <2.0.0" 190 | -------------------------------------------------------------------------------- /lib/src/work_manager/models.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | 6 | import '../constraints.dart'; 7 | 8 | export '../constraints.dart'; 9 | 10 | /// An abstract class representing a work request. 11 | @immutable 12 | abstract class WorkRequest { 13 | /// Tags for grouping work. 14 | final Iterable tags; 15 | 16 | /// Input data of the work. 17 | final Map input; 18 | 19 | /// The duration of initial delay of the work. 20 | final Duration initialDelay; 21 | 22 | /// Constraints for the work to run. 23 | final WorkConstraints constraints; 24 | 25 | /// Sets the backoff policy and backoff delay for the work. 26 | final BackoffCriteria backoffCriteria; 27 | 28 | /// Instantiates a WorkRequest with optional [tags] and [input] data. 29 | /// 30 | /// Optionally provides an [initialDelay]. 31 | const WorkRequest({ 32 | this.tags, 33 | this.input, 34 | this.initialDelay, 35 | this.constraints, 36 | this.backoffCriteria, 37 | }); 38 | 39 | /// Serializes this work request into a json object. 40 | Map toJson() => { 41 | 'type': (this is OneTimeWorkRequest) ? 'OneTime' : 'Periodic', 42 | 'tags': tags, 43 | 'input': { 44 | 'data': jsonEncode(input ?? {}), // always encode the input data 45 | }, 46 | 'initialDelay': max(initialDelay?.inMicroseconds ?? 0, 0), 47 | 'constraints': constraints?.toJson(), 48 | 'backoffCriteria': backoffCriteria?.toJson(), 49 | }; 50 | } 51 | 52 | /// Defines an one-off work request. 53 | @immutable 54 | class OneTimeWorkRequest extends WorkRequest { 55 | /// Instantiates an [OneTimeWorkRequest]. 56 | /// 57 | /// With optional [tags] and [input] data. 58 | const OneTimeWorkRequest({ 59 | Iterable tags, 60 | Map input, 61 | Duration initialDelay, 62 | WorkConstraints constraints, 63 | BackoffCriteria backoffCriteria, 64 | }) : super( 65 | tags: tags, 66 | input: input, 67 | initialDelay: initialDelay, 68 | constraints: constraints, 69 | backoffCriteria: backoffCriteria, 70 | ); 71 | } 72 | 73 | /// Defines a periodic work request. 74 | @immutable 75 | class PeriodicWorkRequest extends WorkRequest { 76 | /// The repeat interval 77 | final Duration repeatInterval; 78 | 79 | /// The duration for which the work repeats from the end of the [repeatInterval]. 80 | /// 81 | /// Note that flex intervals are ignored for certain OS versions (in particular, API 23). 82 | final Duration flexInterval; 83 | 84 | /// Creates a [PeriodicWorkRequest] to run periodically once every [repeatInterval] period 85 | /// , with an optional [flexInterval]. 86 | const PeriodicWorkRequest({ 87 | @required this.repeatInterval, 88 | this.flexInterval, 89 | Iterable tags, 90 | Map input, 91 | Duration initialDelay, 92 | WorkConstraints constraints, 93 | BackoffCriteria backoffCriteria, 94 | }) : super( 95 | tags: tags, 96 | input: input, 97 | initialDelay: initialDelay, 98 | constraints: constraints, 99 | backoffCriteria: backoffCriteria, 100 | ); 101 | 102 | @override 103 | Map toJson() => super.toJson() 104 | ..['repeatInterval'] = repeatInterval.inMicroseconds 105 | ..['flexInterval'] = flexInterval?.inMicroseconds; 106 | } 107 | 108 | /// Sets the backoff policy and backoff delay for the work. 109 | @immutable 110 | class BackoffCriteria { 111 | /// Backoff policy for the work. 112 | /// 113 | /// The default value is dependent on the native `WorkManager`, 114 | /// which should be [BackoffPolicy.exponential] according to 115 | /// the [documentation](https://developer.android.com/reference/androidx/work/WorkRequest.Builder#setBackoffCriteria(androidx.work.BackoffPolicy,%20long,%20java.util.concurrent.TimeUnit)). 116 | final BackoffPolicy policy; 117 | 118 | /// Backoff backoff delay for the work. 119 | /// 120 | /// The default value and the valid range is dependent on the native `WorkManager`. 121 | /// According to the [documentation](https://developer.android.com/reference/androidx/work/WorkRequest.Builder#setBackoffCriteria(androidx.work.BackoffPolicy,%20long,%20java.util.concurrent.TimeUnit)) 122 | /// it defaults to `30` seconds, and will be clamped between `10` seconds and `5` hours. 123 | final Duration delay; 124 | 125 | /// Creates a [BackoffCriteria] with the backoff [policy] and backoff [delay]. 126 | const BackoffCriteria({ 127 | @required this.policy, 128 | @required this.delay, 129 | }); 130 | 131 | /// Serializes this backoff criteria into a json object. 132 | Map toJson() => { 133 | 'policy': policy?.index, 134 | 'delay': delay?.inMicroseconds, 135 | }; 136 | } 137 | 138 | /// An enumeration of backoff policies when retrying work. 139 | /// 140 | /// TODO: These policies are used when you have a return ListenableWorker.Result.retry() from a worker 141 | /// to determine the correct backoff time. 142 | enum BackoffPolicy { 143 | /// Indicates that the backoff time should be increased exponentially. 144 | exponential, 145 | 146 | /// Indicates that the backoff time should be increased linearly. 147 | linear, 148 | } 149 | -------------------------------------------------------------------------------- /android/src/main/java/dev/thinkng/flt_worker/internal/BackgroundWorkerPlugin.java: -------------------------------------------------------------------------------- 1 | package dev.thinkng.flt_worker.internal; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.Keep; 7 | import androidx.annotation.Nullable; 8 | import androidx.annotation.UiThread; 9 | import androidx.work.Worker; 10 | 11 | import java.util.Arrays; 12 | import java.util.HashMap; 13 | import java.util.LinkedList; 14 | import java.util.Map; 15 | import java.util.concurrent.ExecutionException; 16 | import java.util.concurrent.Future; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | import dev.thinkng.flt_worker.FltWorkerPlugin; 20 | import io.flutter.plugin.common.MethodChannel; 21 | import io.flutter.plugin.common.PluginRegistry; 22 | import io.flutter.view.FlutterCallbackInformation; 23 | import io.flutter.view.FlutterMain; 24 | import io.flutter.view.FlutterNativeView; 25 | import io.flutter.view.FlutterRunArguments; 26 | 27 | /** WorkerPlugin dedicated to the background isolate. */ 28 | @Keep 29 | public class BackgroundWorkerPlugin extends AbsWorkerPlugin { 30 | private static BackgroundWorkerPlugin instance; 31 | 32 | public static BackgroundWorkerPlugin getInstance(Context context) { 33 | synchronized (BackgroundWorkerPlugin.class) { 34 | if (instance == null) { 35 | instance = new BackgroundWorkerPlugin(context); 36 | } 37 | } 38 | return instance; 39 | } 40 | 41 | // The headless Flutter instance to run the callbacks. 42 | private FlutterNativeView headlessView; 43 | private final AtomicBoolean headlessViewStarted = new AtomicBoolean(); 44 | 45 | private BackgroundWorkerPlugin(Context context) { 46 | super(context); 47 | } 48 | 49 | /** 50 | * Callback for doing the background work. 51 | */ 52 | public Future doWork(@Nullable Worker worker) throws ExecutionException, InterruptedException { 53 | if (worker != null) { 54 | Log.d(TAG, "executing Work id=" + worker.getId() + " tags=" + worker.getTags()); 55 | } else { 56 | Log.d(TAG, "executing an empty Work (test only)"); 57 | } 58 | 59 | runOnMainThread(new Runnable() { 60 | @Override 61 | public void run() { 62 | startHeadlessEngine(context); 63 | } 64 | }).get(); 65 | return dispatchCallback(getPrefs().getLong("worker_handle", 0), worker); 66 | } 67 | 68 | @UiThread 69 | private void startHeadlessEngine(Context context) { 70 | synchronized (headlessViewStarted) { 71 | if (headlessView == null) { 72 | long handle = getPrefs().getLong("callback_dispatcher_handle", 0); 73 | FlutterCallbackInformation cbInfo = FlutterCallbackInformation.lookupCallbackInformation(handle); 74 | //noinspection ConstantConditions 75 | if (cbInfo == null) { 76 | Log.w(TAG, "callback dispatcher handle not found!"); 77 | return; 78 | } 79 | 80 | headlessView = new FlutterNativeView(context, true); 81 | // register plugins to the callback isolate 82 | registerPluginsForHeadlessView(); 83 | 84 | FlutterRunArguments args = new FlutterRunArguments(); 85 | args.bundlePath = FlutterMain.findAppBundlePath(); 86 | args.entrypoint = cbInfo.callbackName; 87 | args.libraryPath = cbInfo.callbackLibraryPath; 88 | headlessView.runFromBundle(args); 89 | } 90 | 91 | if (channel != null) { 92 | channel.setMethodCallHandler(null); 93 | } 94 | channel = new MethodChannel(headlessView, CHANNEL_NAME); 95 | channel.setMethodCallHandler(this); 96 | } 97 | } 98 | 99 | /* Register plugins for the headless isolate */ 100 | private void registerPluginsForHeadlessView() { 101 | PluginRegistry registry = headlessView.getPluginRegistry(); 102 | // if (!registry.hasPlugin("FltWorkerPlugin")) { 103 | // PluginRegistry.Registrar registrar = registry.registrarFor("FltWorkerPlugin"); 104 | // new MethodChannel(registrar.messenger(), CHANNEL_NAME) 105 | // .setMethodCallHandler(this); 106 | // } 107 | 108 | if (FltWorkerPlugin.registerPluginsForWorkers != null) { 109 | FltWorkerPlugin.registerPluginsForWorkers.apply(registry); 110 | } 111 | } 112 | 113 | /** Dispatch a callback function call */ 114 | private Future dispatchCallback(final long handle, 115 | @Nullable final Worker worker) 116 | throws InterruptedException, ExecutionException { 117 | final Map payload = new HashMap<>(); 118 | if (worker != null) { 119 | // worker is null only if it's a testing request 120 | payload.put("id", worker.getId().toString()); 121 | payload.put("input", worker.getInputData().getKeyValueMap()); 122 | 123 | LinkedList tags = new LinkedList<>(); 124 | for (String tag : worker.getTags()) { 125 | if (!tag.startsWith("dev.thinkng.flt_worker")) { 126 | tags.add(tag); 127 | } 128 | } 129 | payload.put("tags", tags); 130 | } 131 | 132 | final MethodCallFuture callback = new MethodCallFuture<>(); 133 | runOnMainThread(new Runnable() { 134 | @Override 135 | public void run() { 136 | channel.invokeMethod(METHOD_PREFIX + "dispatch", 137 | Arrays.asList(handle, payload), 138 | callback); 139 | } 140 | }).get(); 141 | return callback; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /android/src/test/java/dev/thinkng/flt_worker/internal/WorkRequestsTest.java: -------------------------------------------------------------------------------- 1 | package dev.thinkng.flt_worker.internal; 2 | 3 | import android.os.Build; 4 | 5 | import androidx.work.BackoffPolicy; 6 | import androidx.work.Constraints; 7 | import androidx.work.NetworkType; 8 | import androidx.work.OneTimeWorkRequest; 9 | import androidx.work.PeriodicWorkRequest; 10 | import androidx.work.WorkRequest; 11 | 12 | import org.junit.Test; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Arrays; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | import static org.junit.Assert.*; 20 | 21 | public class WorkRequestsTest { 22 | 23 | @Test 24 | public void parseOneTimeWorkRequest() { 25 | Map json = new HashMap<>(); 26 | json.put("tags", Arrays.asList("test", "work")); 27 | json.put("initialDelay", 10000000); // 10 seconds in microseconds 28 | 29 | WorkRequest req = WorkRequests.parseRequest(json); 30 | 31 | assertTrue(req instanceof OneTimeWorkRequest); 32 | assertEquals(Arrays.asList(BackgroundWorker.class.getName(), "test", "work"), 33 | new ArrayList<>(req.getTags())); 34 | assertEquals(10000, req.getWorkSpec().initialDelay); // 10 seconds in milliseconds 35 | } 36 | 37 | @Test 38 | public void parsePeriodicWorkRequest() { 39 | Map json = new HashMap<>(); 40 | json.put("type", "Periodic"); 41 | json.put("repeatInterval", 86400 * 1000000L); // 1 day in microseconds 42 | json.put("tags", Arrays.asList("test", "periodic")); 43 | json.put("initialDelay", 10000000); // 10 seconds in microseconds 44 | 45 | WorkRequest req = WorkRequests.parseRequest(json); 46 | 47 | assertTrue(req instanceof PeriodicWorkRequest); 48 | assertEquals(Arrays.asList(BackgroundWorker.class.getName(), "test", "periodic"), 49 | new ArrayList<>(req.getTags())); 50 | assertEquals(10000, req.getWorkSpec().initialDelay); // 10 seconds in milliseconds 51 | assertEquals(86400 * 1000L, req.getWorkSpec().intervalDuration); // 1 day in milliseconds 52 | } 53 | 54 | @Test 55 | public void parseBackoffCriteria() { 56 | Map json = new HashMap<>(); 57 | Map backoffCriteriaJson = new HashMap<>(); 58 | backoffCriteriaJson.put("policy", BackoffPolicy.LINEAR.ordinal()); 59 | backoffCriteriaJson.put("delay", 12000000); // 12 seconds in microseconds 60 | json.put("backoffCriteria", backoffCriteriaJson); 61 | 62 | WorkRequest req = WorkRequests.parseRequest(json); 63 | 64 | assertEquals(BackoffPolicy.LINEAR, req.getWorkSpec().backoffPolicy); 65 | assertEquals(12000, req.getWorkSpec().backoffDelayDuration); // 12 seconds in milliseconds 66 | } 67 | 68 | @Test 69 | public void parseInvalidBackoffCriteria() { 70 | // empty criteria 71 | Map json = new HashMap<>(); 72 | Map backoffCriteriaJson = new HashMap<>(); 73 | json.put("backoffCriteria", backoffCriteriaJson); 74 | 75 | WorkRequest req = WorkRequests.parseRequest(json); 76 | 77 | // should fallback to defaults 78 | assertEquals(BackoffPolicy.EXPONENTIAL, req.getWorkSpec().backoffPolicy); 79 | 80 | // the same should happen to an incomplete criteria 81 | backoffCriteriaJson.put("policy", BackoffPolicy.LINEAR.ordinal()); 82 | req = WorkRequests.parseRequest(json); 83 | assertEquals(BackoffPolicy.EXPONENTIAL, req.getWorkSpec().backoffPolicy); 84 | 85 | json.clear(); 86 | backoffCriteriaJson.put("delay", 12000000); 87 | req = WorkRequests.parseRequest(json); 88 | assertEquals(BackoffPolicy.EXPONENTIAL, req.getWorkSpec().backoffPolicy); 89 | assertEquals(WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS, req.getWorkSpec().backoffDelayDuration); 90 | } 91 | 92 | @SuppressWarnings("ConstantConditions") 93 | @Test 94 | public void parseConstraints() { 95 | Map constraintsJson = new HashMap<>(); 96 | constraintsJson.put("networkType", NetworkType.NOT_ROAMING.ordinal()); 97 | constraintsJson.put("batteryNotLow", true); 98 | constraintsJson.put("charging", null); 99 | constraintsJson.put("deviceIdle", true); 100 | constraintsJson.put("storageNotLow", false); 101 | 102 | Map reqJson = new HashMap<>(); 103 | reqJson.put("constraints", constraintsJson); 104 | WorkRequest req = WorkRequests.parseRequest(reqJson); 105 | 106 | assertTrue("should has constraints", req.getWorkSpec().hasConstraints()); 107 | final Constraints constraints = req.getWorkSpec().constraints; 108 | assertEquals(NetworkType.NOT_ROAMING, constraints.getRequiredNetworkType()); 109 | assertTrue(constraints.requiresBatteryNotLow()); 110 | if (Build.VERSION.SDK_INT >= 23) assertTrue(constraints.requiresDeviceIdle()); 111 | assertFalse(constraints.requiresStorageNotLow()); 112 | assertFalse(constraints.requiresCharging()); // fallback to defaults 113 | } 114 | 115 | @Test 116 | public void parseFlexInterval() { 117 | Map json = new HashMap<>(); 118 | json.put("type", "Periodic"); 119 | json.put("repeatInterval", 86400 * 1000000L); // 1 day in microseconds 120 | json.put("flexInterval", 600 * 1000000L); // 10 minutes in microseconds 121 | 122 | WorkRequest req = WorkRequests.parseRequest(json); 123 | 124 | assertTrue(req instanceof PeriodicWorkRequest); 125 | assertEquals(86400 * 1000L, req.getWorkSpec().intervalDuration); // 1 day in milliseconds 126 | assertEquals(600 * 1000L, req.getWorkSpec().flexDuration); // 10 minutes in milliseconds 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /android/src/main/java/dev/thinkng/flt_worker/internal/WorkRequests.java: -------------------------------------------------------------------------------- 1 | package dev.thinkng.flt_worker.internal; 2 | 3 | import android.os.Build; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | import androidx.annotation.VisibleForTesting; 8 | import androidx.core.util.Pair; 9 | import androidx.work.BackoffPolicy; 10 | import androidx.work.Constraints; 11 | import androidx.work.Data; 12 | import androidx.work.NetworkType; 13 | import androidx.work.OneTimeWorkRequest; 14 | import androidx.work.PeriodicWorkRequest; 15 | import androidx.work.WorkRequest; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | final class WorkRequests { 23 | private WorkRequests() {} 24 | 25 | @Nullable 26 | static List parseRequests(@NonNull Object input) { 27 | if (!(input instanceof List) || ((List) input).isEmpty()) { 28 | return null; 29 | } 30 | 31 | List requests = new ArrayList<>(); 32 | List requestsJson = (List) input; 33 | for (Object json : requestsJson) { 34 | if (json instanceof Map) { 35 | requests.add(parseRequest((Map) json)); 36 | } 37 | } 38 | return requests; 39 | } 40 | 41 | @SuppressWarnings("WeakerAccess") 42 | @VisibleForTesting 43 | @NonNull 44 | public static WorkRequest parseRequest(@NonNull Map json) { 45 | try { 46 | String type = (String) json.get("type"); 47 | if ("Periodic".equals(type)) { 48 | return parsePeriodicWorkRequest(json); 49 | } else { 50 | return parseOneTimeWorkRequest(json); 51 | } 52 | } catch (Exception e) { 53 | throw new IllegalArgumentException("Failed to parse WorkRequest from " + json, e); 54 | } 55 | } 56 | 57 | @NonNull 58 | private static WorkRequest parseOneTimeWorkRequest(@NonNull Map json) { 59 | OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(BackgroundWorker.class); 60 | populateRequestBuilder(builder, json); 61 | return builder.build(); 62 | } 63 | 64 | @SuppressWarnings("ConstantConditions") 65 | @NonNull 66 | private static WorkRequest parsePeriodicWorkRequest(@NonNull Map json) { 67 | long repeatInterval = ((Number) json.get("repeatInterval")).longValue(); 68 | Number flexInterval = (Number) json.get("flexInterval"); 69 | 70 | PeriodicWorkRequest.Builder builder = flexInterval != null ? 71 | new PeriodicWorkRequest.Builder(BackgroundWorker.class, 72 | repeatInterval, TimeUnit.MICROSECONDS, 73 | flexInterval.longValue(), TimeUnit.MICROSECONDS) : 74 | new PeriodicWorkRequest.Builder(BackgroundWorker.class, 75 | repeatInterval, TimeUnit.MICROSECONDS); 76 | 77 | populateRequestBuilder(builder, json); 78 | return builder.build(); 79 | } 80 | 81 | private static void populateRequestBuilder(WorkRequest.Builder builder, @NonNull Map json) { 82 | Object tagsJson = json.get("tags"); 83 | if (tagsJson instanceof Iterable) { 84 | for (Object tag : (List) tagsJson) { 85 | builder.addTag((String) tag); 86 | } 87 | } 88 | 89 | Object delayJson = json.get("initialDelay"); 90 | if (delayJson instanceof Number) { 91 | long delay = ((Number) delayJson).longValue(); 92 | if (delay > 0) { 93 | builder.setInitialDelay(delay, TimeUnit.MICROSECONDS); 94 | } 95 | } 96 | 97 | Constraints constraints = parseConstraints(json.get("constraints")); 98 | if (constraints != null) { 99 | builder.setConstraints(constraints); 100 | } 101 | 102 | Pair backoffCriteria = parseBackoffCriteria(json.get("backoffCriteria")); 103 | if (backoffCriteria != null) { 104 | //noinspection ConstantConditions 105 | builder.setBackoffCriteria(backoffCriteria.first, 106 | backoffCriteria.second, TimeUnit.MICROSECONDS); 107 | } 108 | 109 | Object inputJson = json.get("input"); 110 | if (inputJson instanceof Map) { 111 | //noinspection unchecked 112 | builder.setInputData(new Data.Builder() 113 | .putAll((Map) inputJson) 114 | .build()); 115 | } 116 | } 117 | 118 | @Nullable 119 | private static Pair parseBackoffCriteria(@Nullable Object json) { 120 | if (json instanceof Map) { 121 | Map criteria = (Map) json; 122 | Integer policy = (Integer) criteria.get("policy"); 123 | if (policy != null) { 124 | Number delay = (Number) criteria.get("delay"); 125 | if (delay != null) { 126 | return new Pair<>( 127 | policy == 1 ? BackoffPolicy.LINEAR : BackoffPolicy.EXPONENTIAL, 128 | delay.longValue() 129 | ); 130 | } 131 | } 132 | } 133 | 134 | return null; 135 | } 136 | 137 | @SuppressWarnings("ConstantConditions") 138 | @Nullable 139 | private static Constraints parseConstraints(@Nullable Object json) { 140 | if (!(json instanceof Map)) { 141 | return null; 142 | } 143 | 144 | Map constraintsJson = (Map) json; 145 | Constraints.Builder builder = new Constraints.Builder(); 146 | 147 | if (constraintsJson.get("networkType") != null) { 148 | builder.setRequiredNetworkType( 149 | NetworkType.values()[(Integer) constraintsJson.get("networkType")]); 150 | } 151 | if (constraintsJson.get("batteryNotLow") != null) { 152 | builder.setRequiresBatteryNotLow((Boolean) constraintsJson.get("batteryNotLow")); 153 | } 154 | if (constraintsJson.get("charging") != null) { 155 | builder.setRequiresCharging((Boolean) constraintsJson.get("charging")); 156 | } 157 | if (Build.VERSION.SDK_INT >= 23 && constraintsJson.get("deviceIdle") != null) { 158 | builder.setRequiresDeviceIdle((Boolean) constraintsJson.get("deviceIdle")); 159 | } 160 | if (constraintsJson.get("storageNotLow") != null) { 161 | builder.setRequiresStorageNotLow((Boolean) constraintsJson.get("storageNotLow")); 162 | } 163 | 164 | return builder.build(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /ios/Classes/internal/BGTaskMgrDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // BGTaskMgrDelegate.m 3 | // flt_worker 4 | // 5 | // Created by Yingxin Wu on 2020/3/12. 6 | // 7 | 8 | #import "BGTaskMgrDelegate.h" 9 | #import "BGTaskHandler.h" 10 | #import "utils.h" 11 | #import 12 | 13 | //#ifdef DEBUG 14 | //#import 15 | //#import 16 | //#endif 17 | 18 | @implementation BGTaskMgrDelegate 19 | 20 | // register background task indentifier/handler pairs 21 | + (void)registerBGTaskHandler { 22 | if (@available(iOS 13.0, *)) { 23 | NSArray *bgTaskIds = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; 24 | for (NSString *taskId in bgTaskIds) { 25 | [BGTaskScheduler.sharedScheduler registerForTaskWithIdentifier:taskId 26 | usingQueue:dispatch_get_main_queue() 27 | launchHandler:^(BGTask * _Nonnull task) { 28 | [BGTaskHandler.instance handleBGTask:task]; 29 | }]; 30 | } 31 | } 32 | } 33 | 34 | - (instancetype)initWithRegistrar:(NSObject*)registrar { 35 | self = [super init]; 36 | if (self) { 37 | _methodChannel = [FlutterMethodChannel methodChannelWithName:@PLUGIN_PKG 38 | binaryMessenger:[registrar messenger]]; 39 | } 40 | return self; 41 | } 42 | 43 | - (instancetype)initWithEngine:(FlutterEngine *)engine { 44 | self = [super init]; 45 | if (self) { 46 | _methodChannel = [FlutterMethodChannel methodChannelWithName:@PLUGIN_PKG 47 | binaryMessenger:[engine binaryMessenger]]; 48 | } 49 | return self; 50 | } 51 | 52 | - (void)saveHandles:(NSArray *)args { 53 | if (args.count > 1) { 54 | [workerDefaults() setObject:args[0] forKey:@DISPATCHER_KEY]; 55 | [workerDefaults() setObject:args[1] forKey:@WORKER_KEY]; 56 | } else { 57 | NSLog(@"Dispatcher & Worker callbacks are required!"); 58 | } 59 | } 60 | 61 | /** Caches an extra input data for the given task */ 62 | - (void)saveExtrasForTask:(NSString*)identifier extras:(NSString*)extras { 63 | [workerDefaults() setObject:extras forKey:TASK_KEY(identifier)]; 64 | } 65 | 66 | /** Makes a payload dict used as input of the dart worker */ 67 | - (NSDictionary*)packPayloadForTask:(NSString*)identifier { 68 | return @{ 69 | @"id": identifier, 70 | @"tags": @[identifier], 71 | @"input": @{ 72 | @"data": [workerDefaults() objectForKey:TASK_KEY(identifier)], 73 | }, 74 | }; 75 | } 76 | 77 | - (BOOL)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 78 | BOOL handled = YES; 79 | NSString *method = call.method; 80 | id args = call.arguments; 81 | if (@available(iOS 13.0, *)) { 82 | if ([@API_METHOD(submitTaskRequest) isEqualToString:method]) { 83 | BGTaskRequest *req = [self parseTaskRequest:args]; 84 | BOOL submitted = [BGTaskScheduler.sharedScheduler submitTaskRequest:req error:nil]; 85 | result(@(submitted)); 86 | } else if ([@API_METHOD(cancelTaskRequest) isEqualToString:method]) { 87 | [BGTaskScheduler.sharedScheduler cancelTaskRequestWithIdentifier:args]; 88 | result(nil); 89 | } else if ([@API_METHOD(cancelAllTaskRequests) isEqualToString:method]) { 90 | [BGTaskScheduler.sharedScheduler cancelAllTaskRequests]; 91 | result(nil); 92 | } else if ([@API_METHOD(simulateLaunchTask) isEqualToString:method]) { 93 | [self simulateLaunchTask:args]; 94 | result(nil); 95 | } else { 96 | handled = NO; 97 | } 98 | } 99 | 100 | return handled; 101 | } 102 | 103 | - (BGTaskRequest*)parseTaskRequest:(id)arguments API_AVAILABLE(ios(13.0)){ 104 | BGTaskRequest *req; 105 | NSString *type = arguments[@"type"]; 106 | NSString *identifier = arguments[@"identifier"]; 107 | NSNumber *date = arguments[@"earliestBeginDate"]; 108 | NSDate *earliestBeginDate = nil; 109 | if (IS_NONNULL(date)) { 110 | earliestBeginDate = [NSDate dateWithTimeIntervalSince1970:(date.doubleValue / 1000.0)]; 111 | } 112 | 113 | if ([type isEqual:@"Processing"]) { 114 | BGProcessingTaskRequest *processReq = [[BGProcessingTaskRequest alloc] initWithIdentifier:identifier]; 115 | req = processReq; 116 | 117 | id power = arguments[@"requiresExternalPower"]; 118 | if (IS_NONNULL(power)) { 119 | processReq.requiresExternalPower = [power boolValue]; 120 | } 121 | 122 | id network = arguments[@"requiresNetworkConnectivity"]; 123 | if (IS_NONNULL(network)) { 124 | processReq.requiresNetworkConnectivity = [network boolValue]; 125 | } 126 | } else { 127 | BGAppRefreshTaskRequest *refreshReq = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:identifier]; 128 | req = refreshReq; 129 | } 130 | 131 | req.earliestBeginDate = earliestBeginDate; 132 | 133 | // parse & cache extra input data 134 | NSString *extras = arguments[@"input"]; 135 | [self saveExtrasForTask:identifier extras:extras]; 136 | return req; 137 | } 138 | 139 | /** Simulate launch BGTask with the given identifier, using reflection & private APIs, for debugging only */ 140 | - (void)simulateLaunchTask:(NSString*)identifier { 141 | //#ifdef DEBUG 142 | // Method simulateMethod = nil; 143 | // SEL simulateSel = nil; 144 | // unsigned int numMethods = 0; 145 | // Method *methods = class_copyMethodList([BGTaskScheduler class], &numMethods); 146 | // 147 | // for (int i = 0; i < numMethods; i++) { 148 | // SEL sel = method_getName(methods[i]); 149 | // const char *name = sel_getName(sel); 150 | // if (strcmp("_simulateLaunchForTaskWithIdentifier:", name) == 0) { 151 | // simulateMethod = methods[i]; 152 | // simulateSel = sel; 153 | // break; 154 | // } 155 | // } 156 | // 157 | // if (simulateMethod) { 158 | // // only works on simulator?? 159 | //// ((void (*)(id, SEL, ...))objc_msgSend)([BGTaskScheduler sharedScheduler], simulateSel, identifier); 160 | //// ((void (*)(id, Method, ...))method_invoke)([BGTaskScheduler sharedScheduler], simulateMethod, identifier); 161 | // } 162 | // if (methods) { 163 | // free(methods); 164 | // } 165 | //#endif 166 | } 167 | 168 | @end 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flt_worker 2 | 3 | [![Pub][pub-badge]][pub] 4 | [![Check Status][check-badge]][github-runs] 5 | [![MIT][license-badge]][license] 6 | 7 | The `flt_worker` plugin allows you to schedule and execute Dart-written background tasks in a dedicated isolate, by utilizing the [WorkManager] API on Android, and the [BackgroundTasks] API on iOS 13.0+, respectively. 8 | 9 | Background processing is suitable for time-consuming tasks like downloading/uploading offline data, fitting a machine learning model, etc. You can use this plugin to schedule work like that. A pre-registed Dart worker will be launched and run in the background whenever the system decides to run the task. 10 | 11 | ## Integration 12 | 13 | Add a dependency to `pubspec.yaml`: 14 | ```yaml 15 | dependencies: 16 | flt_worker: ^0.1.0 17 | ``` 18 | 19 | A worker is running in a separate instance of Flutter engine. Any plugins needed in the worker have to be registered again. In the following example, the `path_provider` plugin is registered for the background isolate. 20 | 21 | iOS: 22 | ```obj-c 23 | - (BOOL)application:(UIApplication *)application 24 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 25 | [GeneratedPluginRegistrant registerWithRegistry:self]; 26 | 27 | // set a callback to register all plugins to a headless engine instance 28 | FltWorkerPlugin.registerPlugins = ^(NSObject *registry) { 29 | [FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]]; 30 | }; 31 | ... 32 | } 33 | ``` 34 | 35 | Android: 36 | ```java 37 | @Override 38 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { 39 | GeneratedPluginRegistrant.registerWith(flutterEngine); 40 | 41 | // set a callback to register all plugins to a headless engine instance 42 | FltWorkerPlugin.registerPluginsForWorkers = registry -> { 43 | io.flutter.plugins.pathprovider.PathProviderPlugin.registerWith( 44 | registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin")); 45 | return null; 46 | }; 47 | } 48 | ``` 49 | 50 | Fortunately, `flt_worker` itself is always available for the worker, so you don't have to register it again. 51 | 52 | One more thing has to be done if you're working on iOS: all task identifiers must be registered before you can subimit any `BGTaskRequest`. 53 | 54 | Add lines like this to the `Info.plist` file: 55 | 56 | ```xml 57 | BGTaskSchedulerPermittedIdentifiers 58 | 59 | com.example.counter_task 60 | dev.example.task2 61 | ... 62 | 63 | ``` 64 | 65 | ## Usage 66 | 67 | ### Initialization & work dispatcher 68 | 69 | Before you can schedule background tasks, a worker callback must be registerted to the plugin: 70 | 71 | ```dart 72 | import 'package:flt_worker/flt_worker.dart'; 73 | 74 | void main() { 75 | runApp(MyApp()); 76 | initializeWorker(worker); 77 | } 78 | ``` 79 | 80 | Please notice that the callback must be a [top-level or static function][CallbackHandle]. 81 | 82 | The `worker` function acts as a dispatcher of all background tasks, you can call different functions according to the payload of the work, and return a `Future` so that the plugin can notify the system scheduler whenever the work is done. 83 | 84 | ```dart 85 | Future worker(WorkPayload payload) { 86 | if (payload.tags.contains('download')) { 87 | return _fetchData(); 88 | } else if (...) { 89 | ... 90 | } else { 91 | return Future.value(); 92 | } 93 | } 94 | 95 | /// Cache data for offline use 96 | Future _fetchData() async { 97 | // fetch data & update local storage 98 | } 99 | ``` 100 | 101 | ### Scheduling work 102 | 103 | You can use the `enqueueWorkIntent` function to schedule a background `WorkIntent` like this: 104 | 105 | ```dart 106 | enqueueWorkIntent(WorkIntent( 107 | identifier: 'counter', 108 | initialDelay: Duration(seconds: 59), 109 | input: { 110 | 'counter': counter, 111 | }, 112 | )); 113 | ``` 114 | 115 | The name of `WorkIntent` is chosen to avoid conflict with the term `WorkRequest` from the [WorkManager] API for Android. 116 | 117 | Please see the documentation and also the example app to find out how to schedule different kinds of background work. 118 | 119 | ## High-level vs. Low-level APIs 120 | 121 | The background processing strategies and APIs are quite different on the Android and iOS platforms. The `flt_worker` plugin manages to provide a unified yet simplified API for general tasks, as the above example. 122 | 123 | However, to leverage the full power of each platform's background processing features, you may consider the low-level platform-specific APIs. 124 | 125 | For example, you can schedule a periodic work using the `WorkManager` APIs on an Android device: 126 | 127 | ```dart 128 | import 'package:flt_worker/android.dart'; 129 | 130 | Future _startPolling() async { 131 | await cancelAllWorkByTag('tag'); // cancel the previous work 132 | await enqueueWorkRequest(const PeriodicWorkRequest( 133 | repeatInterval: Duration(hours: 4), 134 | flexInterval: Duration(minutes: 5), 135 | tags: ['tag'], 136 | constraints: WorkConstraints( 137 | networkType: NetworkType.connected, 138 | storageNotLow: true, 139 | ), 140 | backoffCriteria: BackoffCriteria( 141 | policy: BackoffPolicy.linear, 142 | delay: Duration(minutes: 1), 143 | ), 144 | )); 145 | } 146 | ``` 147 | 148 | Or to use the `BackgroundTasks` APIs on iOS 13.0+: 149 | 150 | ```dart 151 | import 'package:flt_worker/ios.dart'; 152 | 153 | void _increaseCounter(int counter) { 154 | submitTaskRequest(BGProcessingTaskRequest( 155 | 'com.example.counter_task', 156 | earliestBeginDate: DateTime.now().add(Duration(seconds: 10)), 157 | requiresNetworkConnectivity: false, 158 | requiresExternalPower: true, 159 | input: { 160 | 'counter': counter, 161 | }, 162 | )); 163 | } 164 | ``` 165 | 166 | ## Limitations 167 | 168 | It's the very beginning of this library, some limitations you may need to notice are: 169 | 170 | - It relies on the `BackgroundTasks` framework, which means it's not working on iOS before `13.0` 171 | - For the Android platform, advanced features of `WorkManager` like [Chaining Work] are not yet supported 172 | 173 | [github-runs]: https://github.com/xinthink/flt_worker/actions 174 | [check-badge]: https://github.com/xinthink/flt_worker/workflows/check/badge.svg 175 | [codecov-badge]: https://codecov.io/gh/xinthink/flt_worker/branch/master/graph/badge.svg 176 | [codecov]: https://codecov.io/gh/xinthink/flt_worker 177 | [license-badge]: https://img.shields.io/github/license/xinthink/flt_worker 178 | [license]: https://raw.githubusercontent.com/xinthink/flt_worker/master/LICENSE 179 | [pub]: https://pub.dev/packages/flt_worker 180 | [pub-badge]: https://img.shields.io/pub/v/flt_worker.svg 181 | [WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager 182 | [BackgroundTasks]: https://developer.apple.com/documentation/backgroundtasks 183 | [CallbackHandle]: https://api.flutter.dev/flutter/dart-ui/PluginUtilities/getCallbackHandle.html 184 | [Chaining Work]: https://developer.android.com/topic/libraries/architecture/workmanager/how-to/chain-work 185 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.1.3" 60 | cupertino_icons: 61 | dependency: "direct main" 62 | description: 63 | name: cupertino_icons 64 | url: "https://pub.flutter-io.cn" 65 | source: hosted 66 | version: "0.1.3" 67 | flt_worker: 68 | dependency: "direct dev" 69 | description: 70 | path: ".." 71 | relative: true 72 | source: path 73 | version: "0.1.0" 74 | flutter: 75 | dependency: "direct main" 76 | description: flutter 77 | source: sdk 78 | version: "0.0.0" 79 | flutter_test: 80 | dependency: "direct dev" 81 | description: flutter 82 | source: sdk 83 | version: "0.0.0" 84 | http: 85 | dependency: "direct main" 86 | description: 87 | name: http 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "0.12.0+4" 91 | http_parser: 92 | dependency: transitive 93 | description: 94 | name: http_parser 95 | url: "https://pub.flutter-io.cn" 96 | source: hosted 97 | version: "3.1.3" 98 | image: 99 | dependency: transitive 100 | description: 101 | name: image 102 | url: "https://pub.flutter-io.cn" 103 | source: hosted 104 | version: "2.1.4" 105 | intl: 106 | dependency: "direct main" 107 | description: 108 | name: intl 109 | url: "https://pub.flutter-io.cn" 110 | source: hosted 111 | version: "0.16.1" 112 | matcher: 113 | dependency: transitive 114 | description: 115 | name: matcher 116 | url: "https://pub.flutter-io.cn" 117 | source: hosted 118 | version: "0.12.6" 119 | meta: 120 | dependency: transitive 121 | description: 122 | name: meta 123 | url: "https://pub.flutter-io.cn" 124 | source: hosted 125 | version: "1.1.8" 126 | path: 127 | dependency: transitive 128 | description: 129 | name: path 130 | url: "https://pub.flutter-io.cn" 131 | source: hosted 132 | version: "1.6.4" 133 | path_provider: 134 | dependency: "direct main" 135 | description: 136 | name: path_provider 137 | url: "https://pub.flutter-io.cn" 138 | source: hosted 139 | version: "1.6.5" 140 | path_provider_macos: 141 | dependency: transitive 142 | description: 143 | name: path_provider_macos 144 | url: "https://pub.flutter-io.cn" 145 | source: hosted 146 | version: "0.0.4" 147 | path_provider_platform_interface: 148 | dependency: transitive 149 | description: 150 | name: path_provider_platform_interface 151 | url: "https://pub.flutter-io.cn" 152 | source: hosted 153 | version: "1.0.1" 154 | pedantic: 155 | dependency: transitive 156 | description: 157 | name: pedantic 158 | url: "https://pub.flutter-io.cn" 159 | source: hosted 160 | version: "1.9.0" 161 | petitparser: 162 | dependency: transitive 163 | description: 164 | name: petitparser 165 | url: "https://pub.flutter-io.cn" 166 | source: hosted 167 | version: "2.4.0" 168 | platform: 169 | dependency: transitive 170 | description: 171 | name: platform 172 | url: "https://pub.flutter-io.cn" 173 | source: hosted 174 | version: "2.2.1" 175 | plugin_platform_interface: 176 | dependency: transitive 177 | description: 178 | name: plugin_platform_interface 179 | url: "https://pub.flutter-io.cn" 180 | source: hosted 181 | version: "1.0.2" 182 | quiver: 183 | dependency: transitive 184 | description: 185 | name: quiver 186 | url: "https://pub.flutter-io.cn" 187 | source: hosted 188 | version: "2.0.5" 189 | sky_engine: 190 | dependency: transitive 191 | description: flutter 192 | source: sdk 193 | version: "0.0.99" 194 | source_span: 195 | dependency: transitive 196 | description: 197 | name: source_span 198 | url: "https://pub.flutter-io.cn" 199 | source: hosted 200 | version: "1.5.5" 201 | stack_trace: 202 | dependency: transitive 203 | description: 204 | name: stack_trace 205 | url: "https://pub.flutter-io.cn" 206 | source: hosted 207 | version: "1.9.3" 208 | stream_channel: 209 | dependency: transitive 210 | description: 211 | name: stream_channel 212 | url: "https://pub.flutter-io.cn" 213 | source: hosted 214 | version: "2.0.0" 215 | string_scanner: 216 | dependency: transitive 217 | description: 218 | name: string_scanner 219 | url: "https://pub.flutter-io.cn" 220 | source: hosted 221 | version: "1.0.5" 222 | term_glyph: 223 | dependency: transitive 224 | description: 225 | name: term_glyph 226 | url: "https://pub.flutter-io.cn" 227 | source: hosted 228 | version: "1.1.0" 229 | test_api: 230 | dependency: transitive 231 | description: 232 | name: test_api 233 | url: "https://pub.flutter-io.cn" 234 | source: hosted 235 | version: "0.2.15" 236 | typed_data: 237 | dependency: transitive 238 | description: 239 | name: typed_data 240 | url: "https://pub.flutter-io.cn" 241 | source: hosted 242 | version: "1.1.6" 243 | vector_math: 244 | dependency: transitive 245 | description: 246 | name: vector_math 247 | url: "https://pub.flutter-io.cn" 248 | source: hosted 249 | version: "2.0.8" 250 | watcher: 251 | dependency: "direct main" 252 | description: 253 | name: watcher 254 | url: "https://pub.flutter-io.cn" 255 | source: hosted 256 | version: "0.9.7+14" 257 | xml: 258 | dependency: transitive 259 | description: 260 | name: xml 261 | url: "https://pub.flutter-io.cn" 262 | source: hosted 263 | version: "3.5.0" 264 | sdks: 265 | dart: ">=2.5.0 <3.0.0" 266 | flutter: ">=1.10.0 <2.0.0" 267 | -------------------------------------------------------------------------------- /android/src/main/java/dev/thinkng/flt_worker/internal/AbsWorkerPlugin.java: -------------------------------------------------------------------------------- 1 | package dev.thinkng.flt_worker.internal; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.os.Handler; 6 | import android.os.Looper; 7 | 8 | import androidx.annotation.Keep; 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.UiThread; 11 | import androidx.work.Operation; 12 | import androidx.work.WorkManager; 13 | import androidx.work.WorkRequest; 14 | 15 | import java.util.List; 16 | import java.util.UUID; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import java.util.concurrent.Future; 20 | import java.util.concurrent.FutureTask; 21 | 22 | import io.flutter.embedding.engine.plugins.FlutterPlugin; 23 | import io.flutter.plugin.common.MethodCall; 24 | import io.flutter.plugin.common.MethodChannel; 25 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 26 | import io.flutter.plugin.common.MethodChannel.Result; 27 | 28 | @Keep 29 | public abstract class AbsWorkerPlugin implements FlutterPlugin, MethodCallHandler { 30 | static final String TAG = "FltWorker"; 31 | protected static final String CHANNEL_NAME = "dev.thinkng.flt_worker"; 32 | protected static final String METHOD_PREFIX = "FltWorkerPlugin#"; 33 | 34 | protected Context context; 35 | private SharedPreferences prefs; 36 | private final ExecutorService workMgrExecutor = Executors.newCachedThreadPool(); 37 | private Handler mainHandler; 38 | 39 | /// The MethodChannel that will the communication between Flutter and native Android 40 | /// 41 | /// This local reference serves to register the plugin with the Flutter Engine and unregister it 42 | /// when the Flutter Engine is detached from the Activity 43 | MethodChannel channel; 44 | 45 | public AbsWorkerPlugin() { 46 | } 47 | 48 | public AbsWorkerPlugin(Context context) { 49 | this.context = context.getApplicationContext(); 50 | } 51 | 52 | protected SharedPreferences getPrefs() { 53 | if (prefs == null) { 54 | prefs = context.getSharedPreferences(CHANNEL_NAME, Context.MODE_PRIVATE); 55 | } 56 | return prefs; 57 | } 58 | 59 | @SuppressWarnings("deprecation") 60 | @Override 61 | public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { 62 | if (context == null) { 63 | context = flutterPluginBinding.getApplicationContext(); 64 | } 65 | channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), CHANNEL_NAME); 66 | channel.setMethodCallHandler(this); 67 | } 68 | 69 | @Override 70 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { 71 | if (channel != null) { 72 | channel.setMethodCallHandler(null); 73 | } 74 | workMgrExecutor.shutdownNow(); 75 | } 76 | 77 | @UiThread 78 | @Override 79 | final public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { 80 | if(!handleMethodCall(call, result)) { 81 | result.notImplemented(); 82 | } 83 | } 84 | 85 | @SuppressWarnings({"unused", "BooleanMethodIsAlwaysInverted"}) 86 | public boolean handleMethodCall(@NonNull MethodCall call, @NonNull Result result) { 87 | boolean handled = true; 88 | switch (call.method) { 89 | case METHOD_PREFIX + "enqueue": 90 | enqueue(call, result); 91 | break; 92 | case METHOD_PREFIX + "cancelAllWorkByTag": 93 | cancelAllWorkByTag(call, result); 94 | break; 95 | case METHOD_PREFIX + "cancelUniqueWork": 96 | cancelUniqueWork(call, result); 97 | break; 98 | case METHOD_PREFIX + "cancelWorkById": 99 | cancelWorkById(call, result); 100 | break; 101 | case METHOD_PREFIX + "cancelAllWork": 102 | cancelAllWork(call, result); 103 | break; 104 | default: 105 | handled = false; 106 | break; 107 | } 108 | return handled; 109 | } 110 | 111 | private void enqueue(@NonNull MethodCall call, @NonNull final Result result) { 112 | List requests = WorkRequests.parseRequests(call.arguments); 113 | if (requests == null || requests.isEmpty()) { 114 | result.success(false); 115 | return; 116 | } 117 | 118 | Operation op = WorkManager.getInstance(context).enqueue(requests); 119 | watchOperation(op, result, "enqueueWorkRequests"); 120 | } 121 | 122 | private void cancelAllWorkByTag(@NonNull MethodCall call, @NonNull final Result result) { 123 | Operation op = WorkManager.getInstance(context).cancelAllWorkByTag((String) call.arguments); 124 | watchOperation(op, result, "cancelAllWorkByTag"); 125 | } 126 | 127 | private void cancelUniqueWork(@NonNull MethodCall call, @NonNull final Result result) { 128 | Operation op = WorkManager.getInstance(context).cancelUniqueWork((String) call.arguments); 129 | watchOperation(op, result, "cancelUniqueWork"); 130 | } 131 | 132 | private void cancelWorkById(@NonNull MethodCall call, @NonNull final Result result) { 133 | Operation op = WorkManager.getInstance(context).cancelWorkById(UUID.fromString((String) call.arguments)); 134 | watchOperation(op, result, "cancelWorkById"); 135 | } 136 | 137 | private void cancelAllWork(@NonNull MethodCall call, @NonNull final Result result) { 138 | Operation op = WorkManager.getInstance(context).cancelAllWork(); 139 | watchOperation(op, result, "cancelAllWork"); 140 | } 141 | 142 | /** 143 | * Waits for the operation's completion, and reports the result. 144 | */ 145 | private void watchOperation(@NonNull final Operation operation, 146 | @NonNull final Result result, 147 | final String operationDesc) { 148 | workMgrExecutor.execute(new Runnable() { 149 | @Override 150 | public void run() { 151 | Throwable err = null; 152 | try { 153 | operation.getResult().get(); 154 | } catch (Throwable e) { 155 | err = e; 156 | } 157 | reportResult(result, err, operationDesc); 158 | } 159 | }); 160 | } 161 | 162 | private void reportResult(@NonNull final Result result, 163 | final Throwable e, 164 | final String operationDesc) { 165 | // reporting results on UI thread 166 | runOnMainThread(new Runnable() { 167 | @Override 168 | public void run() { 169 | if (e != null) { 170 | result.error("E", "Failed to " + operationDesc + ": " + e.getMessage(), null); 171 | } else { 172 | result.success(true); 173 | } 174 | } 175 | }); 176 | } 177 | 178 | private void ensureMainHandler() { 179 | synchronized (AbsWorkerPlugin.class) { 180 | if (mainHandler == null) { 181 | mainHandler = new Handler(Looper.getMainLooper()); 182 | } 183 | } 184 | } 185 | 186 | // /** Run the given [callable] on the main thread. */ 187 | // Future runOnMainThread(final Callable callable) { 188 | // ensureMainHandler(); 189 | // FutureTask task = new FutureTask<>(callable); 190 | // mainHandler.post(task); 191 | // return task; 192 | // } 193 | 194 | /** Run the given [runnable] on the main thread. */ 195 | Future runOnMainThread(final Runnable runnable) { 196 | ensureMainHandler(); 197 | FutureTask task = new FutureTask<>(runnable, null); 198 | mainHandler.post(task); 199 | return task; 200 | } 201 | } 202 | --------------------------------------------------------------------------------