From 63166893b2a3e497429aa5626414b946344834eb Mon Sep 17 00:00:00 2001 From: Simon Pham Date: Sun, 7 Sep 2025 21:39:37 +0700 Subject: [PATCH 01/32] feat: add mediaProcessing foreground service type --- .../flutter_foreground_task/models/ForegroundServiceTypes.kt | 1 + lib/models/foreground_service_types.dart | 3 +++ 2 files changed, 4 insertions(+) diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt index 315bca08..bea66546 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt @@ -70,6 +70,7 @@ data class ForegroundServiceTypes(val value: Int) { 10 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE else null 11 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else null 12 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED else null + 13 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING else null else -> null } } diff --git a/lib/models/foreground_service_types.dart b/lib/models/foreground_service_types.dart index 97327dbd..a307169b 100644 --- a/lib/models/foreground_service_types.dart +++ b/lib/models/foreground_service_types.dart @@ -49,6 +49,9 @@ class ForegroundServiceTypes { /// Reserved for system applications and specific system integrations, to continue to use foreground services. static const systemExempted = ForegroundServiceTypes(12); + /// Service for performing time-consuming operations on media assets, like converting media to different formats. + static const mediaProcessing = ForegroundServiceTypes(13); + /// The raw value of [ForegroundServiceTypes]. final int rawValue; } From e698150cd13f3060907939b3cea1a6c6db29bfc3 Mon Sep 17 00:00:00 2001 From: deakjahn Date: Fri, 19 Dec 2025 21:40:45 +0100 Subject: [PATCH 02/32] Fix FLAG_STOP_WITH_TASK bitmask check --- .../flutter_foreground_task/utils/ForegroundServiceUtils.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt index fcaed8a8..54f4f85f 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt @@ -17,7 +17,7 @@ class ForegroundServiceUtils { val pm = context.packageManager val cName = ComponentName(context, ForegroundService::class.java) val flags = pm.getServiceInfo(cName, PackageManager.GET_META_DATA).flags - (flags and ServiceInfo.FLAG_STOP_WITH_TASK) == 1 + (flags and ServiceInfo.FLAG_STOP_WITH_TASK) != 0 } catch (e: NameNotFoundException) { Log.e(TAG, "isSetStopWithTaskFlag >> The service component cannot be found on the system.") true @@ -27,4 +27,4 @@ class ForegroundServiceUtils { } } } -} +} \ No newline at end of file From 2b8bca01ca711523262e2f782c2d9042a919f3c9 Mon Sep 17 00:00:00 2001 From: deakjahn Date: Sat, 20 Dec 2025 13:33:10 +0100 Subject: [PATCH 03/32] Add stopWithTask override --- .../flutter_foreground_task/PreferencesKey.kt | 5 +-- .../models/ForegroundTaskOptions.kt | 15 ++++++-- .../service/ForegroundService.kt | 12 ++++++- .../service/RestartReceiver.kt | 2 +- .../utils/ForegroundServiceUtils.kt | 13 ++++--- .../utils/TrackVisibilityUtils.kt | 35 +++++++++++++++++++ lib/models/foreground_task_options.dart | 12 ++++++- 7 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/TrackVisibilityUtils.kt diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/PreferencesKey.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/PreferencesKey.kt index 2f98a359..af45a7a3 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/PreferencesKey.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/PreferencesKey.kt @@ -53,7 +53,8 @@ object PreferencesKey { const val ALLOW_WAKE_LOCK = "allowWakeLock" const val ALLOW_WIFI_LOCK = "allowWifiLock" const val ALLOW_AUTO_RESTART = "allowAutoRestart" + const val STOP_WITH_TASK = "stopWithTask" - // task data + // task data const val CALLBACK_HANDLE = "callbackHandle" -} +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskOptions.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskOptions.kt index b1f1515f..cdfb3627 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskOptions.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskOptions.kt @@ -10,7 +10,8 @@ data class ForegroundTaskOptions( val autoRunOnMyPackageReplaced: Boolean, val allowWakeLock: Boolean, val allowWifiLock: Boolean, - val allowAutoRestart: Boolean + val allowAutoRestart: Boolean, + val stopWithTask: Boolean? ) { companion object { fun getData(context: Context): ForegroundTaskOptions { @@ -36,6 +37,11 @@ data class ForegroundTaskOptions( val allowWakeLock = prefs.getBoolean(PrefsKey.ALLOW_WAKE_LOCK, true) val allowWifiLock = prefs.getBoolean(PrefsKey.ALLOW_WIFI_LOCK, false) val allowAutoRestart = prefs.getBoolean(PrefsKey.ALLOW_AUTO_RESTART, false) + val stopWithTask: Boolean? = + if (prefs.contains(PrefsKey.STOP_WITH_TASK)) + prefs.getBoolean(PrefsKey.STOP_WITH_TASK, false) + else + null return ForegroundTaskOptions( eventAction = eventAction, @@ -44,6 +50,7 @@ data class ForegroundTaskOptions( allowWakeLock = allowWakeLock, allowWifiLock = allowWifiLock, allowAutoRestart = allowAutoRestart, + stopWithTask = stopWithTask, ) } @@ -62,6 +69,7 @@ data class ForegroundTaskOptions( val allowWakeLock = map?.get(PrefsKey.ALLOW_WAKE_LOCK) as? Boolean ?: true val allowWifiLock = map?.get(PrefsKey.ALLOW_WIFI_LOCK) as? Boolean ?: false val allowAutoRestart = map?.get(PrefsKey.ALLOW_AUTO_RESTART) as? Boolean ?: false + val stopWithTask = map?.get(PrefsKey.STOP_WITH_TASK) as? Boolean with(prefs.edit()) { putString(PrefsKey.TASK_EVENT_ACTION, eventActionJsonString) @@ -70,6 +78,7 @@ data class ForegroundTaskOptions( putBoolean(PrefsKey.ALLOW_WAKE_LOCK, allowWakeLock) putBoolean(PrefsKey.ALLOW_WIFI_LOCK, allowWifiLock) putBoolean(PrefsKey.ALLOW_AUTO_RESTART, allowAutoRestart) + stopWithTask?.let { putBoolean(PrefsKey.STOP_WITH_TASK, it) } ?: remove(PrefsKey.STOP_WITH_TASK) commit() } } @@ -89,6 +98,7 @@ data class ForegroundTaskOptions( val allowWakeLock = map?.get(PrefsKey.ALLOW_WAKE_LOCK) as? Boolean val allowWifiLock = map?.get(PrefsKey.ALLOW_WIFI_LOCK) as? Boolean val allowAutoRestart = map?.get(PrefsKey.ALLOW_AUTO_RESTART) as? Boolean + val stopWithTask = map?.get(PrefsKey.STOP_WITH_TASK) as? Boolean with(prefs.edit()) { eventActionJsonString?.let { putString(PrefsKey.TASK_EVENT_ACTION, it) } @@ -97,6 +107,7 @@ data class ForegroundTaskOptions( allowWakeLock?.let { putBoolean(PrefsKey.ALLOW_WAKE_LOCK, it) } allowWifiLock?.let { putBoolean(PrefsKey.ALLOW_WIFI_LOCK, it) } allowAutoRestart?.let { putBoolean(PrefsKey.ALLOW_AUTO_RESTART, it) } + stopWithTask?.let { putBoolean(PrefsKey.STOP_WITH_TASK, it) } ?: remove(PrefsKey.STOP_WITH_TASK) commit() } } @@ -111,4 +122,4 @@ data class ForegroundTaskOptions( } } } -} +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt index 7d4b3d6f..a90ae098 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt @@ -17,7 +17,8 @@ import androidx.core.content.ContextCompat import com.pravera.flutter_foreground_task.FlutterForegroundTaskLifecycleListener import com.pravera.flutter_foreground_task.RequestCode import com.pravera.flutter_foreground_task.models.* -import com.pravera.flutter_foreground_task.utils.ForegroundServiceUtils +import com.pravera.flutter_foreground_task.utils.* +import com.pravera.flutter_foreground_task.PreferencesKey as PrefsKey import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -125,6 +126,15 @@ class ForegroundService : Service() { isTimeout = false loadDataFromPreferences() + val prefs = getSharedPreferences(PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + if (prefs.contains(PrefsKey.STOP_WITH_TASK) && prefs.getBoolean(PrefsKey.STOP_WITH_TASK, false)) { + (application as? Application)?.let { + TrackVisibilityUtils.install(it) { + stopForegroundService() + } + } + } + var action = foregroundServiceStatus.action val isSetStopWithTaskFlag = ForegroundServiceUtils.isSetStopWithTaskFlag(this) diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RestartReceiver.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RestartReceiver.kt index dbf12498..3c15a79a 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RestartReceiver.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RestartReceiver.kt @@ -100,4 +100,4 @@ class RestartReceiver : BroadcastReceiver() { } } } -} +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt index 54f4f85f..bf887bc9 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt @@ -7,23 +7,28 @@ import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.ServiceInfo import android.util.Log import com.pravera.flutter_foreground_task.service.ForegroundService +import com.pravera.flutter_foreground_task.PreferencesKey as PrefsKey class ForegroundServiceUtils { companion object { private val TAG = ForegroundServiceUtils::class.java.simpleName fun isSetStopWithTaskFlag(context: Context): Boolean { - return try { + try { + val prefs = context.getSharedPreferences(PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + if (prefs.contains(PrefsKey.STOP_WITH_TASK)) + return prefs.getBoolean(PrefsKey.STOP_WITH_TASK, false) + val pm = context.packageManager val cName = ComponentName(context, ForegroundService::class.java) val flags = pm.getServiceInfo(cName, PackageManager.GET_META_DATA).flags - (flags and ServiceInfo.FLAG_STOP_WITH_TASK) != 0 + return (flags and ServiceInfo.FLAG_STOP_WITH_TASK) != 0 } catch (e: NameNotFoundException) { Log.e(TAG, "isSetStopWithTaskFlag >> The service component cannot be found on the system.") - true + return true } catch (e: Exception) { Log.e(TAG, "isSetStopWithTaskFlag >> $e") - true + return true } } } diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/TrackVisibilityUtils.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/TrackVisibilityUtils.kt new file mode 100644 index 00000000..2e1c755f --- /dev/null +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/TrackVisibilityUtils.kt @@ -0,0 +1,35 @@ +package com.pravera.flutter_foreground_task.utils + +import android.app.* +import android.os.* + +object TrackVisibilityUtils : Application.ActivityLifecycleCallbacks { + private var installed = false + private val resumed = HashSet() + private var onAllGone: (() -> Unit)? = null + + fun install(application: Application, callback: () -> Unit) { + onAllGone = callback + if (!installed) { + installed = true + application.registerActivityLifecycleCallbacks(this) + } + } + + override fun onActivityResumed(activity: Activity) { + resumed.add(System.identityHashCode(activity)) + } + + override fun onActivityPaused(activity: Activity) { + resumed.remove(System.identityHashCode(activity)) + if (resumed.isEmpty()) { + onAllGone?.invoke() + } + } + + override fun onActivityStarted(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivityCreated(a: Activity, b: Bundle?) {} + override fun onActivitySaveInstanceState(a: Activity, b: Bundle) {} + override fun onActivityDestroyed(a: Activity) {} +} \ No newline at end of file diff --git a/lib/models/foreground_task_options.dart b/lib/models/foreground_task_options.dart index 9a10c21f..814bb16a 100644 --- a/lib/models/foreground_task_options.dart +++ b/lib/models/foreground_task_options.dart @@ -10,6 +10,7 @@ class ForegroundTaskOptions { this.allowWakeLock = true, this.allowWifiLock = false, this.allowAutoRestart = true, + this.stopWithTask, }); /// The action of onRepeatEvent in [TaskHandler]. @@ -38,6 +39,10 @@ class ForegroundTaskOptions { /// https://developer.android.com/about/versions/15/behavior-changes-15?hl=pt-br#datasync-timeout final bool allowAutoRestart; + /// Allows an application to automatically stop when the app task is removed by the system. + /// If set, overrides the service android:stopWithTask behavior. + final bool? stopWithTask; + /// Returns the data fields of [ForegroundTaskOptions] in JSON format. Map toJson() { return { @@ -47,9 +52,12 @@ class ForegroundTaskOptions { 'allowWakeLock': allowWakeLock, 'allowWifiLock': allowWifiLock, 'allowAutoRestart': allowAutoRestart, + if (stopWithTask != null) 'stopWithTask': stopWithTask, }; } + static const _unset = Object(); + /// Creates a copy of the object replaced with new values. ForegroundTaskOptions copyWith({ ForegroundTaskEventAction? eventAction, @@ -58,6 +66,7 @@ class ForegroundTaskOptions { bool? allowWakeLock, bool? allowWifiLock, bool? allowAutoRestart, + Object? stopWithTask = _unset, }) => ForegroundTaskOptions( eventAction: eventAction ?? this.eventAction, @@ -66,5 +75,6 @@ class ForegroundTaskOptions { allowWakeLock: allowWakeLock ?? this.allowWakeLock, allowWifiLock: allowWifiLock ?? this.allowWifiLock, allowAutoRestart: allowAutoRestart ?? this.allowAutoRestart, + stopWithTask: identical(stopWithTask, _unset) ? this.stopWithTask : stopWithTask as bool?, ); -} +} \ No newline at end of file From 42a88ad85df0a4e8d6dde296f5939dcc863be83e Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Wed, 24 Dec 2025 12:09:12 +0900 Subject: [PATCH 04/32] release: 9.2.0 --- CHANGELOG.md | 7 +++++++ README.md | 2 +- example/android/app/build.gradle | 2 +- pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8c4aa8..ea4d34db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 9.2.0 + +* [**FEAT**] Add `allowAutoRestart` option, which enables the service to automatically restart if it is terminated by the system. +* [**FEAT**] Add `stopWithTask` option, to ensure service termination when the app UI is dismissed. +* [**FEAT**] Add `mediaProcessing` foreground service type. +* [**FIX**] Fix `FLAG_STOP_WITH_TASK` bitmask check error. + ## 9.1.0 * [**FEAT**] Support manual foregroundServiceType via serviceTypes in startService diff --git a/README.md b/README.md index 90c89a7b..3ae93d93 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^9.1.0 + flutter_foreground_task: ^9.2.0 ``` After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 66e580eb..2d41fcda 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.pravera.flutter_foreground_task_example" - minSdkVersion 21 + minSdkVersion flutter.minSdkVersion targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/pubspec.yaml b/pubspec.yaml index 54431ee2..25bcb7de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 9.1.0 +version: 9.2.0 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: From e55150b1c15080998b9355338440f05a0b1ba9d5 Mon Sep 17 00:00:00 2001 From: Udyan Singh Date: Sat, 17 Jan 2026 11:55:31 +0530 Subject: [PATCH 05/32] fix(android): Prevent crash in onDestroy Resolves an UninitializedPropertyAccessException that occurs when the ForegroundService is destroyed by the OS before foregroundTaskOptions has been initialized. This change adds a defensive check using '::foregroundTaskOptions.isInitialized' before accessing the property in the onDestroy method, ensuring the app does not crash in this edge case. --- .../service/ForegroundService.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt index a90ae098..5b4970ee 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt @@ -204,10 +204,14 @@ class ForegroundService : Service() { if (::foregroundServiceStatus.isInitialized) { isCorrectlyStopped = foregroundServiceStatus.isCorrectlyStopped() } - val allowAutoRestart = foregroundTaskOptions.allowAutoRestart - if (allowAutoRestart && !isCorrectlyStopped && !ForegroundServiceUtils.isSetStopWithTaskFlag(this)) { - Log.e(TAG, "The service will be restarted after 5 seconds because it wasn't properly stopped.") - RestartReceiver.setRestartAlarm(this, 5000) + + // Safely handle auto-restart by checking if options are initialized. + if (::foregroundTaskOptions.isInitialized) { + val allowAutoRestart = foregroundTaskOptions.allowAutoRestart + if (allowAutoRestart && !isCorrectlyStopped && !ForegroundServiceUtils.isSetStopWithTaskFlag(this)) { + Log.e(TAG, "The service will be restarted after 5 seconds because it wasn't properly stopped.") + RestartReceiver.setRestartAlarm(this, 5000) + } } } From d628c363f81425d478b2cf2107e1638a72666f76 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Tue, 3 Mar 2026 13:41:25 +0900 Subject: [PATCH 06/32] release: 9.2.1 --- CHANGELOG.md | 4 ++++ README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4d34db..2f5b003a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.2.1 + +* [**FIX**] Prevent crash in onDestroy [#372](https://github.com/Dev-hwang/flutter_foreground_task/pull/372) + ## 9.2.0 * [**FEAT**] Add `allowAutoRestart` option, which enables the service to automatically restart if it is terminated by the system. diff --git a/README.md b/README.md index 3ae93d93..88a8a703 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^9.2.0 + flutter_foreground_task: ^9.2.1 ``` After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. diff --git a/pubspec.yaml b/pubspec.yaml index 25bcb7de..606ceaaa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 9.2.0 +version: 9.2.1 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: From eea409e4e75eb8d6718cabd3da06fb34732e190e Mon Sep 17 00:00:00 2001 From: Meliksah Cakir Date: Wed, 25 Feb 2026 12:11:47 +0100 Subject: [PATCH 07/32] fix: DidNotStartInTimeException & DidNotStopInTimeException --- .../service/ForegroundServiceManager.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundServiceManager.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundServiceManager.kt index 18ad0df1..70cb0a26 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundServiceManager.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundServiceManager.kt @@ -60,7 +60,10 @@ class ForegroundServiceManager { ForegroundTaskOptions.updateData(context, argsMap) ForegroundTaskData.updateData(context, argsMap) NotificationContent.updateData(context, argsMap) - ContextCompat.startForegroundService(context, nIntent) + // Use startService instead of startForegroundService because the service + // is already running in the foreground and we only need to deliver the + // update command. This avoids the startForeground() contract requirement. + context.startService(nIntent) } /** Stop the foreground service. */ @@ -76,7 +79,10 @@ class ForegroundServiceManager { ForegroundTaskOptions.clearData(context) ForegroundTaskData.clearData(context) NotificationContent.clearData(context) - ContextCompat.startForegroundService(context, nIntent) + // Use startService instead of startForegroundService because the service + // is already running in the foreground and we only need to deliver the + // stop command. This avoids the startForeground() contract requirement. + context.startService(nIntent) } /** Send data to TaskHandler. */ From b3d3d308724276565c473758ba08515230d7a03f Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Wed, 1 Apr 2026 13:21:44 +0900 Subject: [PATCH 08/32] release: 9.2.2 --- CHANGELOG.md | 4 ++++ README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5b003a..59763e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.2.2 + +* [**FIX**] Resolved frequent ForegroundServiceDidNotStartInTime and ForegroundServiceDidNotStopInTime exceptions by avoiding redundant foreground service start contracts. [#377](https://github.com/Dev-hwang/flutter_foreground_task/pull/377) + ## 9.2.1 * [**FIX**] Prevent crash in onDestroy [#372](https://github.com/Dev-hwang/flutter_foreground_task/pull/372) diff --git a/README.md b/README.md index 88a8a703..0bd48a52 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^9.2.1 + flutter_foreground_task: ^9.2.2 ``` After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. diff --git a/pubspec.yaml b/pubspec.yaml index 606ceaaa..14ad72b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 9.2.1 +version: 9.2.2 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: From 65868ab705cbfe4b173c69a3cf04ff7b69fab9b6 Mon Sep 17 00:00:00 2001 From: Ravdeep Singh Date: Mon, 8 Jun 2026 19:51:11 +0530 Subject: [PATCH 09/32] Migrate to built-in Kotlin Migrates the plugin to use built-in Kotlin per the official Flutter migration guide: https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-plugin-authors AGP 9.0 will remove support for plugins that apply the Kotlin Gradle Plugin. This PR removes the legacy `apply plugin: "kotlin-android"` line and the KGP classpath from `android/build.gradle`, and the same from the example app's `example/android/app/build.gradle`. The example app builds cleanly with `flutter build apk --debug` on Flutter 3.44.1 with no KGP deprecation warnings. Bumps the minimum Flutter version to 3.44.0 and Dart SDK to 3.12.0, as required by the new `kotlin.compilerOptions` DSL. Fixes the warning: "Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP)". --- CHANGELOG.md | 4 ++++ android/build.gradle | 13 ++++++------- example/android/app/build.gradle | 11 ++++++----- example/android/gradle.properties | 2 ++ pubspec.yaml | 6 +++--- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59763e6c..bac053da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.3.0 + +* [**CHORE**] Migrate to built-in Kotlin. AGP 9.0 will remove support for plugins that apply the Kotlin Gradle Plugin. Removes `apply plugin: "kotlin-android"` and the KGP classpath from `android/build.gradle`, and the same from `example/android/app/build.gradle`. Bumps minimum Flutter version to 3.44.0 and Dart SDK to 3.12.0 as required by the new `kotlin.compilerOptions` DSL. + ## 9.2.2 * [**FIX**] Resolved frequent ForegroundServiceDidNotStartInTime and ForegroundServiceDidNotStopInTime exceptions by avoiding redundant foreground service start contracts. [#377](https://github.com/Dev-hwang/flutter_foreground_task/pull/377) diff --git a/android/build.gradle b/android/build.gradle index ccc66918..6d692ec2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,6 @@ group 'com.pravera.flutter_foreground_task' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() @@ -10,7 +9,6 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.6.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -22,7 +20,6 @@ rootProject.allprojects { } apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' android { // Conditional for compatibility with AGP <4.2. @@ -37,10 +34,6 @@ android { targetCompatibility JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 - } - sourceSets { main.java.srcDirs += 'src/main/kotlin' test.java.srcDirs += 'src/test/kotlin' @@ -71,3 +64,9 @@ android { } } } + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 + } +} diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 2d41fcda..e701b5e7 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,6 +1,5 @@ plugins { id "com.android.application" - id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } @@ -33,10 +32,6 @@ android { targetCompatibility JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 - } - sourceSets { main.java.srcDirs += 'src/main/kotlin' } @@ -72,3 +67,9 @@ flutter { } dependencies {} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 + } +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 1967e2d9..9e5fb82e 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -2,3 +2,5 @@ org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true android.enableR8=true +android.builtInKotlin=true +android.newDsl=true diff --git a/pubspec.yaml b/pubspec.yaml index 14ad72b2..1445029a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 9.2.2 +version: 9.3.0 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: - sdk: ^3.4.0 - flutter: ">=3.22.0" + sdk: ^3.12.0 + flutter: ">=3.44.0" dependencies: flutter: From 33aa919e670d560526f1928e4b879353d6878117 Mon Sep 17 00:00:00 2001 From: TeddyYeung Date: Fri, 26 Jun 2026 16:32:50 +0900 Subject: [PATCH 10/32] feat(ios): add Swift Package Manager support --- .gitignore | 2 + CHANGELOG.md | 4 + ios/flutter_foreground_task.podspec | 2 +- ios/flutter_foreground_task/Package.swift | 20 ++ ...utterForegroundTaskLifecycleListener.swift | 35 +++ .../FlutterForegroundTaskStarter.swift | 13 ++ .../PreferencesKey.swift | 26 +++ .../SwiftFlutterForegroundTaskPlugin.swift | 191 ++++++++++++++++ .../errors/ServiceError.swift | 30 +++ .../models/BackgroundServiceAction.swift | 17 ++ .../models/BackgroundServiceStatus.swift | 25 +++ .../models/ForegroundTaskData.swift | 42 ++++ .../models/ForegroundTaskEventAction.swift | 39 ++++ .../models/ForegroundTaskEventType.swift | 18 ++ .../models/ForegroundTaskOptions.swift | 64 ++++++ .../models/NotificationButton.swift | 33 +++ .../models/NotificationContent.swift | 79 +++++++ .../models/NotificationOptions.swift | 38 ++++ .../models/NotificationPermission.swift | 13 ++ .../service/BackgroundService.swift | 206 ++++++++++++++++++ .../service/BackgroundServiceManager.swift | 96 ++++++++ .../service/ForegroundTask.swift | 193 ++++++++++++++++ .../ForegroundTaskLifecycleListeners.swift | 55 +++++ .../NotificationPermissionManager.swift | 39 ++++ pubspec.yaml | 2 +- 25 files changed, 1280 insertions(+), 2 deletions(-) create mode 100644 ios/flutter_foreground_task/Package.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskLifecycleListener.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskStarter.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/PreferencesKey.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/errors/ServiceError.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/BackgroundServiceAction.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/BackgroundServiceStatus.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskData.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskEventAction.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskEventType.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskOptions.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationButton.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationContent.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationOptions.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationPermission.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/service/BackgroundService.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/service/BackgroundServiceManager.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/service/ForegroundTask.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/service/ForegroundTaskLifecycleListeners.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/service/NotificationPermissionManager.swift diff --git a/.gitignore b/.gitignore index ddde1066..9245c580 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .idea/ .packages .pub/ +.build/ +.swiftpm/ /pubspec.lock build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 59763e6c..ef6c4015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.2.3 + +* [**FEAT**] Add Swift Package Manager (SPM) support for iOS. + ## 9.2.2 * [**FIX**] Resolved frequent ForegroundServiceDidNotStartInTime and ForegroundServiceDidNotStopInTime exceptions by avoiding redundant foreground service start contracts. [#377](https://github.com/Dev-hwang/flutter_foreground_task/pull/377) diff --git a/ios/flutter_foreground_task.podspec b/ios/flutter_foreground_task.podspec index 45788fda..65ee9da2 100644 --- a/ios/flutter_foreground_task.podspec +++ b/ios/flutter_foreground_task.podspec @@ -15,7 +15,7 @@ A new Flutter plugin. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '12.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/ios/flutter_foreground_task/Package.swift b/ios/flutter_foreground_task/Package.swift new file mode 100644 index 00000000..36d203e4 --- /dev/null +++ b/ios/flutter_foreground_task/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "flutter_foreground_task", + platforms: [ + .iOS("12.0") + ], + products: [ + .library(name: "flutter-foreground-task", targets: ["flutter_foreground_task"]) + ], + dependencies: [], + targets: [ + .target( + name: "flutter_foreground_task", + dependencies: [], + path: "Sources/flutter_foreground_task" + ) + ] +) diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskLifecycleListener.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskLifecycleListener.swift new file mode 100644 index 00000000..c4879001 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskLifecycleListener.swift @@ -0,0 +1,35 @@ +// +// FlutterForegroundTaskLifecycleListener.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 7/15/24. +// + +import Foundation +import Flutter + +/** A listener that can listen to the task lifecycle events. */ +public protocol FlutterForegroundTaskLifecycleListener : AnyObject { + /** + * Each time a task starts, a new FlutterEngine is created. + * + * This is called before onTaskStart, Initialize the service you want to use in the task. (like PlatformChannel initialization) + */ + func onEngineCreate(flutterEngine: FlutterEngine?) + + /** Called when the task is started. */ + func onTaskStart(starter: FlutterForegroundTaskStarter) + + /** Called based on the eventAction set in ForegroundTaskOptions. */ + func onTaskRepeatEvent() + + /** Called when the task is destroyed. */ + func onTaskDestroy() + + /** + * If one task is finished or replaced by another, the FlutterEngine is destroyed. + * + * This is called after onTaskDestroy, where dispose the service that was initialized in onEngineCreate. + */ + func onEngineWillDestroy() +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskStarter.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskStarter.swift new file mode 100644 index 00000000..cf69cf83 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskStarter.swift @@ -0,0 +1,13 @@ +// +// FlutterForegroundTaskStarter.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/23/24. +// + +import Foundation + +public enum FlutterForegroundTaskStarter : Int { + case DEVELOPER = 0 + case SYSTEM = 1 +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/PreferencesKey.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/PreferencesKey.swift new file mode 100644 index 00000000..da46a244 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/PreferencesKey.swift @@ -0,0 +1,26 @@ +// +// BackgroundServicePrefsKey.swift +// flutter_foreground_task +// +// Created by WOO JIN HWANG on 2021/08/11. +// + +// service status +let BACKGROUND_SERVICE_ACTION = "backgroundServiceAction" + +// notification options +let SHOW_NOTIFICATION = "showNotification" +let PLAY_SOUND = "playSound" + +// notification content +let NOTIFICATION_CONTENT_TITLE = "notificationContentTitle" +let NOTIFICATION_CONTENT_TEXT = "notificationContentText" +let NOTIFICATION_CONTENT_BUTTONS = "buttons" + +// task options +let TASK_EVENT_ACTION = "taskEventAction" // new +let INTERVAL = "interval" // deprecated +let IS_ONCE_EVENT = "isOnceEvent" // deprecated + +// task data +let CALLBACK_HANDLE = "callbackHandle" diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift new file mode 100644 index 00000000..8c163e66 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift @@ -0,0 +1,191 @@ +import Flutter +import UIKit +import BackgroundTasks + +public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { + // ====================== Plugin ====================== + static private(set) var registerPlugins: FlutterPluginRegistrantCallback? = nil + + private var notificationPermissionManager: NotificationPermissionManager? = nil + private var backgroundServiceManager: BackgroundServiceManager? = nil + + private var foregroundChannel: FlutterMethodChannel? = nil + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = SwiftFlutterForegroundTaskPlugin() + instance.initServices() + instance.initChannels(registrar.messenger()) + registrar.addApplicationDelegate(instance) + } + + public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { + registerPlugins = callback + } + + public static func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { + BackgroundService.sharedInstance.addTaskLifecycleListener(listener) + } + + public static func removeTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { + BackgroundService.sharedInstance.removeTaskLifecycleListener(listener) + } + + private func initServices() { + notificationPermissionManager = NotificationPermissionManager() + backgroundServiceManager = BackgroundServiceManager() + } + + private func initChannels(_ messenger: FlutterBinaryMessenger) { + foregroundChannel = FlutterMethodChannel(name: "flutter_foreground_task/methods", binaryMessenger: messenger) + foregroundChannel?.setMethodCallHandler(onMethodCall) + } + + private func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments + do { + switch call.method { + case "checkNotificationPermission": + notificationPermissionManager!.checkPermission { permission in + result(permission.rawValue) + } + case "requestNotificationPermission": + notificationPermissionManager!.requestPermission { permission in + result(permission.rawValue) + } + case "startService": + try backgroundServiceManager!.start(arguments: args) + result(true) + case "restartService": + try backgroundServiceManager!.restart(arguments: args) + result(true) + case "updateService": + try backgroundServiceManager!.update(arguments: args) + result(true) + case "stopService": + try backgroundServiceManager!.stop() + result(true) + case "sendData": + backgroundServiceManager!.sendData(data: args) + case "isRunningService": + result(backgroundServiceManager!.isRunningService()) + case "minimizeApp": + UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) + case "isAppOnForeground": + result(UIApplication.shared.applicationState == .active) + default: + result(FlutterMethodNotImplemented) + } + } catch { + let code = String(describing: error.self) + let message = error.localizedDescription + let flutterError = FlutterError(code: code, message: message, details: nil) + result(flutterError) + } + } + + // ================== App Lifecycle =================== + public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable : Any] = [:]) -> Bool { + UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) + if #available(iOS 13.0, *) { + SwiftFlutterForegroundTaskPlugin.registerAppRefresh() + } + return true + } + + public func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool { + completionHandler(.newData) + return true + } + + public func applicationDidEnterBackground(_ application: UIApplication) { + if #available(iOS 13.0, *) { + SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() + } + } + + public func applicationWillTerminate(_ application: UIApplication) { + if !BackgroundService.sharedInstance.isRunningService { + return + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.APP_TERMINATE) + BackgroundService.sharedInstance.run() + + // Chance to handle onDestroy before app terminates + sleep(5) + } + + // ================= Service Delegate ================= + @available(iOS 10.0, *) + public func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + BackgroundService.sharedInstance.userNotificationCenter(center, response, completionHandler) + } + + @available(iOS 10.0, *) + public func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + BackgroundService.sharedInstance.userNotificationCenter(center, notification, completionHandler) + } + + // ============== Background App Refresh ============== + public static var refreshIdentifier: String = "com.pravera.flutter_foreground_task.refresh" + + @available(iOS 13.0, *) + private static func registerAppRefresh() { + BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshIdentifier, using: nil) { task in + handleAppRefresh(task: task as! BGAppRefreshTask) + } + } + + @available(iOS 13.0, *) + private static func scheduleAppRefresh() { + let request = BGAppRefreshTaskRequest(identifier: refreshIdentifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + print("Could not schedule app refresh: \(error)") + } + } + + @available(iOS 13.0, *) + private static func cancelAppRefresh() { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshIdentifier) + } + + @available(iOS 13.0, *) + private static func handleAppRefresh(task: BGAppRefreshTask) { + let queue = OperationQueue() + let operation = AppRefreshOperation() + + task.expirationHandler = { + operation.cancel() + } + + operation.completionBlock = { + // Schedule a new refresh task + scheduleAppRefresh() + + task.setTaskCompleted(success: true) + } + + queue.addOperation(operation) + } +} + +class AppRefreshOperation: Operation { + override func main() { + let semaphore = DispatchSemaphore(value: 0) + + // avoid non-platform thread + DispatchQueue.main.asyncAfter(deadline: .now() + 25) { + semaphore.signal() + } + + semaphore.wait() + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/errors/ServiceError.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/errors/ServiceError.swift new file mode 100644 index 00000000..c3f8bb35 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/errors/ServiceError.swift @@ -0,0 +1,30 @@ +// +// ServiceError.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 7/17/24. +// + +import Foundation + +enum ServiceError { + case ServiceArgumentNullException + case ServiceAlreadyStartedException + case ServiceNotStartedException + case ServiceNotSupportedException +} + +extension ServiceError : LocalizedError { + public var errorDescription: String? { + switch self { + case .ServiceArgumentNullException: + return NSLocalizedString("The required argument was not passed to the service.", comment: "ServiceArgumentNullException") + case .ServiceAlreadyStartedException: + return NSLocalizedString("The service has already started.", comment: "ServiceAlreadyStartedException") + case .ServiceNotStartedException: + return NSLocalizedString("The service is not started.", comment: "ServiceNotStartedException") + case .ServiceNotSupportedException: + return NSLocalizedString("The current iOS version does not support the service.", comment: "ServiceNotSupportedException") + } + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/BackgroundServiceAction.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/BackgroundServiceAction.swift new file mode 100644 index 00000000..4c39f27a --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/BackgroundServiceAction.swift @@ -0,0 +1,17 @@ +// +// BackgroundServiceAction.swift +// flutter_foreground_task +// +// Created by WOO JIN HWANG on 2021/08/11. +// + +import Foundation + +enum BackgroundServiceAction: String { + case API_START + case API_RESTART + case API_UPDATE + case API_STOP + + case APP_TERMINATE +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/BackgroundServiceStatus.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/BackgroundServiceStatus.swift new file mode 100644 index 00000000..7bd95995 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/BackgroundServiceStatus.swift @@ -0,0 +1,25 @@ +// +// BackgroundServiceStatus.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/13/24. +// + +import Foundation + +struct BackgroundServiceStatus { + let action: BackgroundServiceAction + + static func getData() -> BackgroundServiceStatus { + let prefs = UserDefaults.standard + let actionValue = prefs.string(forKey: BACKGROUND_SERVICE_ACTION) ?? BackgroundServiceAction.API_STOP.rawValue + let action = BackgroundServiceAction(rawValue: actionValue)! + + return BackgroundServiceStatus(action: action) + } + + static func setData(action: BackgroundServiceAction) { + let prefs = UserDefaults.standard + prefs.set(action.rawValue, forKey: BACKGROUND_SERVICE_ACTION) + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskData.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskData.swift new file mode 100644 index 00000000..1a35a261 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskData.swift @@ -0,0 +1,42 @@ +// +// ForegroundTaskData.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/24/24. +// + +import Foundation + +struct ForegroundTaskData { + let callbackHandle: Int64? + + static func getData() -> ForegroundTaskData { + let prefs = UserDefaults.standard + + let callbackHandle = prefs.object(forKey: CALLBACK_HANDLE) as? Int64 + + return ForegroundTaskData(callbackHandle: callbackHandle) + } + + static func setData(args: Dictionary) { + let prefs = UserDefaults.standard + + prefs.removeObject(forKey: CALLBACK_HANDLE) + if let callbackHandle = args[CALLBACK_HANDLE] as? Int64 { + prefs.set(callbackHandle, forKey: CALLBACK_HANDLE) + } + } + + static func updateData(args: Dictionary) { + let prefs = UserDefaults.standard + + if let callbackHandle = args[CALLBACK_HANDLE] as? Int64 { + prefs.set(callbackHandle, forKey: CALLBACK_HANDLE) + } + } + + static func clearData() { + let prefs = UserDefaults.standard + prefs.removeObject(forKey: CALLBACK_HANDLE) + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskEventAction.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskEventAction.swift new file mode 100644 index 00000000..ac52a1b4 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskEventAction.swift @@ -0,0 +1,39 @@ +// +// ForegroundTaskEventAction.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/28/24. +// + +import Foundation + +private let TASK_EVENT_TYPE_KEY = "taskEventType" +private let TASK_EVENT_INTERVAL_KEY = "taskEventInterval" + +struct ForegroundTaskEventAction: Equatable { + let type: ForegroundTaskEventType + let interval: Int + + static func fromJsonString(_ jsonString: String) -> ForegroundTaskEventAction { + var type: ForegroundTaskEventType = .NOTHING + var interval: Int = 5000 + + if let jsonData = jsonString.data(using: .utf8) { + if let jsonObj = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? Dictionary { + if let _type = jsonObj[TASK_EVENT_TYPE_KEY] as? Int { + type = ForegroundTaskEventType.fromValue(_type) + } + + if let _interval = jsonObj[TASK_EVENT_INTERVAL_KEY] as? Int { + interval = _interval + } + } + } + + return ForegroundTaskEventAction(type: type, interval: interval) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.type.rawValue == rhs.type.rawValue && lhs.interval == rhs.interval + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskEventType.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskEventType.swift new file mode 100644 index 00000000..224eba1c --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskEventType.swift @@ -0,0 +1,18 @@ +// +// ForegroundTaskEventType.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/28/24. +// + +import Foundation + +enum ForegroundTaskEventType: Int { + case NOTHING = 1 + case ONCE = 2 + case REPEAT = 3 + + static func fromValue(_ value: Int) -> ForegroundTaskEventType { + return ForegroundTaskEventType(rawValue: value) ?? .NOTHING + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskOptions.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskOptions.swift new file mode 100644 index 00000000..c5cb1f93 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/ForegroundTaskOptions.swift @@ -0,0 +1,64 @@ +// +// ForegroundTaskOptions.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/24/24. +// + +import Foundation + +struct ForegroundTaskOptions { + let eventAction: ForegroundTaskEventAction + + static func getData() -> ForegroundTaskOptions { + let prefs = UserDefaults.standard + + let eventActionJsonString = prefs.string(forKey: TASK_EVENT_ACTION) + let eventAction: ForegroundTaskEventAction + if eventActionJsonString != nil { + eventAction = ForegroundTaskEventAction.fromJsonString(eventActionJsonString!) + } else { + // for deprecated api + let oldIsOnceEvent = prefs.bool(forKey: IS_ONCE_EVENT) + let oldInterval = prefs.integer(forKey: INTERVAL) + if oldIsOnceEvent { + eventAction = ForegroundTaskEventAction(type: .ONCE, interval: oldInterval) + } else { + eventAction = ForegroundTaskEventAction(type: .REPEAT, interval: oldInterval) + } + } + + return ForegroundTaskOptions(eventAction: eventAction) + } + + static func setData(args: Dictionary) { + let prefs = UserDefaults.standard + + if let eventActionJson = args[TASK_EVENT_ACTION] as? Dictionary { + if let eventActionJsonData = try? JSONSerialization.data(withJSONObject: eventActionJson, options: []) { + if let eventActionJsonString = String(data: eventActionJsonData, encoding: .utf8) { + prefs.set(eventActionJsonString, forKey: TASK_EVENT_ACTION) + } + } + } + } + + static func updateData(args: Dictionary) { + let prefs = UserDefaults.standard + + if let eventActionJson = args[TASK_EVENT_ACTION] as? Dictionary { + if let eventActionJsonData = try? JSONSerialization.data(withJSONObject: eventActionJson, options: []) { + if let eventActionJsonString = String(data: eventActionJsonData, encoding: .utf8) { + prefs.set(eventActionJsonString, forKey: TASK_EVENT_ACTION) + } + } + } + } + + static func clearData() { + let prefs = UserDefaults.standard + prefs.removeObject(forKey: TASK_EVENT_ACTION) // new + prefs.removeObject(forKey: INTERVAL) // deprecated + prefs.removeObject(forKey: IS_ONCE_EVENT) // deprecated + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationButton.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationButton.swift new file mode 100644 index 00000000..5ea18708 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationButton.swift @@ -0,0 +1,33 @@ +// +// NotificationButton.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/5/24. +// + +import Foundation + +private let ID_KEY = "id" +private let TEXT_KEY = "text" + +struct NotificationButton { + let id: String + let text: String + + static func fromJSONObject(_ jsonObj: Any) -> NotificationButton { + var id: String = "unknown" + var text: String = "" + + if let _jsonObj = jsonObj as? Dictionary { + if let _id = _jsonObj[ID_KEY] as? String { + id = _id + } + + if let _text = _jsonObj[TEXT_KEY] as? String { + text = _text + } + } + + return NotificationButton(id: id, text: text) + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationContent.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationContent.swift new file mode 100644 index 00000000..2d44f3c9 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationContent.swift @@ -0,0 +1,79 @@ +// +// NotificationContent.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/2/24. +// + +import Foundation + +struct NotificationContent { + let title: String + let text: String + let buttons: Array + + static func getData() -> NotificationContent { + let prefs = UserDefaults.standard + + let title = prefs.string(forKey: NOTIFICATION_CONTENT_TITLE) ?? "" + let text = prefs.string(forKey: NOTIFICATION_CONTENT_TEXT) ?? "" + + var buttons: [NotificationButton] = [] + if let buttonsJsonString = prefs.string(forKey: NOTIFICATION_CONTENT_BUTTONS) { + if let buttonsJsonData = buttonsJsonString.data(using: .utf8) { + if let buttonsJsonArr = try? JSONSerialization.jsonObject(with: buttonsJsonData, options: []) as? [Any] { + for buttonJsonObj in buttonsJsonArr { + buttons.append(NotificationButton.fromJSONObject(buttonJsonObj)) + } + } + } + } + + return NotificationContent(title: title, text: text, buttons: buttons) + } + + static func setData(args: Dictionary) { + let prefs = UserDefaults.standard + + let title = args[NOTIFICATION_CONTENT_TITLE] as? String ?? "" + prefs.set(title, forKey: NOTIFICATION_CONTENT_TITLE) + + let text = args[NOTIFICATION_CONTENT_TEXT] as? String ?? "" + prefs.set(text, forKey: NOTIFICATION_CONTENT_TEXT) + + if let buttonsJson = args[NOTIFICATION_CONTENT_BUTTONS] as? [Any] { + if let buttonsJsonData = try? JSONSerialization.data(withJSONObject: buttonsJson, options: []) { + if let buttonsJsonString = String(data: buttonsJsonData, encoding: .utf8) { + prefs.set(buttonsJsonString, forKey: NOTIFICATION_CONTENT_BUTTONS) + } + } + } + } + + static func updateData(args: Dictionary) { + let prefs = UserDefaults.standard + + if let title = args[NOTIFICATION_CONTENT_TITLE] as? String { + prefs.set(title, forKey: NOTIFICATION_CONTENT_TITLE) + } + + if let text = args[NOTIFICATION_CONTENT_TEXT] as? String { + prefs.set(text, forKey: NOTIFICATION_CONTENT_TEXT) + } + + if let buttonsJson = args[NOTIFICATION_CONTENT_BUTTONS] as? [Any] { + if let buttonsJsonData = try? JSONSerialization.data(withJSONObject: buttonsJson, options: []) { + if let buttonsJsonString = String(data: buttonsJsonData, encoding: .utf8) { + prefs.set(buttonsJsonString, forKey: NOTIFICATION_CONTENT_BUTTONS) + } + } + } + } + + static func clearData() { + let prefs = UserDefaults.standard + prefs.removeObject(forKey: NOTIFICATION_CONTENT_TITLE) + prefs.removeObject(forKey: NOTIFICATION_CONTENT_TEXT) + prefs.removeObject(forKey: NOTIFICATION_CONTENT_BUTTONS) + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationOptions.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationOptions.swift new file mode 100644 index 00000000..14fa854a --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationOptions.swift @@ -0,0 +1,38 @@ +// +// NotificationOptions.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/2/24. +// + +import Foundation + +struct NotificationOptions { + let showNotification: Bool + let playSound: Bool + + static func getData() -> NotificationOptions { + let prefs = UserDefaults.standard + + let showNotification = prefs.bool(forKey: SHOW_NOTIFICATION) + let playSound = prefs.bool(forKey: PLAY_SOUND) + + return NotificationOptions(showNotification: showNotification, playSound: playSound) + } + + static func setData(args: Dictionary) { + let prefs = UserDefaults.standard + + let showNotification = args[SHOW_NOTIFICATION] as? Bool ?? false + let playSound = args[PLAY_SOUND] as? Bool ?? false + + prefs.set(showNotification, forKey: SHOW_NOTIFICATION) + prefs.set(playSound, forKey: PLAY_SOUND) + } + + static func clearData() { + let prefs = UserDefaults.standard + prefs.removeObject(forKey: SHOW_NOTIFICATION) + prefs.removeObject(forKey: PLAY_SOUND) + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationPermission.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationPermission.swift new file mode 100644 index 00000000..157bd9db --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/models/NotificationPermission.swift @@ -0,0 +1,13 @@ +// +// NotificationPermission.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/6/24. +// + +import Foundation + +enum NotificationPermission : Int { + case GRANTED = 0 + case DENIED = 1 +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/BackgroundService.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/BackgroundService.swift new file mode 100644 index 00000000..4029ad80 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/BackgroundService.swift @@ -0,0 +1,206 @@ +// +// BackgroundService.swift +// flutter_foreground_task +// +// Created by WOO JIN HWANG on 2021/08/11. +// + +import Flutter +import Foundation +import UserNotifications + +private let NOTIFICATION_ID = "flutter_foreground_task/notification" +private let NOTIFICATION_CATEGORY_ID = "flutter_foreground_task/notification_category" + +private let ACTION_RECEIVE_DATA = "onReceiveData" +private let ACTION_NOTIFICATION_BUTTON_PRESSED = "onNotificationButtonPressed" +private let ACTION_NOTIFICATION_PRESSED = "onNotificationPressed" +private let ACTION_NOTIFICATION_DISMISSED = "onNotificationDismissed" + +@available(iOS 10.0, *) +class BackgroundService: NSObject { + static let sharedInstance = BackgroundService() + + private(set) var isRunningService: Bool = false + + private var foregroundTask: ForegroundTask? = nil + private var taskLifecycleListeners = ForegroundTaskLifecycleListeners() + + func sendData(data: Any?) { + if isRunningService { + foregroundTask?.invokeMethod(ACTION_RECEIVE_DATA, arguments: data) + } + } + + func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { + taskLifecycleListeners.addListener(listener) + } + + func removeTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { + taskLifecycleListeners.removeListener(listener) + } + + private let notificationCenter: UNUserNotificationCenter + private let notificationPermissionManager: NotificationPermissionManager + private var canReceiveNotificationResponse: Bool = false + + private var backgroundServiceStatus: BackgroundServiceStatus + private var notificationOptions: NotificationOptions + private var notificationContent: NotificationContent + private var prevForegroundTaskOptions: ForegroundTaskOptions? + private var currForegroundTaskOptions: ForegroundTaskOptions + private var prevForegroundTaskData: ForegroundTaskData? + private var currForegroundTaskData: ForegroundTaskData + + override init() { + notificationCenter = UNUserNotificationCenter.current() + notificationPermissionManager = NotificationPermissionManager() + backgroundServiceStatus = BackgroundServiceStatus.getData() + notificationOptions = NotificationOptions.getData() + notificationContent = NotificationContent.getData() + currForegroundTaskOptions = ForegroundTaskOptions.getData() + currForegroundTaskData = ForegroundTaskData.getData() + super.init() + } + + func run() { + backgroundServiceStatus = BackgroundServiceStatus.getData() + notificationOptions = NotificationOptions.getData() + notificationContent = NotificationContent.getData() + prevForegroundTaskOptions = currForegroundTaskOptions + currForegroundTaskOptions = ForegroundTaskOptions.getData() + prevForegroundTaskData = currForegroundTaskData + currForegroundTaskData = ForegroundTaskData.getData() + + switch backgroundServiceStatus.action { + case .API_START, .API_RESTART: + requestNotification() + createForegroundTask() + isRunningService = true + break + case .API_UPDATE: + requestNotification() + let prevCallbackHandle = prevForegroundTaskData?.callbackHandle + let currCallbackHandle = currForegroundTaskData.callbackHandle + if prevCallbackHandle != currCallbackHandle { + createForegroundTask() + } else { + let prevEventAction = prevForegroundTaskOptions?.eventAction + let currEventAction = currForegroundTaskOptions.eventAction + if prevEventAction != currEventAction { + updateForegroundTask() + } + } + break + case .API_STOP, .APP_TERMINATE: + destroyForegroundTask() + removeAllNotification() + isRunningService = false + break + } + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + _ response: UNNotificationResponse, + _ completionHandler: @escaping () -> Void) { + // If it is not a notification requested by this plugin, the processing below is ignored. + if response.notification.request.identifier != NOTIFICATION_ID { return } + + // Prevents duplicate processing due to the `registrar.addApplicationDelegate`. + if !canReceiveNotificationResponse { return } + canReceiveNotificationResponse = false + + let actionId = response.actionIdentifier + if notificationContent.buttons.contains(where: { $0.id == actionId }) { + foregroundTask?.invokeMethod(ACTION_NOTIFICATION_BUTTON_PRESSED, arguments: actionId) + } else if actionId == UNNotificationDefaultActionIdentifier { + foregroundTask?.invokeMethod(ACTION_NOTIFICATION_PRESSED, arguments: nil) + } else if actionId == UNNotificationDismissActionIdentifier { + foregroundTask?.invokeMethod(ACTION_NOTIFICATION_DISMISSED, arguments: nil) + } + + completionHandler() + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + _ notification: UNNotification, + _ completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + // If it is not a notification requested by this plugin, the processing below is ignored. + if notification.request.identifier != NOTIFICATION_ID { return } + + if notificationOptions.playSound { + completionHandler([.alert, .sound]) + } else { + completionHandler([.alert]) + } + + // Prevents duplicate processing due to the `registrar.addApplicationDelegate`. + canReceiveNotificationResponse = true + } + + private func setNotificationActions() { + var actions: [UNNotificationAction] = [] + for button in notificationContent.buttons { + let action = UNNotificationAction(identifier: button.id, title: button.text) + actions.append(action) + } + + let category = UNNotificationCategory( + identifier: NOTIFICATION_CATEGORY_ID, + actions: actions, + intentIdentifiers: [], + options: .customDismissAction + ) + + notificationCenter.setNotificationCategories([category]) + } + + private func requestNotification() { + if !notificationOptions.showNotification { + return + } + + notificationPermissionManager.checkPermission { permission in + if permission == NotificationPermission.DENIED { + return + } + + let content = UNMutableNotificationContent() + content.title = self.notificationContent.title + content.body = self.notificationContent.text + content.categoryIdentifier = NOTIFICATION_CATEGORY_ID + if self.notificationOptions.playSound { + content.sound = .default + } + self.setNotificationActions() + + let request = UNNotificationRequest(identifier: NOTIFICATION_ID, content: content, trigger: nil) + self.notificationCenter.add(request, withCompletionHandler: nil) + } + } + + private func removeAllNotification() { + notificationCenter.removePendingNotificationRequests(withIdentifiers: [NOTIFICATION_ID]) + notificationCenter.removeDeliveredNotifications(withIdentifiers: [NOTIFICATION_ID]) + } + + private func createForegroundTask() { + destroyForegroundTask() + + foregroundTask = ForegroundTask( + serviceStatus: backgroundServiceStatus, + taskData: currForegroundTaskData, + taskEventAction: currForegroundTaskOptions.eventAction, + taskLifecycleListener: taskLifecycleListeners + ) + } + + private func updateForegroundTask() { + foregroundTask?.update(taskEventAction: currForegroundTaskOptions.eventAction) + } + + private func destroyForegroundTask() { + foregroundTask?.destroy() + foregroundTask = nil + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/BackgroundServiceManager.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/BackgroundServiceManager.swift new file mode 100644 index 00000000..ae30acb5 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/BackgroundServiceManager.swift @@ -0,0 +1,96 @@ +// +// BackgroundServiceManager.swift +// flutter_foreground_task +// +// Created by WOO JIN HWANG on 2021/08/10. +// + +import Flutter +import Foundation + +class BackgroundServiceManager: NSObject { + func start(arguments: Any?) throws { + if #available(iOS 10.0, *) { + if BackgroundService.sharedInstance.isRunningService { + throw ServiceError.ServiceAlreadyStartedException + } + + guard let args = arguments as? Dictionary else { + throw ServiceError.ServiceArgumentNullException + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_START) + NotificationOptions.setData(args: args) + NotificationContent.setData(args: args) + ForegroundTaskOptions.setData(args: args) + ForegroundTaskData.setData(args: args) + BackgroundService.sharedInstance.run() + } else { + throw ServiceError.ServiceNotSupportedException + } + } + + func restart(arguments: Any?) throws { + if #available(iOS 10.0, *) { + if !BackgroundService.sharedInstance.isRunningService { + throw ServiceError.ServiceNotStartedException + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_RESTART) + BackgroundService.sharedInstance.run() + } else { + throw ServiceError.ServiceNotSupportedException + } + } + + func update(arguments: Any?) throws { + if #available(iOS 10.0, *) { + if !BackgroundService.sharedInstance.isRunningService { + throw ServiceError.ServiceNotStartedException + } + + guard let args = arguments as? Dictionary else { + throw ServiceError.ServiceArgumentNullException + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_UPDATE) + NotificationContent.updateData(args: args) + ForegroundTaskOptions.updateData(args: args) + ForegroundTaskData.updateData(args: args) + BackgroundService.sharedInstance.run() + } else { + throw ServiceError.ServiceNotSupportedException + } + } + + func stop() throws { + if #available(iOS 10.0, *) { + if !BackgroundService.sharedInstance.isRunningService { + throw ServiceError.ServiceNotStartedException + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_STOP) + NotificationOptions.clearData() + NotificationContent.clearData() + ForegroundTaskOptions.clearData() + ForegroundTaskData.clearData() + BackgroundService.sharedInstance.run() + } else { + throw ServiceError.ServiceNotSupportedException + } + } + + func sendData(data: Any?) { + if data != nil { + BackgroundService.sharedInstance.sendData(data: data) + } + } + + func isRunningService() -> Bool { + if #available(iOS 10.0, *) { + return BackgroundService.sharedInstance.isRunningService + } else { + return false + } + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/ForegroundTask.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/ForegroundTask.swift new file mode 100644 index 00000000..20c5b585 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/ForegroundTask.swift @@ -0,0 +1,193 @@ +// +// ForegroundTask.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/24/24. +// + +import Foundation +import Flutter + +private let BG_ISOLATE_NAME = "flutter_foreground_task/backgroundIsolate" +private let BG_CHANNEL_NAME = "flutter_foreground_task/background" + +private let ACTION_TASK_START = "onStart" +private let ACTION_TASK_REPEAT_EVENT = "onRepeatEvent" +private let ACTION_TASK_DESTROY = "onDestroy" + +class ForegroundTask { + private let serviceStatus: BackgroundServiceStatus + private let taskData: ForegroundTaskData + private var taskEventAction: ForegroundTaskEventAction + private let taskLifecycleListener: FlutterForegroundTaskLifecycleListener + + private var flutterEngine: FlutterEngine? = nil + private var backgroundChannel: FlutterMethodChannel? = nil + private var repeatTask: Timer? = nil + private var isDestroyed: Bool = false + + init( + serviceStatus: BackgroundServiceStatus, + taskData: ForegroundTaskData, + taskEventAction: ForegroundTaskEventAction, + taskLifecycleListener: FlutterForegroundTaskLifecycleListener + ) { + self.serviceStatus = serviceStatus + self.taskData = taskData + self.taskEventAction = taskEventAction + self.taskLifecycleListener = taskLifecycleListener + initialize() + } + + private func initialize() { + guard let registerPlugins = SwiftFlutterForegroundTaskPlugin.registerPlugins else { + print("Please register the registerPlugins function using the SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback.") + return + } + + guard let callbackHandle = taskData.callbackHandle else { + // no callback -> Unlike Android, the flutter engine does not start. + return + } + + // lookup callback + let callbackInfo = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) + guard let entrypoint = callbackInfo?.callbackName else { + print("Entrypoint not found in callback information.") + return + } + guard let libraryURI = callbackInfo?.callbackLibraryPath else { + print("LibraryURI not found in callback information.") + return + } + + // create flutter engine & execute callback + let flutterEngine = FlutterEngine(name: BG_ISOLATE_NAME, project: nil, allowHeadlessExecution: true) + let isRunningEngine = flutterEngine.run(withEntrypoint: entrypoint, libraryURI: libraryURI) + + if isRunningEngine { + // register plugins + registerPlugins(flutterEngine) + taskLifecycleListener.onEngineCreate(flutterEngine: flutterEngine) + + // create background channel + let messenger = flutterEngine.binaryMessenger + let backgroundChannel = FlutterMethodChannel(name: BG_CHANNEL_NAME, binaryMessenger: messenger) + backgroundChannel.setMethodCallHandler(onMethodCall) + + self.flutterEngine = flutterEngine + self.backgroundChannel = backgroundChannel + } + } + + func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "start": + start() + default: + result(FlutterMethodNotImplemented) + } + } + + private func start() { + runIfNotDestroyed { + runIfCallbackHandleExists { + let serviceAction = serviceStatus.action + let starter: FlutterForegroundTaskStarter + if serviceAction == .API_START || serviceAction == .API_RESTART || serviceAction == .API_UPDATE { + starter = .DEVELOPER + } else { + starter = .SYSTEM + } + + backgroundChannel?.invokeMethod(ACTION_TASK_START, arguments: starter.rawValue) { _ in + self.runIfNotDestroyed { + self.startRepeatTask() + } + } + taskLifecycleListener.onTaskStart(starter: starter) + } + } + } + + private func invokeTaskRepeatEvent() { + backgroundChannel?.invokeMethod(ACTION_TASK_REPEAT_EVENT, arguments: nil) + taskLifecycleListener.onTaskRepeatEvent() + } + + private func startRepeatTask() { + stopRepeatTask() + + let type = taskEventAction.type + let interval = TimeInterval(Double(taskEventAction.interval) / 1000) + + if type == .NOTHING { + return + } + + if type == .ONCE { + invokeTaskRepeatEvent() + return + } + + repeatTask = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in + self.invokeTaskRepeatEvent() + } + } + + private func stopRepeatTask() { + repeatTask?.invalidate() + repeatTask = nil + } + + func invokeMethod(_ method: String, arguments: Any?) { + runIfNotDestroyed { + backgroundChannel?.invokeMethod(method, arguments: arguments) + } + } + + func update(taskEventAction: ForegroundTaskEventAction) { + runIfNotDestroyed { + runIfCallbackHandleExists { + self.taskEventAction = taskEventAction + startRepeatTask() + } + } + } + + func destroy() { + runIfNotDestroyed { + stopRepeatTask() + + backgroundChannel?.setMethodCallHandler(nil) + if taskData.callbackHandle == nil { + taskLifecycleListener.onEngineWillDestroy() + flutterEngine?.destroyContext() + flutterEngine = nil + } else { + backgroundChannel?.invokeMethod(ACTION_TASK_DESTROY, arguments: nil) { _ in + self.flutterEngine?.destroyContext() + self.flutterEngine = nil + } + taskLifecycleListener.onTaskDestroy() + taskLifecycleListener.onEngineWillDestroy() + } + + isDestroyed = true + } + } + + private func runIfCallbackHandleExists(call: () -> Void) { + if taskData.callbackHandle == nil { + return + } + call() + } + + private func runIfNotDestroyed(call: () -> Void) { + if isDestroyed { + return + } + call() + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/ForegroundTaskLifecycleListeners.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/ForegroundTaskLifecycleListeners.swift new file mode 100644 index 00000000..7724b559 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/ForegroundTaskLifecycleListeners.swift @@ -0,0 +1,55 @@ +// +// ForegroundTaskLifecycleListeners.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/24/24. +// + +import Foundation +import Flutter + +class ForegroundTaskLifecycleListeners : FlutterForegroundTaskLifecycleListener { + private var listeners: Array = [] + + func addListener(_ listener: FlutterForegroundTaskLifecycleListener) { + if listeners.contains(where: { $0 === listener }) == false { + listeners.append(listener) + } + } + + func removeListener(_ listener: FlutterForegroundTaskLifecycleListener) { + if let index = listeners.firstIndex(where: { $0 === listener }) { + listeners.remove(at: index) + } + } + + func onEngineCreate(flutterEngine: FlutterEngine?) { + for listener in listeners { + listener.onEngineCreate(flutterEngine: flutterEngine) + } + } + + func onTaskStart(starter: FlutterForegroundTaskStarter) { + for listener in listeners { + listener.onTaskStart(starter: starter) + } + } + + func onTaskRepeatEvent() { + for listener in listeners { + listener.onTaskRepeatEvent() + } + } + + func onTaskDestroy() { + for listener in listeners { + listener.onTaskDestroy() + } + } + + func onEngineWillDestroy() { + for listener in listeners { + listener.onEngineWillDestroy() + } + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/NotificationPermissionManager.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/NotificationPermissionManager.swift new file mode 100644 index 00000000..6df0b3f1 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/service/NotificationPermissionManager.swift @@ -0,0 +1,39 @@ +// +// NotificationPermissionManager.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/6/24. +// + +import Foundation +import UserNotifications + +class NotificationPermissionManager { + func checkPermission(completion: @escaping (NotificationPermission) -> Void) { + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .authorized: + completion(NotificationPermission.GRANTED) + case .denied, .ephemeral, .notDetermined, .provisional: + completion(NotificationPermission.DENIED) + @unknown default: + completion(NotificationPermission.DENIED) + } + } + } + + func requestPermission(completion: @escaping (NotificationPermission) -> Void) { + let options = UNAuthorizationOptions(arrayLiteral: .alert, .sound) + UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in + if error != nil { + completion(NotificationPermission.DENIED) + } else { + if (granted) { + completion(NotificationPermission.GRANTED) + } else { + completion(NotificationPermission.DENIED) + } + } + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 14ad72b2..5d9771af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,4 +26,4 @@ flutter: package: com.pravera.flutter_foreground_task pluginClass: FlutterForegroundTaskPlugin ios: - pluginClass: FlutterForegroundTaskPlugin + pluginClass: SwiftFlutterForegroundTaskPlugin From d487d59196344a941e533502f64b939e08d07576 Mon Sep 17 00:00:00 2001 From: Jack Gibbons Date: Thu, 19 Mar 2026 14:34:08 +1300 Subject: [PATCH 11/32] Add UIScene life cycle support --- ios/Classes/FlutterForegroundTaskPlugin.m | 9 +++++++++ .../SwiftFlutterForegroundTaskPlugin.swift | 20 ++++++++++++++++++- pubspec.yaml | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/ios/Classes/FlutterForegroundTaskPlugin.m b/ios/Classes/FlutterForegroundTaskPlugin.m index e4e7a71f..ec7add77 100644 --- a/ios/Classes/FlutterForegroundTaskPlugin.m +++ b/ios/Classes/FlutterForegroundTaskPlugin.m @@ -16,3 +16,12 @@ + (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { [SwiftFlutterForegroundTaskPlugin setPluginRegistrantCallback:callback]; } @end + +@interface FlutterForegroundTaskEarlyRegistration : NSObject +@end + +@implementation FlutterForegroundTaskEarlyRegistration ++ (void)load { + [SwiftFlutterForegroundTaskPlugin registerAppRefreshForBackgroundLaunch]; +} +@end diff --git a/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift b/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift index 8c163e66..a5ec09e7 100644 --- a/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift +++ b/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift @@ -2,7 +2,7 @@ import Flutter import UIKit import BackgroundTasks -public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { +public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin, FlutterSceneLifeCycleDelegate { // ====================== Plugin ====================== static private(set) var registerPlugins: FlutterPluginRegistrantCallback? = nil @@ -16,11 +16,20 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { instance.initServices() instance.initChannels(registrar.messenger()) registrar.addApplicationDelegate(instance) + registrar.addSceneDelegate(instance) } public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { registerPlugins = callback } + + @objc public static func registerAppRefreshForBackgroundLaunch() { + if #available(iOS 13.0, *) { + let permitted = Bundle.main.object(forInfoDictionaryKey: "BGTaskSchedulerPermittedIdentifiers") as? [String] ?? [] + guard permitted.contains(refreshIdentifier) else { return } + registerAppRefresh() + } + } public static func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { BackgroundService.sharedInstance.addTaskLifecycleListener(listener) @@ -115,6 +124,12 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { sleep(5) } + // ================ Scene Lifecycle =================== + @available(iOS 13.0, *) + public func sceneDidEnterBackground(_ scene: UIScene) { + SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() + } + // ================= Service Delegate ================= @available(iOS 10.0, *) public func userNotificationCenter(_ center: UNUserNotificationCenter, @@ -132,9 +147,12 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { // ============== Background App Refresh ============== public static var refreshIdentifier: String = "com.pravera.flutter_foreground_task.refresh" + private static var isBgTaskRegistered: Bool = false @available(iOS 13.0, *) private static func registerAppRefresh() { + guard !isBgTaskRegistered else { return } + isBgTaskRegistered = true BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshIdentifier, using: nil) { task in handleAppRefresh(task: task as! BGAppRefreshTask) } diff --git a/pubspec.yaml b/pubspec.yaml index 14ad72b2..e280e944 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: sdk: ^3.4.0 - flutter: ">=3.22.0" + flutter: ">=3.38.0" dependencies: flutter: From 82cd8a24694ad9aa2fb19bf711d46261308a45bc Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Wed, 15 Jul 2026 09:43:08 +0900 Subject: [PATCH 12/32] release: 10.0.0 --- CHANGELOG.md | 5 +++ README.md | 2 +- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../Flutter/ephemeral/flutter_lldb_helper.py | 32 +++++++++++++++++++ .../ios/Flutter/ephemeral/flutter_lldbinit | 5 +++ example/ios/Podfile | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 6 ++-- .../xcshareddata/xcschemes/Runner.xcscheme | 3 ++ pubspec.yaml | 2 +- 9 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 example/ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 example/ios/Flutter/ephemeral/flutter_lldbinit diff --git a/CHANGELOG.md b/CHANGELOG.md index 59763e6c..7218cb6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 10.0.0 + +* [**CHORE**] Bump minimum supported SDK version to `Flutter 3.38.0` +* [**FEAT**] Add UIScene life cycle support for iOS [#378](https://github.com/Dev-hwang/flutter_foreground_task/pull/378) + ## 9.2.2 * [**FIX**] Resolved frequent ForegroundServiceDidNotStartInTime and ForegroundServiceDidNotStopInTime exceptions by avoiding redundant foreground service start contracts. [#377](https://github.com/Dev-hwang/flutter_foreground_task/pull/377) diff --git a/README.md b/README.md index 0bd48a52..752c8e09 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^9.2.2 + flutter_foreground_task: ^10.0.0 ``` After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c569640..1dc6cf76 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/example/ios/Podfile b/example/ios/Podfile index 279576f3..e72e0b48 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index bb8cbb51..fb86ac29 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -361,7 +361,7 @@ DEVELOPMENT_TEAM = 2BHAXN696Y; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -491,7 +491,7 @@ DEVELOPMENT_TEAM = 2BHAXN696Y; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -515,7 +515,7 @@ DEVELOPMENT_TEAM = 2BHAXN696Y; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 67709153..a9d43dad 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/pubspec.yaml b/pubspec.yaml index e280e944..23772e83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 9.2.2 +version: 10.0.0 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: From 5ac0c7306aa0c2f9c2e5937c8d51909391ec6bfa Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Wed, 15 Jul 2026 11:14:01 +0900 Subject: [PATCH 13/32] chore(android): upgrade Gradle, AGP, Kotlin and compileSdk - Bump Gradle wrapper to 8.14 - Bump AGP to 8.11.1 - Bump Kotlin to 2.2.20 - Set compileSdk to 36 (required by shared_preferences_android) --- example/android/app/build.gradle | 2 +- example/android/gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index e701b5e7..3fdcdccf 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -24,7 +24,7 @@ if (flutterVersionName == null) { android { namespace "com.pravera.flutter_foreground_task_example" - compileSdk 35 + compileSdk 36 ndkVersion = flutter.ndkVersion compileOptions { diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 7aeeb11c..1dbf2bb0 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index bf71fb92..b6ae39a4 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -19,8 +19,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.6.0" apply false - id "org.jetbrains.kotlin.android" version "1.9.10" apply false + id "com.android.application" version "8.11.1" apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false } include ":app" From afc8c9ee0d546c3ce52dd802fd0fd6bde6cad500 Mon Sep 17 00:00:00 2001 From: Ravdeep Singh Date: Mon, 8 Jun 2026 23:21:11 +0900 Subject: [PATCH 14/32] feat(ios): add Swift Package Manager support --- example/ios/Flutter/AppFrameworkInfo.plist | 2 -- ios/flutter_foreground_task/Package.swift | 8 +++++-- .../SwiftFlutterForegroundTaskPlugin.swift | 22 +++++++++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/flutter_foreground_task/Package.swift b/ios/flutter_foreground_task/Package.swift index 36d203e4..27979b76 100644 --- a/ios/flutter_foreground_task/Package.swift +++ b/ios/flutter_foreground_task/Package.swift @@ -9,11 +9,15 @@ let package = Package( products: [ .library(name: "flutter-foreground-task", targets: ["flutter_foreground_task"]) ], - dependencies: [], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework") + ], targets: [ .target( name: "flutter_foreground_task", - dependencies: [], + dependencies: [ + .product(name: "FlutterFramework", path: "../FlutterFramework") + ], path: "Sources/flutter_foreground_task" ) ] diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift index 8c163e66..d50bb393 100644 --- a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift @@ -2,7 +2,7 @@ import Flutter import UIKit import BackgroundTasks -public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { +public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin, FlutterSceneLifeCycleDelegate { // ====================== Plugin ====================== static private(set) var registerPlugins: FlutterPluginRegistrantCallback? = nil @@ -16,11 +16,20 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { instance.initServices() instance.initChannels(registrar.messenger()) registrar.addApplicationDelegate(instance) + registrar.addSceneDelegate(instance) } public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { registerPlugins = callback } + + @objc public static func registerAppRefreshForBackgroundLaunch() { + if #available(iOS 13.0, *) { + let permitted = Bundle.main.object(forInfoDictionaryKey: "BGTaskSchedulerPermittedIdentifiers") as? [String] ?? [] + guard permitted.contains(refreshIdentifier) else { return } + registerAppRefresh() + } + } public static func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { BackgroundService.sharedInstance.addTaskLifecycleListener(listener) @@ -114,7 +123,13 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { // Chance to handle onDestroy before app terminates sleep(5) } - + + // ================ Scene Lifecycle =================== + @available(iOS 13.0, *) + public func sceneDidEnterBackground(_ scene: UIScene) { + SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() + } + // ================= Service Delegate ================= @available(iOS 10.0, *) public func userNotificationCenter(_ center: UNUserNotificationCenter, @@ -132,9 +147,12 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { // ============== Background App Refresh ============== public static var refreshIdentifier: String = "com.pravera.flutter_foreground_task.refresh" + private static var isBgTaskRegistered: Bool = false @available(iOS 13.0, *) private static func registerAppRefresh() { + guard !isBgTaskRegistered else { return } + isBgTaskRegistered = true BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshIdentifier, using: nil) { task in handleAppRefresh(task: task as! BGAppRefreshTask) } From e335c5b6f5be92b283aa32e597a306e14df09e3c Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Fri, 14 Aug 2026 16:25:48 +0900 Subject: [PATCH 15/32] fix(ios): Fix wrong dependencies --- ios/flutter_foreground_task.podspec | 2 +- ios/flutter_foreground_task/Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/flutter_foreground_task.podspec b/ios/flutter_foreground_task.podspec index 65ee9da2..191bb9ed 100644 --- a/ios/flutter_foreground_task.podspec +++ b/ios/flutter_foreground_task.podspec @@ -13,7 +13,7 @@ A new Flutter plugin. s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.source_files = 'flutter_foreground_task/Sources/flutter_foreground_task/**/*' s.dependency 'Flutter' s.platform = :ios, '12.0' diff --git a/ios/flutter_foreground_task/Package.swift b/ios/flutter_foreground_task/Package.swift index 27979b76..dbf136d3 100644 --- a/ios/flutter_foreground_task/Package.swift +++ b/ios/flutter_foreground_task/Package.swift @@ -16,7 +16,7 @@ let package = Package( .target( name: "flutter_foreground_task", dependencies: [ - .product(name: "FlutterFramework", path: "../FlutterFramework") + .product(name: "FlutterFramework", package: "FlutterFramework") ], path: "Sources/flutter_foreground_task" ) From 25a2b7b8fa0801bce646aafd079668b31c6e53e2 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Fri, 14 Aug 2026 16:42:59 +0900 Subject: [PATCH 16/32] fix(ios): Guard addSceneDelegate call with iOS 13 availability check --- ios/Classes/SwiftFlutterForegroundTaskPlugin.swift | 4 +++- .../SwiftFlutterForegroundTaskPlugin.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift b/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift index a5ec09e7..d5f547d9 100644 --- a/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift +++ b/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift @@ -16,7 +16,9 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin, FlutterS instance.initServices() instance.initChannels(registrar.messenger()) registrar.addApplicationDelegate(instance) - registrar.addSceneDelegate(instance) + if #available(iOS 13.0, *) { + registrar.addSceneDelegate(instance) + } } public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift index d50bb393..5f67dea0 100644 --- a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift @@ -16,7 +16,9 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin, FlutterS instance.initServices() instance.initChannels(registrar.messenger()) registrar.addApplicationDelegate(instance) - registrar.addSceneDelegate(instance) + if #available(iOS 13.0, *) { + registrar.addSceneDelegate(instance) + } } public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { From 7909dba5799555b0d10062ae2c37d0850617f5de Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Fri, 14 Aug 2026 17:05:02 +0900 Subject: [PATCH 17/32] fix(ios): Resolve iOS availability mismatch in FlutterSceneLifeCycleDelegate conformance --- .../SwiftFlutterForegroundTaskPlugin.swift | 15 ++++++++------- .../SwiftFlutterForegroundTaskPlugin.swift | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift b/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift index d5f547d9..a4883127 100644 --- a/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift +++ b/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift @@ -2,7 +2,7 @@ import Flutter import UIKit import BackgroundTasks -public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin, FlutterSceneLifeCycleDelegate { +public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { // ====================== Plugin ====================== static private(set) var registerPlugins: FlutterPluginRegistrantCallback? = nil @@ -125,12 +125,6 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin, FlutterS // Chance to handle onDestroy before app terminates sleep(5) } - - // ================ Scene Lifecycle =================== - @available(iOS 13.0, *) - public func sceneDidEnterBackground(_ scene: UIScene) { - SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() - } // ================= Service Delegate ================= @available(iOS 10.0, *) @@ -209,3 +203,10 @@ class AppRefreshOperation: Operation { semaphore.wait() } } + +@available(iOS 13.0, *) +extension SwiftFlutterForegroundTaskPlugin: FlutterSceneLifeCycleDelegate { + public func sceneDidEnterBackground(_ scene: UIScene) { + SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() + } +} diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift index 5f67dea0..a4883127 100644 --- a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift @@ -2,7 +2,7 @@ import Flutter import UIKit import BackgroundTasks -public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin, FlutterSceneLifeCycleDelegate { +public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { // ====================== Plugin ====================== static private(set) var registerPlugins: FlutterPluginRegistrantCallback? = nil @@ -126,12 +126,6 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin, FlutterS sleep(5) } - // ================ Scene Lifecycle =================== - @available(iOS 13.0, *) - public func sceneDidEnterBackground(_ scene: UIScene) { - SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() - } - // ================= Service Delegate ================= @available(iOS 10.0, *) public func userNotificationCenter(_ center: UNUserNotificationCenter, @@ -209,3 +203,10 @@ class AppRefreshOperation: Operation { semaphore.wait() } } + +@available(iOS 13.0, *) +extension SwiftFlutterForegroundTaskPlugin: FlutterSceneLifeCycleDelegate { + public func sceneDidEnterBackground(_ scene: UIScene) { + SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() + } +} From 1e7ec26a25310dc552394012b35ebf86fb919219 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Fri, 14 Aug 2026 18:20:20 +0900 Subject: [PATCH 18/32] ci(ios): Disable Swift Package Manager --- .github/workflows/ci_master.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci_master.yml b/.github/workflows/ci_master.yml index 55563c51..339ca364 100644 --- a/.github/workflows/ci_master.yml +++ b/.github/workflows/ci_master.yml @@ -65,6 +65,9 @@ jobs: channel: stable - run: flutter --version + - name: Disable Swift Package Manager + run: flutter config --no-enable-swift-package-manager + - name: Build example app working-directory: example run: flutter build ios --debug --simulator From f773bf23be1b4ef9ea79aa4e4cd65d9728f0a11d Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Tue, 18 Aug 2026 13:42:00 +0900 Subject: [PATCH 19/32] Revert "ci(ios): Disable Swift Package Manager" This reverts commit 1e7ec26a25310dc552394012b35ebf86fb919219. --- .github/workflows/ci_master.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci_master.yml b/.github/workflows/ci_master.yml index 339ca364..55563c51 100644 --- a/.github/workflows/ci_master.yml +++ b/.github/workflows/ci_master.yml @@ -65,9 +65,6 @@ jobs: channel: stable - run: flutter --version - - name: Disable Swift Package Manager - run: flutter config --no-enable-swift-package-manager - - name: Build example app working-directory: example run: flutter build ios --debug --simulator From a8c0b5943ae03c586a39feb1d2eef1af4641425b Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Tue, 18 Aug 2026 12:11:08 +0900 Subject: [PATCH 20/32] feat(ios): add Swift Package Manager support --- example/ios/Runner.xcodeproj/project.pbxproj | 46 ++-- .../xcshareddata/xcschemes/Runner.xcscheme | 18 ++ example/ios/Runner/AppDelegate.swift | 1 + example/ios/Runner/Runner-Bridging-Header.h | 2 +- ios/Assets/.gitkeep | 0 ...utterForegroundTaskLifecycleListener.swift | 35 --- ios/Classes/FlutterForegroundTaskPlugin.h | 4 - ios/Classes/FlutterForegroundTaskPlugin.m | 27 --- .../FlutterForegroundTaskStarter.swift | 13 -- ios/Classes/PreferencesKey.swift | 26 --- .../SwiftFlutterForegroundTaskPlugin.swift | 212 ------------------ ios/Classes/errors/ServiceError.swift | 30 --- .../models/BackgroundServiceAction.swift | 17 -- .../models/BackgroundServiceStatus.swift | 25 --- ios/Classes/models/ForegroundTaskData.swift | 42 ---- .../models/ForegroundTaskEventAction.swift | 39 ---- .../models/ForegroundTaskEventType.swift | 18 -- .../models/ForegroundTaskOptions.swift | 64 ------ ios/Classes/models/NotificationButton.swift | 33 --- ios/Classes/models/NotificationContent.swift | 79 ------- ios/Classes/models/NotificationOptions.swift | 38 ---- .../models/NotificationPermission.swift | 13 -- ios/Classes/service/BackgroundService.swift | 206 ----------------- .../service/BackgroundServiceManager.swift | 96 -------- ios/Classes/service/ForegroundTask.swift | 192 ---------------- .../ForegroundTaskLifecycleListeners.swift | 54 ----- .../NotificationPermissionManager.swift | 38 ---- ios/flutter_foreground_task.podspec | 2 +- ios/flutter_foreground_task/Package.swift | 17 +- ...FlutterForegroundTaskPluginSPMBridge.swift | 23 ++ .../Flutterforegroundtaskearlyregistration.m | 11 + .../Flutterforegroundtaskearlyregistration.h | 6 + pubspec.yaml | 2 +- 33 files changed, 103 insertions(+), 1326 deletions(-) delete mode 100644 ios/Assets/.gitkeep delete mode 100644 ios/Classes/FlutterForegroundTaskLifecycleListener.swift delete mode 100644 ios/Classes/FlutterForegroundTaskPlugin.h delete mode 100644 ios/Classes/FlutterForegroundTaskPlugin.m delete mode 100644 ios/Classes/FlutterForegroundTaskStarter.swift delete mode 100644 ios/Classes/PreferencesKey.swift delete mode 100644 ios/Classes/SwiftFlutterForegroundTaskPlugin.swift delete mode 100644 ios/Classes/errors/ServiceError.swift delete mode 100644 ios/Classes/models/BackgroundServiceAction.swift delete mode 100644 ios/Classes/models/BackgroundServiceStatus.swift delete mode 100644 ios/Classes/models/ForegroundTaskData.swift delete mode 100644 ios/Classes/models/ForegroundTaskEventAction.swift delete mode 100644 ios/Classes/models/ForegroundTaskEventType.swift delete mode 100644 ios/Classes/models/ForegroundTaskOptions.swift delete mode 100644 ios/Classes/models/NotificationButton.swift delete mode 100644 ios/Classes/models/NotificationContent.swift delete mode 100644 ios/Classes/models/NotificationOptions.swift delete mode 100644 ios/Classes/models/NotificationPermission.swift delete mode 100644 ios/Classes/service/BackgroundService.swift delete mode 100644 ios/Classes/service/BackgroundServiceManager.swift delete mode 100644 ios/Classes/service/ForegroundTask.swift delete mode 100644 ios/Classes/service/ForegroundTaskLifecycleListeners.swift delete mode 100644 ios/Classes/service/NotificationPermissionManager.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskPluginSPMBridge.swift create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task_early_registration/Flutterforegroundtaskearlyregistration.m create mode 100644 ios/flutter_foreground_task/Sources/flutter_foreground_task_early_registration/include/flutter_foreground_task_early_registration/Flutterforegroundtaskearlyregistration.h diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index fb86ac29..6e5c4144 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 37B6493A4744452DDC4DEBF7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A32D55799B66D9DF641C7ED8 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -36,6 +37,9 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = Flutter/ephemeral/Packages/.packages/FlutterFramework; sourceTree = ""; }; + 78DABEA22ED26510000E7860 /* flutter_foreground_task */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = flutter_foreground_task; path = ../../ios/flutter_foreground_task; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 8BA7286763F4F5A5D4D4791F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -54,6 +58,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 37B6493A4744452DDC4DEBF7 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -74,6 +79,9 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78DABEA22ED26510000E7860 /* flutter_foreground_task */, + 784666492D4C4C64000A1A5F /* FlutterFramework */, + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -138,13 +146,15 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 6EA6E717D2A4A7547969F4C2 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -173,6 +183,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -213,23 +226,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 6EA6E717D2A4A7547969F4C2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 8D396599160DECB90EDD47BE /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -552,6 +548,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a9d43dad..d5a29ad2 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + +//#import diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/ios/Classes/FlutterForegroundTaskLifecycleListener.swift b/ios/Classes/FlutterForegroundTaskLifecycleListener.swift deleted file mode 100644 index c4879001..00000000 --- a/ios/Classes/FlutterForegroundTaskLifecycleListener.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// FlutterForegroundTaskLifecycleListener.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 7/15/24. -// - -import Foundation -import Flutter - -/** A listener that can listen to the task lifecycle events. */ -public protocol FlutterForegroundTaskLifecycleListener : AnyObject { - /** - * Each time a task starts, a new FlutterEngine is created. - * - * This is called before onTaskStart, Initialize the service you want to use in the task. (like PlatformChannel initialization) - */ - func onEngineCreate(flutterEngine: FlutterEngine?) - - /** Called when the task is started. */ - func onTaskStart(starter: FlutterForegroundTaskStarter) - - /** Called based on the eventAction set in ForegroundTaskOptions. */ - func onTaskRepeatEvent() - - /** Called when the task is destroyed. */ - func onTaskDestroy() - - /** - * If one task is finished or replaced by another, the FlutterEngine is destroyed. - * - * This is called after onTaskDestroy, where dispose the service that was initialized in onEngineCreate. - */ - func onEngineWillDestroy() -} diff --git a/ios/Classes/FlutterForegroundTaskPlugin.h b/ios/Classes/FlutterForegroundTaskPlugin.h deleted file mode 100644 index db8610b7..00000000 --- a/ios/Classes/FlutterForegroundTaskPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface FlutterForegroundTaskPlugin : NSObject -@end diff --git a/ios/Classes/FlutterForegroundTaskPlugin.m b/ios/Classes/FlutterForegroundTaskPlugin.m deleted file mode 100644 index ec7add77..00000000 --- a/ios/Classes/FlutterForegroundTaskPlugin.m +++ /dev/null @@ -1,27 +0,0 @@ -#import "FlutterForegroundTaskPlugin.h" -#if __has_include() -#import -#else -// Support project import fallback if the generated compatibility header -// is not copied when this plugin is created as a library. -// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "flutter_foreground_task-Swift.h" -#endif - -@implementation FlutterForegroundTaskPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftFlutterForegroundTaskPlugin registerWithRegistrar:registrar]; -} -+ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { - [SwiftFlutterForegroundTaskPlugin setPluginRegistrantCallback:callback]; -} -@end - -@interface FlutterForegroundTaskEarlyRegistration : NSObject -@end - -@implementation FlutterForegroundTaskEarlyRegistration -+ (void)load { - [SwiftFlutterForegroundTaskPlugin registerAppRefreshForBackgroundLaunch]; -} -@end diff --git a/ios/Classes/FlutterForegroundTaskStarter.swift b/ios/Classes/FlutterForegroundTaskStarter.swift deleted file mode 100644 index cf69cf83..00000000 --- a/ios/Classes/FlutterForegroundTaskStarter.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// FlutterForegroundTaskStarter.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 9/23/24. -// - -import Foundation - -public enum FlutterForegroundTaskStarter : Int { - case DEVELOPER = 0 - case SYSTEM = 1 -} diff --git a/ios/Classes/PreferencesKey.swift b/ios/Classes/PreferencesKey.swift deleted file mode 100644 index da46a244..00000000 --- a/ios/Classes/PreferencesKey.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// BackgroundServicePrefsKey.swift -// flutter_foreground_task -// -// Created by WOO JIN HWANG on 2021/08/11. -// - -// service status -let BACKGROUND_SERVICE_ACTION = "backgroundServiceAction" - -// notification options -let SHOW_NOTIFICATION = "showNotification" -let PLAY_SOUND = "playSound" - -// notification content -let NOTIFICATION_CONTENT_TITLE = "notificationContentTitle" -let NOTIFICATION_CONTENT_TEXT = "notificationContentText" -let NOTIFICATION_CONTENT_BUTTONS = "buttons" - -// task options -let TASK_EVENT_ACTION = "taskEventAction" // new -let INTERVAL = "interval" // deprecated -let IS_ONCE_EVENT = "isOnceEvent" // deprecated - -// task data -let CALLBACK_HANDLE = "callbackHandle" diff --git a/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift b/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift deleted file mode 100644 index a4883127..00000000 --- a/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift +++ /dev/null @@ -1,212 +0,0 @@ -import Flutter -import UIKit -import BackgroundTasks - -public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { - // ====================== Plugin ====================== - static private(set) var registerPlugins: FlutterPluginRegistrantCallback? = nil - - private var notificationPermissionManager: NotificationPermissionManager? = nil - private var backgroundServiceManager: BackgroundServiceManager? = nil - - private var foregroundChannel: FlutterMethodChannel? = nil - - public static func register(with registrar: FlutterPluginRegistrar) { - let instance = SwiftFlutterForegroundTaskPlugin() - instance.initServices() - instance.initChannels(registrar.messenger()) - registrar.addApplicationDelegate(instance) - if #available(iOS 13.0, *) { - registrar.addSceneDelegate(instance) - } - } - - public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { - registerPlugins = callback - } - - @objc public static func registerAppRefreshForBackgroundLaunch() { - if #available(iOS 13.0, *) { - let permitted = Bundle.main.object(forInfoDictionaryKey: "BGTaskSchedulerPermittedIdentifiers") as? [String] ?? [] - guard permitted.contains(refreshIdentifier) else { return } - registerAppRefresh() - } - } - - public static func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { - BackgroundService.sharedInstance.addTaskLifecycleListener(listener) - } - - public static func removeTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { - BackgroundService.sharedInstance.removeTaskLifecycleListener(listener) - } - - private func initServices() { - notificationPermissionManager = NotificationPermissionManager() - backgroundServiceManager = BackgroundServiceManager() - } - - private func initChannels(_ messenger: FlutterBinaryMessenger) { - foregroundChannel = FlutterMethodChannel(name: "flutter_foreground_task/methods", binaryMessenger: messenger) - foregroundChannel?.setMethodCallHandler(onMethodCall) - } - - private func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { - let args = call.arguments - do { - switch call.method { - case "checkNotificationPermission": - notificationPermissionManager!.checkPermission { permission in - result(permission.rawValue) - } - case "requestNotificationPermission": - notificationPermissionManager!.requestPermission { permission in - result(permission.rawValue) - } - case "startService": - try backgroundServiceManager!.start(arguments: args) - result(true) - case "restartService": - try backgroundServiceManager!.restart(arguments: args) - result(true) - case "updateService": - try backgroundServiceManager!.update(arguments: args) - result(true) - case "stopService": - try backgroundServiceManager!.stop() - result(true) - case "sendData": - backgroundServiceManager!.sendData(data: args) - case "isRunningService": - result(backgroundServiceManager!.isRunningService()) - case "minimizeApp": - UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) - case "isAppOnForeground": - result(UIApplication.shared.applicationState == .active) - default: - result(FlutterMethodNotImplemented) - } - } catch { - let code = String(describing: error.self) - let message = error.localizedDescription - let flutterError = FlutterError(code: code, message: message, details: nil) - result(flutterError) - } - } - - // ================== App Lifecycle =================== - public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable : Any] = [:]) -> Bool { - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) - if #available(iOS 13.0, *) { - SwiftFlutterForegroundTaskPlugin.registerAppRefresh() - } - return true - } - - public func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool { - completionHandler(.newData) - return true - } - - public func applicationDidEnterBackground(_ application: UIApplication) { - if #available(iOS 13.0, *) { - SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() - } - } - - public func applicationWillTerminate(_ application: UIApplication) { - if !BackgroundService.sharedInstance.isRunningService { - return - } - - BackgroundServiceStatus.setData(action: BackgroundServiceAction.APP_TERMINATE) - BackgroundService.sharedInstance.run() - - // Chance to handle onDestroy before app terminates - sleep(5) - } - - // ================= Service Delegate ================= - @available(iOS 10.0, *) - public func userNotificationCenter(_ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) { - BackgroundService.sharedInstance.userNotificationCenter(center, response, completionHandler) - } - - @available(iOS 10.0, *) - public func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - BackgroundService.sharedInstance.userNotificationCenter(center, notification, completionHandler) - } - - // ============== Background App Refresh ============== - public static var refreshIdentifier: String = "com.pravera.flutter_foreground_task.refresh" - private static var isBgTaskRegistered: Bool = false - - @available(iOS 13.0, *) - private static func registerAppRefresh() { - guard !isBgTaskRegistered else { return } - isBgTaskRegistered = true - BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshIdentifier, using: nil) { task in - handleAppRefresh(task: task as! BGAppRefreshTask) - } - } - - @available(iOS 13.0, *) - private static func scheduleAppRefresh() { - let request = BGAppRefreshTaskRequest(identifier: refreshIdentifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) - - do { - try BGTaskScheduler.shared.submit(request) - } catch { - print("Could not schedule app refresh: \(error)") - } - } - - @available(iOS 13.0, *) - private static func cancelAppRefresh() { - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshIdentifier) - } - - @available(iOS 13.0, *) - private static func handleAppRefresh(task: BGAppRefreshTask) { - let queue = OperationQueue() - let operation = AppRefreshOperation() - - task.expirationHandler = { - operation.cancel() - } - - operation.completionBlock = { - // Schedule a new refresh task - scheduleAppRefresh() - - task.setTaskCompleted(success: true) - } - - queue.addOperation(operation) - } -} - -class AppRefreshOperation: Operation { - override func main() { - let semaphore = DispatchSemaphore(value: 0) - - // avoid non-platform thread - DispatchQueue.main.asyncAfter(deadline: .now() + 25) { - semaphore.signal() - } - - semaphore.wait() - } -} - -@available(iOS 13.0, *) -extension SwiftFlutterForegroundTaskPlugin: FlutterSceneLifeCycleDelegate { - public func sceneDidEnterBackground(_ scene: UIScene) { - SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() - } -} diff --git a/ios/Classes/errors/ServiceError.swift b/ios/Classes/errors/ServiceError.swift deleted file mode 100644 index c3f8bb35..00000000 --- a/ios/Classes/errors/ServiceError.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ServiceError.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 7/17/24. -// - -import Foundation - -enum ServiceError { - case ServiceArgumentNullException - case ServiceAlreadyStartedException - case ServiceNotStartedException - case ServiceNotSupportedException -} - -extension ServiceError : LocalizedError { - public var errorDescription: String? { - switch self { - case .ServiceArgumentNullException: - return NSLocalizedString("The required argument was not passed to the service.", comment: "ServiceArgumentNullException") - case .ServiceAlreadyStartedException: - return NSLocalizedString("The service has already started.", comment: "ServiceAlreadyStartedException") - case .ServiceNotStartedException: - return NSLocalizedString("The service is not started.", comment: "ServiceNotStartedException") - case .ServiceNotSupportedException: - return NSLocalizedString("The current iOS version does not support the service.", comment: "ServiceNotSupportedException") - } - } -} diff --git a/ios/Classes/models/BackgroundServiceAction.swift b/ios/Classes/models/BackgroundServiceAction.swift deleted file mode 100644 index 4c39f27a..00000000 --- a/ios/Classes/models/BackgroundServiceAction.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// BackgroundServiceAction.swift -// flutter_foreground_task -// -// Created by WOO JIN HWANG on 2021/08/11. -// - -import Foundation - -enum BackgroundServiceAction: String { - case API_START - case API_RESTART - case API_UPDATE - case API_STOP - - case APP_TERMINATE -} diff --git a/ios/Classes/models/BackgroundServiceStatus.swift b/ios/Classes/models/BackgroundServiceStatus.swift deleted file mode 100644 index 7bd95995..00000000 --- a/ios/Classes/models/BackgroundServiceStatus.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// BackgroundServiceStatus.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 9/13/24. -// - -import Foundation - -struct BackgroundServiceStatus { - let action: BackgroundServiceAction - - static func getData() -> BackgroundServiceStatus { - let prefs = UserDefaults.standard - let actionValue = prefs.string(forKey: BACKGROUND_SERVICE_ACTION) ?? BackgroundServiceAction.API_STOP.rawValue - let action = BackgroundServiceAction(rawValue: actionValue)! - - return BackgroundServiceStatus(action: action) - } - - static func setData(action: BackgroundServiceAction) { - let prefs = UserDefaults.standard - prefs.set(action.rawValue, forKey: BACKGROUND_SERVICE_ACTION) - } -} diff --git a/ios/Classes/models/ForegroundTaskData.swift b/ios/Classes/models/ForegroundTaskData.swift deleted file mode 100644 index 1a35a261..00000000 --- a/ios/Classes/models/ForegroundTaskData.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ForegroundTaskData.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 9/24/24. -// - -import Foundation - -struct ForegroundTaskData { - let callbackHandle: Int64? - - static func getData() -> ForegroundTaskData { - let prefs = UserDefaults.standard - - let callbackHandle = prefs.object(forKey: CALLBACK_HANDLE) as? Int64 - - return ForegroundTaskData(callbackHandle: callbackHandle) - } - - static func setData(args: Dictionary) { - let prefs = UserDefaults.standard - - prefs.removeObject(forKey: CALLBACK_HANDLE) - if let callbackHandle = args[CALLBACK_HANDLE] as? Int64 { - prefs.set(callbackHandle, forKey: CALLBACK_HANDLE) - } - } - - static func updateData(args: Dictionary) { - let prefs = UserDefaults.standard - - if let callbackHandle = args[CALLBACK_HANDLE] as? Int64 { - prefs.set(callbackHandle, forKey: CALLBACK_HANDLE) - } - } - - static func clearData() { - let prefs = UserDefaults.standard - prefs.removeObject(forKey: CALLBACK_HANDLE) - } -} diff --git a/ios/Classes/models/ForegroundTaskEventAction.swift b/ios/Classes/models/ForegroundTaskEventAction.swift deleted file mode 100644 index ac52a1b4..00000000 --- a/ios/Classes/models/ForegroundTaskEventAction.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ForegroundTaskEventAction.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 8/28/24. -// - -import Foundation - -private let TASK_EVENT_TYPE_KEY = "taskEventType" -private let TASK_EVENT_INTERVAL_KEY = "taskEventInterval" - -struct ForegroundTaskEventAction: Equatable { - let type: ForegroundTaskEventType - let interval: Int - - static func fromJsonString(_ jsonString: String) -> ForegroundTaskEventAction { - var type: ForegroundTaskEventType = .NOTHING - var interval: Int = 5000 - - if let jsonData = jsonString.data(using: .utf8) { - if let jsonObj = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? Dictionary { - if let _type = jsonObj[TASK_EVENT_TYPE_KEY] as? Int { - type = ForegroundTaskEventType.fromValue(_type) - } - - if let _interval = jsonObj[TASK_EVENT_INTERVAL_KEY] as? Int { - interval = _interval - } - } - } - - return ForegroundTaskEventAction(type: type, interval: interval) - } - - static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.type.rawValue == rhs.type.rawValue && lhs.interval == rhs.interval - } -} diff --git a/ios/Classes/models/ForegroundTaskEventType.swift b/ios/Classes/models/ForegroundTaskEventType.swift deleted file mode 100644 index 224eba1c..00000000 --- a/ios/Classes/models/ForegroundTaskEventType.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ForegroundTaskEventType.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 8/28/24. -// - -import Foundation - -enum ForegroundTaskEventType: Int { - case NOTHING = 1 - case ONCE = 2 - case REPEAT = 3 - - static func fromValue(_ value: Int) -> ForegroundTaskEventType { - return ForegroundTaskEventType(rawValue: value) ?? .NOTHING - } -} diff --git a/ios/Classes/models/ForegroundTaskOptions.swift b/ios/Classes/models/ForegroundTaskOptions.swift deleted file mode 100644 index c5cb1f93..00000000 --- a/ios/Classes/models/ForegroundTaskOptions.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ForegroundTaskOptions.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 9/24/24. -// - -import Foundation - -struct ForegroundTaskOptions { - let eventAction: ForegroundTaskEventAction - - static func getData() -> ForegroundTaskOptions { - let prefs = UserDefaults.standard - - let eventActionJsonString = prefs.string(forKey: TASK_EVENT_ACTION) - let eventAction: ForegroundTaskEventAction - if eventActionJsonString != nil { - eventAction = ForegroundTaskEventAction.fromJsonString(eventActionJsonString!) - } else { - // for deprecated api - let oldIsOnceEvent = prefs.bool(forKey: IS_ONCE_EVENT) - let oldInterval = prefs.integer(forKey: INTERVAL) - if oldIsOnceEvent { - eventAction = ForegroundTaskEventAction(type: .ONCE, interval: oldInterval) - } else { - eventAction = ForegroundTaskEventAction(type: .REPEAT, interval: oldInterval) - } - } - - return ForegroundTaskOptions(eventAction: eventAction) - } - - static func setData(args: Dictionary) { - let prefs = UserDefaults.standard - - if let eventActionJson = args[TASK_EVENT_ACTION] as? Dictionary { - if let eventActionJsonData = try? JSONSerialization.data(withJSONObject: eventActionJson, options: []) { - if let eventActionJsonString = String(data: eventActionJsonData, encoding: .utf8) { - prefs.set(eventActionJsonString, forKey: TASK_EVENT_ACTION) - } - } - } - } - - static func updateData(args: Dictionary) { - let prefs = UserDefaults.standard - - if let eventActionJson = args[TASK_EVENT_ACTION] as? Dictionary { - if let eventActionJsonData = try? JSONSerialization.data(withJSONObject: eventActionJson, options: []) { - if let eventActionJsonString = String(data: eventActionJsonData, encoding: .utf8) { - prefs.set(eventActionJsonString, forKey: TASK_EVENT_ACTION) - } - } - } - } - - static func clearData() { - let prefs = UserDefaults.standard - prefs.removeObject(forKey: TASK_EVENT_ACTION) // new - prefs.removeObject(forKey: INTERVAL) // deprecated - prefs.removeObject(forKey: IS_ONCE_EVENT) // deprecated - } -} diff --git a/ios/Classes/models/NotificationButton.swift b/ios/Classes/models/NotificationButton.swift deleted file mode 100644 index 5ea18708..00000000 --- a/ios/Classes/models/NotificationButton.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// NotificationButton.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 8/5/24. -// - -import Foundation - -private let ID_KEY = "id" -private let TEXT_KEY = "text" - -struct NotificationButton { - let id: String - let text: String - - static func fromJSONObject(_ jsonObj: Any) -> NotificationButton { - var id: String = "unknown" - var text: String = "" - - if let _jsonObj = jsonObj as? Dictionary { - if let _id = _jsonObj[ID_KEY] as? String { - id = _id - } - - if let _text = _jsonObj[TEXT_KEY] as? String { - text = _text - } - } - - return NotificationButton(id: id, text: text) - } -} diff --git a/ios/Classes/models/NotificationContent.swift b/ios/Classes/models/NotificationContent.swift deleted file mode 100644 index 2d44f3c9..00000000 --- a/ios/Classes/models/NotificationContent.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// NotificationContent.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 8/2/24. -// - -import Foundation - -struct NotificationContent { - let title: String - let text: String - let buttons: Array - - static func getData() -> NotificationContent { - let prefs = UserDefaults.standard - - let title = prefs.string(forKey: NOTIFICATION_CONTENT_TITLE) ?? "" - let text = prefs.string(forKey: NOTIFICATION_CONTENT_TEXT) ?? "" - - var buttons: [NotificationButton] = [] - if let buttonsJsonString = prefs.string(forKey: NOTIFICATION_CONTENT_BUTTONS) { - if let buttonsJsonData = buttonsJsonString.data(using: .utf8) { - if let buttonsJsonArr = try? JSONSerialization.jsonObject(with: buttonsJsonData, options: []) as? [Any] { - for buttonJsonObj in buttonsJsonArr { - buttons.append(NotificationButton.fromJSONObject(buttonJsonObj)) - } - } - } - } - - return NotificationContent(title: title, text: text, buttons: buttons) - } - - static func setData(args: Dictionary) { - let prefs = UserDefaults.standard - - let title = args[NOTIFICATION_CONTENT_TITLE] as? String ?? "" - prefs.set(title, forKey: NOTIFICATION_CONTENT_TITLE) - - let text = args[NOTIFICATION_CONTENT_TEXT] as? String ?? "" - prefs.set(text, forKey: NOTIFICATION_CONTENT_TEXT) - - if let buttonsJson = args[NOTIFICATION_CONTENT_BUTTONS] as? [Any] { - if let buttonsJsonData = try? JSONSerialization.data(withJSONObject: buttonsJson, options: []) { - if let buttonsJsonString = String(data: buttonsJsonData, encoding: .utf8) { - prefs.set(buttonsJsonString, forKey: NOTIFICATION_CONTENT_BUTTONS) - } - } - } - } - - static func updateData(args: Dictionary) { - let prefs = UserDefaults.standard - - if let title = args[NOTIFICATION_CONTENT_TITLE] as? String { - prefs.set(title, forKey: NOTIFICATION_CONTENT_TITLE) - } - - if let text = args[NOTIFICATION_CONTENT_TEXT] as? String { - prefs.set(text, forKey: NOTIFICATION_CONTENT_TEXT) - } - - if let buttonsJson = args[NOTIFICATION_CONTENT_BUTTONS] as? [Any] { - if let buttonsJsonData = try? JSONSerialization.data(withJSONObject: buttonsJson, options: []) { - if let buttonsJsonString = String(data: buttonsJsonData, encoding: .utf8) { - prefs.set(buttonsJsonString, forKey: NOTIFICATION_CONTENT_BUTTONS) - } - } - } - } - - static func clearData() { - let prefs = UserDefaults.standard - prefs.removeObject(forKey: NOTIFICATION_CONTENT_TITLE) - prefs.removeObject(forKey: NOTIFICATION_CONTENT_TEXT) - prefs.removeObject(forKey: NOTIFICATION_CONTENT_BUTTONS) - } -} diff --git a/ios/Classes/models/NotificationOptions.swift b/ios/Classes/models/NotificationOptions.swift deleted file mode 100644 index 14fa854a..00000000 --- a/ios/Classes/models/NotificationOptions.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// NotificationOptions.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 8/2/24. -// - -import Foundation - -struct NotificationOptions { - let showNotification: Bool - let playSound: Bool - - static func getData() -> NotificationOptions { - let prefs = UserDefaults.standard - - let showNotification = prefs.bool(forKey: SHOW_NOTIFICATION) - let playSound = prefs.bool(forKey: PLAY_SOUND) - - return NotificationOptions(showNotification: showNotification, playSound: playSound) - } - - static func setData(args: Dictionary) { - let prefs = UserDefaults.standard - - let showNotification = args[SHOW_NOTIFICATION] as? Bool ?? false - let playSound = args[PLAY_SOUND] as? Bool ?? false - - prefs.set(showNotification, forKey: SHOW_NOTIFICATION) - prefs.set(playSound, forKey: PLAY_SOUND) - } - - static func clearData() { - let prefs = UserDefaults.standard - prefs.removeObject(forKey: SHOW_NOTIFICATION) - prefs.removeObject(forKey: PLAY_SOUND) - } -} diff --git a/ios/Classes/models/NotificationPermission.swift b/ios/Classes/models/NotificationPermission.swift deleted file mode 100644 index 157bd9db..00000000 --- a/ios/Classes/models/NotificationPermission.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// NotificationPermission.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 8/6/24. -// - -import Foundation - -enum NotificationPermission : Int { - case GRANTED = 0 - case DENIED = 1 -} diff --git a/ios/Classes/service/BackgroundService.swift b/ios/Classes/service/BackgroundService.swift deleted file mode 100644 index 4029ad80..00000000 --- a/ios/Classes/service/BackgroundService.swift +++ /dev/null @@ -1,206 +0,0 @@ -// -// BackgroundService.swift -// flutter_foreground_task -// -// Created by WOO JIN HWANG on 2021/08/11. -// - -import Flutter -import Foundation -import UserNotifications - -private let NOTIFICATION_ID = "flutter_foreground_task/notification" -private let NOTIFICATION_CATEGORY_ID = "flutter_foreground_task/notification_category" - -private let ACTION_RECEIVE_DATA = "onReceiveData" -private let ACTION_NOTIFICATION_BUTTON_PRESSED = "onNotificationButtonPressed" -private let ACTION_NOTIFICATION_PRESSED = "onNotificationPressed" -private let ACTION_NOTIFICATION_DISMISSED = "onNotificationDismissed" - -@available(iOS 10.0, *) -class BackgroundService: NSObject { - static let sharedInstance = BackgroundService() - - private(set) var isRunningService: Bool = false - - private var foregroundTask: ForegroundTask? = nil - private var taskLifecycleListeners = ForegroundTaskLifecycleListeners() - - func sendData(data: Any?) { - if isRunningService { - foregroundTask?.invokeMethod(ACTION_RECEIVE_DATA, arguments: data) - } - } - - func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { - taskLifecycleListeners.addListener(listener) - } - - func removeTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { - taskLifecycleListeners.removeListener(listener) - } - - private let notificationCenter: UNUserNotificationCenter - private let notificationPermissionManager: NotificationPermissionManager - private var canReceiveNotificationResponse: Bool = false - - private var backgroundServiceStatus: BackgroundServiceStatus - private var notificationOptions: NotificationOptions - private var notificationContent: NotificationContent - private var prevForegroundTaskOptions: ForegroundTaskOptions? - private var currForegroundTaskOptions: ForegroundTaskOptions - private var prevForegroundTaskData: ForegroundTaskData? - private var currForegroundTaskData: ForegroundTaskData - - override init() { - notificationCenter = UNUserNotificationCenter.current() - notificationPermissionManager = NotificationPermissionManager() - backgroundServiceStatus = BackgroundServiceStatus.getData() - notificationOptions = NotificationOptions.getData() - notificationContent = NotificationContent.getData() - currForegroundTaskOptions = ForegroundTaskOptions.getData() - currForegroundTaskData = ForegroundTaskData.getData() - super.init() - } - - func run() { - backgroundServiceStatus = BackgroundServiceStatus.getData() - notificationOptions = NotificationOptions.getData() - notificationContent = NotificationContent.getData() - prevForegroundTaskOptions = currForegroundTaskOptions - currForegroundTaskOptions = ForegroundTaskOptions.getData() - prevForegroundTaskData = currForegroundTaskData - currForegroundTaskData = ForegroundTaskData.getData() - - switch backgroundServiceStatus.action { - case .API_START, .API_RESTART: - requestNotification() - createForegroundTask() - isRunningService = true - break - case .API_UPDATE: - requestNotification() - let prevCallbackHandle = prevForegroundTaskData?.callbackHandle - let currCallbackHandle = currForegroundTaskData.callbackHandle - if prevCallbackHandle != currCallbackHandle { - createForegroundTask() - } else { - let prevEventAction = prevForegroundTaskOptions?.eventAction - let currEventAction = currForegroundTaskOptions.eventAction - if prevEventAction != currEventAction { - updateForegroundTask() - } - } - break - case .API_STOP, .APP_TERMINATE: - destroyForegroundTask() - removeAllNotification() - isRunningService = false - break - } - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, - _ response: UNNotificationResponse, - _ completionHandler: @escaping () -> Void) { - // If it is not a notification requested by this plugin, the processing below is ignored. - if response.notification.request.identifier != NOTIFICATION_ID { return } - - // Prevents duplicate processing due to the `registrar.addApplicationDelegate`. - if !canReceiveNotificationResponse { return } - canReceiveNotificationResponse = false - - let actionId = response.actionIdentifier - if notificationContent.buttons.contains(where: { $0.id == actionId }) { - foregroundTask?.invokeMethod(ACTION_NOTIFICATION_BUTTON_PRESSED, arguments: actionId) - } else if actionId == UNNotificationDefaultActionIdentifier { - foregroundTask?.invokeMethod(ACTION_NOTIFICATION_PRESSED, arguments: nil) - } else if actionId == UNNotificationDismissActionIdentifier { - foregroundTask?.invokeMethod(ACTION_NOTIFICATION_DISMISSED, arguments: nil) - } - - completionHandler() - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, - _ notification: UNNotification, - _ completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - // If it is not a notification requested by this plugin, the processing below is ignored. - if notification.request.identifier != NOTIFICATION_ID { return } - - if notificationOptions.playSound { - completionHandler([.alert, .sound]) - } else { - completionHandler([.alert]) - } - - // Prevents duplicate processing due to the `registrar.addApplicationDelegate`. - canReceiveNotificationResponse = true - } - - private func setNotificationActions() { - var actions: [UNNotificationAction] = [] - for button in notificationContent.buttons { - let action = UNNotificationAction(identifier: button.id, title: button.text) - actions.append(action) - } - - let category = UNNotificationCategory( - identifier: NOTIFICATION_CATEGORY_ID, - actions: actions, - intentIdentifiers: [], - options: .customDismissAction - ) - - notificationCenter.setNotificationCategories([category]) - } - - private func requestNotification() { - if !notificationOptions.showNotification { - return - } - - notificationPermissionManager.checkPermission { permission in - if permission == NotificationPermission.DENIED { - return - } - - let content = UNMutableNotificationContent() - content.title = self.notificationContent.title - content.body = self.notificationContent.text - content.categoryIdentifier = NOTIFICATION_CATEGORY_ID - if self.notificationOptions.playSound { - content.sound = .default - } - self.setNotificationActions() - - let request = UNNotificationRequest(identifier: NOTIFICATION_ID, content: content, trigger: nil) - self.notificationCenter.add(request, withCompletionHandler: nil) - } - } - - private func removeAllNotification() { - notificationCenter.removePendingNotificationRequests(withIdentifiers: [NOTIFICATION_ID]) - notificationCenter.removeDeliveredNotifications(withIdentifiers: [NOTIFICATION_ID]) - } - - private func createForegroundTask() { - destroyForegroundTask() - - foregroundTask = ForegroundTask( - serviceStatus: backgroundServiceStatus, - taskData: currForegroundTaskData, - taskEventAction: currForegroundTaskOptions.eventAction, - taskLifecycleListener: taskLifecycleListeners - ) - } - - private func updateForegroundTask() { - foregroundTask?.update(taskEventAction: currForegroundTaskOptions.eventAction) - } - - private func destroyForegroundTask() { - foregroundTask?.destroy() - foregroundTask = nil - } -} diff --git a/ios/Classes/service/BackgroundServiceManager.swift b/ios/Classes/service/BackgroundServiceManager.swift deleted file mode 100644 index ae30acb5..00000000 --- a/ios/Classes/service/BackgroundServiceManager.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// BackgroundServiceManager.swift -// flutter_foreground_task -// -// Created by WOO JIN HWANG on 2021/08/10. -// - -import Flutter -import Foundation - -class BackgroundServiceManager: NSObject { - func start(arguments: Any?) throws { - if #available(iOS 10.0, *) { - if BackgroundService.sharedInstance.isRunningService { - throw ServiceError.ServiceAlreadyStartedException - } - - guard let args = arguments as? Dictionary else { - throw ServiceError.ServiceArgumentNullException - } - - BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_START) - NotificationOptions.setData(args: args) - NotificationContent.setData(args: args) - ForegroundTaskOptions.setData(args: args) - ForegroundTaskData.setData(args: args) - BackgroundService.sharedInstance.run() - } else { - throw ServiceError.ServiceNotSupportedException - } - } - - func restart(arguments: Any?) throws { - if #available(iOS 10.0, *) { - if !BackgroundService.sharedInstance.isRunningService { - throw ServiceError.ServiceNotStartedException - } - - BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_RESTART) - BackgroundService.sharedInstance.run() - } else { - throw ServiceError.ServiceNotSupportedException - } - } - - func update(arguments: Any?) throws { - if #available(iOS 10.0, *) { - if !BackgroundService.sharedInstance.isRunningService { - throw ServiceError.ServiceNotStartedException - } - - guard let args = arguments as? Dictionary else { - throw ServiceError.ServiceArgumentNullException - } - - BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_UPDATE) - NotificationContent.updateData(args: args) - ForegroundTaskOptions.updateData(args: args) - ForegroundTaskData.updateData(args: args) - BackgroundService.sharedInstance.run() - } else { - throw ServiceError.ServiceNotSupportedException - } - } - - func stop() throws { - if #available(iOS 10.0, *) { - if !BackgroundService.sharedInstance.isRunningService { - throw ServiceError.ServiceNotStartedException - } - - BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_STOP) - NotificationOptions.clearData() - NotificationContent.clearData() - ForegroundTaskOptions.clearData() - ForegroundTaskData.clearData() - BackgroundService.sharedInstance.run() - } else { - throw ServiceError.ServiceNotSupportedException - } - } - - func sendData(data: Any?) { - if data != nil { - BackgroundService.sharedInstance.sendData(data: data) - } - } - - func isRunningService() -> Bool { - if #available(iOS 10.0, *) { - return BackgroundService.sharedInstance.isRunningService - } else { - return false - } - } -} diff --git a/ios/Classes/service/ForegroundTask.swift b/ios/Classes/service/ForegroundTask.swift deleted file mode 100644 index 35ea4187..00000000 --- a/ios/Classes/service/ForegroundTask.swift +++ /dev/null @@ -1,192 +0,0 @@ -// -// ForegroundTask.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 9/24/24. -// - -import Foundation - -private let BG_ISOLATE_NAME = "flutter_foreground_task/backgroundIsolate" -private let BG_CHANNEL_NAME = "flutter_foreground_task/background" - -private let ACTION_TASK_START = "onStart" -private let ACTION_TASK_REPEAT_EVENT = "onRepeatEvent" -private let ACTION_TASK_DESTROY = "onDestroy" - -class ForegroundTask { - private let serviceStatus: BackgroundServiceStatus - private let taskData: ForegroundTaskData - private var taskEventAction: ForegroundTaskEventAction - private let taskLifecycleListener: FlutterForegroundTaskLifecycleListener - - private var flutterEngine: FlutterEngine? = nil - private var backgroundChannel: FlutterMethodChannel? = nil - private var repeatTask: Timer? = nil - private var isDestroyed: Bool = false - - init( - serviceStatus: BackgroundServiceStatus, - taskData: ForegroundTaskData, - taskEventAction: ForegroundTaskEventAction, - taskLifecycleListener: FlutterForegroundTaskLifecycleListener - ) { - self.serviceStatus = serviceStatus - self.taskData = taskData - self.taskEventAction = taskEventAction - self.taskLifecycleListener = taskLifecycleListener - initialize() - } - - private func initialize() { - guard let registerPlugins = SwiftFlutterForegroundTaskPlugin.registerPlugins else { - print("Please register the registerPlugins function using the SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback.") - return - } - - guard let callbackHandle = taskData.callbackHandle else { - // no callback -> Unlike Android, the flutter engine does not start. - return - } - - // lookup callback - let callbackInfo = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) - guard let entrypoint = callbackInfo?.callbackName else { - print("Entrypoint not found in callback information.") - return - } - guard let libraryURI = callbackInfo?.callbackLibraryPath else { - print("LibraryURI not found in callback information.") - return - } - - // create flutter engine & execute callback - let flutterEngine = FlutterEngine(name: BG_ISOLATE_NAME, project: nil, allowHeadlessExecution: true) - let isRunningEngine = flutterEngine.run(withEntrypoint: entrypoint, libraryURI: libraryURI) - - if isRunningEngine { - // register plugins - registerPlugins(flutterEngine) - taskLifecycleListener.onEngineCreate(flutterEngine: flutterEngine) - - // create background channel - let messenger = flutterEngine.binaryMessenger - let backgroundChannel = FlutterMethodChannel(name: BG_CHANNEL_NAME, binaryMessenger: messenger) - backgroundChannel.setMethodCallHandler(onMethodCall) - - self.flutterEngine = flutterEngine - self.backgroundChannel = backgroundChannel - } - } - - func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "start": - start() - default: - result(FlutterMethodNotImplemented) - } - } - - private func start() { - runIfNotDestroyed { - runIfCallbackHandleExists { - let serviceAction = serviceStatus.action - let starter: FlutterForegroundTaskStarter - if serviceAction == .API_START || serviceAction == .API_RESTART || serviceAction == .API_UPDATE { - starter = .DEVELOPER - } else { - starter = .SYSTEM - } - - backgroundChannel?.invokeMethod(ACTION_TASK_START, arguments: starter.rawValue) { _ in - self.runIfNotDestroyed { - self.startRepeatTask() - } - } - taskLifecycleListener.onTaskStart(starter: starter) - } - } - } - - private func invokeTaskRepeatEvent() { - backgroundChannel?.invokeMethod(ACTION_TASK_REPEAT_EVENT, arguments: nil) - taskLifecycleListener.onTaskRepeatEvent() - } - - private func startRepeatTask() { - stopRepeatTask() - - let type = taskEventAction.type - let interval = TimeInterval(Double(taskEventAction.interval) / 1000) - - if type == .NOTHING { - return - } - - if type == .ONCE { - invokeTaskRepeatEvent() - return - } - - repeatTask = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in - self.invokeTaskRepeatEvent() - } - } - - private func stopRepeatTask() { - repeatTask?.invalidate() - repeatTask = nil - } - - func invokeMethod(_ method: String, arguments: Any?) { - runIfNotDestroyed { - backgroundChannel?.invokeMethod(method, arguments: arguments) - } - } - - func update(taskEventAction: ForegroundTaskEventAction) { - runIfNotDestroyed { - runIfCallbackHandleExists { - self.taskEventAction = taskEventAction - startRepeatTask() - } - } - } - - func destroy() { - runIfNotDestroyed { - stopRepeatTask() - - backgroundChannel?.setMethodCallHandler(nil) - if taskData.callbackHandle == nil { - taskLifecycleListener.onEngineWillDestroy() - flutterEngine?.destroyContext() - flutterEngine = nil - } else { - backgroundChannel?.invokeMethod(ACTION_TASK_DESTROY, arguments: nil) { _ in - self.flutterEngine?.destroyContext() - self.flutterEngine = nil - } - taskLifecycleListener.onTaskDestroy() - taskLifecycleListener.onEngineWillDestroy() - } - - isDestroyed = true - } - } - - private func runIfCallbackHandleExists(call: () -> Void) { - if taskData.callbackHandle == nil { - return - } - call() - } - - private func runIfNotDestroyed(call: () -> Void) { - if isDestroyed { - return - } - call() - } -} diff --git a/ios/Classes/service/ForegroundTaskLifecycleListeners.swift b/ios/Classes/service/ForegroundTaskLifecycleListeners.swift deleted file mode 100644 index 4a578534..00000000 --- a/ios/Classes/service/ForegroundTaskLifecycleListeners.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// ForegroundTaskLifecycleListeners.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 9/24/24. -// - -import Foundation - -class ForegroundTaskLifecycleListeners : FlutterForegroundTaskLifecycleListener { - private var listeners: Array = [] - - func addListener(_ listener: FlutterForegroundTaskLifecycleListener) { - if listeners.contains(where: { $0 === listener }) == false { - listeners.append(listener) - } - } - - func removeListener(_ listener: FlutterForegroundTaskLifecycleListener) { - if let index = listeners.firstIndex(where: { $0 === listener }) { - listeners.remove(at: index) - } - } - - func onEngineCreate(flutterEngine: FlutterEngine?) { - for listener in listeners { - listener.onEngineCreate(flutterEngine: flutterEngine) - } - } - - func onTaskStart(starter: FlutterForegroundTaskStarter) { - for listener in listeners { - listener.onTaskStart(starter: starter) - } - } - - func onTaskRepeatEvent() { - for listener in listeners { - listener.onTaskRepeatEvent() - } - } - - func onTaskDestroy() { - for listener in listeners { - listener.onTaskDestroy() - } - } - - func onEngineWillDestroy() { - for listener in listeners { - listener.onEngineWillDestroy() - } - } -} diff --git a/ios/Classes/service/NotificationPermissionManager.swift b/ios/Classes/service/NotificationPermissionManager.swift deleted file mode 100644 index 461c9f7d..00000000 --- a/ios/Classes/service/NotificationPermissionManager.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// NotificationPermissionManager.swift -// flutter_foreground_task -// -// Created by Woo Jin Hwang on 8/6/24. -// - -import Foundation - -class NotificationPermissionManager { - func checkPermission(completion: @escaping (NotificationPermission) -> Void) { - UNUserNotificationCenter.current().getNotificationSettings { settings in - switch settings.authorizationStatus { - case .authorized: - completion(NotificationPermission.GRANTED) - case .denied, .ephemeral, .notDetermined, .provisional: - completion(NotificationPermission.DENIED) - @unknown default: - completion(NotificationPermission.DENIED) - } - } - } - - func requestPermission(completion: @escaping (NotificationPermission) -> Void) { - let options = UNAuthorizationOptions(arrayLiteral: .alert, .sound) - UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in - if error != nil { - completion(NotificationPermission.DENIED) - } else { - if (granted) { - completion(NotificationPermission.GRANTED) - } else { - completion(NotificationPermission.DENIED) - } - } - } - } -} diff --git a/ios/flutter_foreground_task.podspec b/ios/flutter_foreground_task.podspec index 191bb9ed..6b1d3593 100644 --- a/ios/flutter_foreground_task.podspec +++ b/ios/flutter_foreground_task.podspec @@ -13,7 +13,7 @@ A new Flutter plugin. s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } - s.source_files = 'flutter_foreground_task/Sources/flutter_foreground_task/**/*' + s.source_files = 'flutter_foreground_task/Sources/flutter_foreground_task/**/*.swift' s.dependency 'Flutter' s.platform = :ios, '12.0' diff --git a/ios/flutter_foreground_task/Package.swift b/ios/flutter_foreground_task/Package.swift index dbf136d3..99ac3e84 100644 --- a/ios/flutter_foreground_task/Package.swift +++ b/ios/flutter_foreground_task/Package.swift @@ -4,10 +4,13 @@ import PackageDescription let package = Package( name: "flutter_foreground_task", platforms: [ - .iOS("12.0") + .iOS("13.0") ], products: [ - .library(name: "flutter-foreground-task", targets: ["flutter_foreground_task"]) + .library( + name: "flutter-foreground-task", + targets: ["flutter_foreground_task", "flutter_foreground_task_early_registration"] + ) ], dependencies: [ .package(name: "FlutterFramework", path: "../FlutterFramework") @@ -17,8 +20,14 @@ let package = Package( name: "flutter_foreground_task", dependencies: [ .product(name: "FlutterFramework", package: "FlutterFramework") - ], - path: "Sources/flutter_foreground_task" + ] + ), + .target( + name: "flutter_foreground_task_early_registration", + dependencies: ["flutter_foreground_task"], + cSettings: [ + .headerSearchPath("include/flutter_foreground_task_early_registration") + ] ) ] ) diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskPluginSPMBridge.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskPluginSPMBridge.swift new file mode 100644 index 00000000..8d5b84e6 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/FlutterForegroundTaskPluginSPMBridge.swift @@ -0,0 +1,23 @@ +#if SWIFT_PACKAGE + +import Flutter + +@objc(FlutterForegroundTaskPlugin) +public class FlutterForegroundTaskPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + SwiftFlutterForegroundTaskPlugin.register(with: registrar) + } + + public static func setPluginRegistrantCallback( + _ callback: @escaping FlutterPluginRegistrantCallback + ) { + SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback(callback) + } +} + +@_cdecl("flutter_foreground_task_register_app_refresh") +public func flutter_foreground_task_register_app_refresh() { + SwiftFlutterForegroundTaskPlugin.registerAppRefreshForBackgroundLaunch() +} + +#endif \ No newline at end of file diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task_early_registration/Flutterforegroundtaskearlyregistration.m b/ios/flutter_foreground_task/Sources/flutter_foreground_task_early_registration/Flutterforegroundtaskearlyregistration.m new file mode 100644 index 00000000..2bdf405c --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task_early_registration/Flutterforegroundtaskearlyregistration.m @@ -0,0 +1,11 @@ +#import "FlutterForegroundTaskEarlyRegistration.h" +#import + +@interface FlutterForegroundTaskEarlyRegistration : NSObject +@end + +@implementation FlutterForegroundTaskEarlyRegistration ++ (void)load { + flutter_foreground_task_register_app_refresh(); +} +@end \ No newline at end of file diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task_early_registration/include/flutter_foreground_task_early_registration/Flutterforegroundtaskearlyregistration.h b/ios/flutter_foreground_task/Sources/flutter_foreground_task_early_registration/include/flutter_foreground_task_early_registration/Flutterforegroundtaskearlyregistration.h new file mode 100644 index 00000000..3fafe7f7 --- /dev/null +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task_early_registration/include/flutter_foreground_task_early_registration/Flutterforegroundtaskearlyregistration.h @@ -0,0 +1,6 @@ +#ifndef FlutterForegroundTaskEarlyRegistration_h +#define FlutterForegroundTaskEarlyRegistration_h + +extern void flutter_foreground_task_register_app_refresh(void); + +#endif \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 11ccc643..79420bb3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,4 +26,4 @@ flutter: package: com.pravera.flutter_foreground_task pluginClass: FlutterForegroundTaskPlugin ios: - pluginClass: SwiftFlutterForegroundTaskPlugin + pluginClass: FlutterForegroundTaskPlugin From 1250c1922918e05a21529397f79761a427bc2337 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Tue, 18 Aug 2026 14:08:55 +0900 Subject: [PATCH 21/32] docs: update example app (SPM, UIScene, env) --- example/ios/Flutter/Debug.xcconfig | 1 - example/ios/Flutter/Release.xcconfig | 1 - example/ios/Podfile | 41 -------------------- example/ios/Runner.xcodeproj/project.pbxproj | 41 -------------------- example/ios/Runner/AppDelegate.swift | 8 ++-- example/ios/Runner/Info.plist | 21 ++++++++++ example/pubspec.yaml | 4 +- 7 files changed, 28 insertions(+), 89 deletions(-) delete mode 100644 example/ios/Podfile diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index ec97fc6f..592ceee8 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index c4855bfe..592ceee8 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index e72e0b48..00000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,41 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '13.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 6e5c4144..14a4d00c 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 37B6493A4744452DDC4DEBF7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A32D55799B66D9DF641C7ED8 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; @@ -33,7 +32,6 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2EDD805CC77857CB5704E03A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -41,7 +39,6 @@ 78DABEA22ED26510000E7860 /* flutter_foreground_task */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = flutter_foreground_task; path = ../../ios/flutter_foreground_task; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8BA7286763F4F5A5D4D4791F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -49,8 +46,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9DCDDA24B41ECFF3C6781683 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - A32D55799B66D9DF641C7ED8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -59,7 +54,6 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - 37B6493A4744452DDC4DEBF7 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -69,9 +63,6 @@ 2105D355AEE26F31BDDE1572 /* Pods */ = { isa = PBXGroup; children = ( - 8BA7286763F4F5A5D4D4791F /* Pods-Runner.debug.xcconfig */, - 9DCDDA24B41ECFF3C6781683 /* Pods-Runner.release.xcconfig */, - 2EDD805CC77857CB5704E03A /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -97,7 +88,6 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 2105D355AEE26F31BDDE1572 /* Pods */, - A07F075276D23FBA8C723152 /* Frameworks */, ); sourceTree = ""; }; @@ -124,14 +114,6 @@ path = Runner; sourceTree = ""; }; - A07F075276D23FBA8C723152 /* Frameworks */ = { - isa = PBXGroup; - children = ( - A32D55799B66D9DF641C7ED8 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -139,7 +121,6 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 8D396599160DECB90EDD47BE /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -226,28 +207,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 8D396599160DECB90EDD47BE /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index df55235a..b43ca61c 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -3,13 +3,11 @@ import Flutter import flutter_foreground_task @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback { registry in GeneratedPluginRegistrant.register(with: registry) } @@ -19,4 +17,8 @@ import flutter_foreground_task return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 8bb4e7ea..2c251415 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -53,5 +53,26 @@ UIViewControllerBasedStatusBarAppearance + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b0bbe590..181c3caf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,8 +6,8 @@ description: Demonstrates how to use the flutter_foreground_task plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ^3.4.0 - flutter: ">=3.22.0" + sdk: ^3.12.0 + flutter: ">=3.44.0" dependencies: flutter: From 7c4a1f1724c8683f32003f887f1b97ff817792f9 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Tue, 18 Aug 2026 15:26:15 +0900 Subject: [PATCH 22/32] release: 11.0.0 --- CHANGELOG.md | 7 +++ README.md | 15 ++---- documentation/migration_documentation.md | 63 ++++++++++++++++++++++++ pubspec.yaml | 2 +- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7218cb6a..8ba19b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 11.0.0 + +* [**CHORE**] Bump minimum supported SDK version to `Flutter 3.44/Dart 3.12` +* [**FEAT**] Migrate to built-in Kotlin (KGP) [#385](https://github.com/Dev-hwang/flutter_foreground_task/pull/385) +* [**FEAT**] Support Swift Package Manager (SPM) [#387](https://github.com/Dev-hwang/flutter_foreground_task/pull/387) +* Check [migration_documentation](./documentation/migration_documentation.md) for changes + ## 10.0.0 * [**CHORE**] Bump minimum supported SDK version to `Flutter 3.38.0` diff --git a/README.md b/README.md index 752c8e09..6c616fb2 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This plugin is used to implement a foreground service on the Android platform. - Flutter: `3.22.0+` - Dart: `3.4.0+` - Android: `5.0+ (minSdkVersion: 21)` -- iOS: `12.0+` +- iOS: `13.0+` ## Getting started @@ -27,7 +27,7 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^10.0.0 + flutter_foreground_task: ^11.0.0 ``` After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. @@ -127,9 +127,7 @@ Open the `ios/Runner/AppDelegate.swift` file and add the commented code. ```objc #import "AppDelegate.h" #import "GeneratedPluginRegistrant.h" - -// this -#import +@import flutter_foreground_task; // this (module import) // this void registerPlugins(NSObject* registry) { @@ -156,17 +154,12 @@ void registerPlugins(NSObject* registry) { **Swift**: -Declare the import statement below in the `ios/Runner/Runner-Bridging-Header.h` file. - -```objc -#import -``` - Open the `ios/Runner/AppDelegate.swift` file and add the commented code. ```swift import UIKit import Flutter +import flutter_foreground_task // this (module import) @main @objc class AppDelegate: FlutterAppDelegate { diff --git a/documentation/migration_documentation.md b/documentation/migration_documentation.md index fc11b470..c22f115f 100644 --- a/documentation/migration_documentation.md +++ b/documentation/migration_documentation.md @@ -1,5 +1,68 @@ ## Migration +### ver 11.0.0 + +- Bump minimum supported SDK version to `Flutter 3.44/Dart 3.12`. + +``` +environment: + // sdk: ^3.4.0 + // flutter: ">=3.38.0" + sdk: ^3.12.0 + flutter: ">=3.44.0" +``` + +- Migrate to built-in Kotlin (KGP). + +``` +>> android/app/build.gradle + +plugins { + // id "kotlin-android" +} + +android { + // kotlinOptions { + // jvmTarget = JavaVersion.VERSION_11 + // } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 + } +} +``` + +``` +>> android/gradle/wrapper/gradle-wrapper.properties + +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip + +----------------------------------------------------------------- + +>> android/settings.gradle + +plugins { + id "com.android.application" version "8.11.1" apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false +} +``` + +- Support Swift Package Manager (SPM). The Obj-C bridging header import is no longer needed; use a module import in `AppDelegate.swift` instead. + +``` +>> ios/Runner/Runner-Bridging-Header.h + +// #import + +----------------------------------------------------------------- + +>> ios/Runner/AppDelegate.swift + +import flutter_foreground_task // module import +``` + ### ver 9.0.0 - chore: Bump minimum supported SDK version to `Flutter 3.22/Dart 3.4`. diff --git a/pubspec.yaml b/pubspec.yaml index 79420bb3..52d3b267 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 10.0.0 +version: 11.0.0 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: From c7a6fc0349a4b880998c4433d989359287b37e77 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Thu, 20 Aug 2026 10:14:16 +0900 Subject: [PATCH 23/32] docs: update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6c616fb2..b2b6cd6e 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ This plugin is used to implement a foreground service on the Android platform. ## Support version -- Flutter: `3.22.0+` -- Dart: `3.4.0+` +- Flutter: `3.44.0+` +- Dart: `3.12.0+` - Android: `5.0+ (minSdkVersion: 21)` - iOS: `13.0+` @@ -34,7 +34,7 @@ After adding the plugin to your flutter project, we need to declare the platform ### :baby_chick: Android -This plugin requires `Kotlin version 1.9.10+` and `Gradle version 8.6.0+`. Please refer to the migration documentation for more details. +This plugin requires `Kotlin version 2.2.20+` and `Gradle version 8.11.1+`. Please refer to the migration documentation for more details. - [project/settings.gradle](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/example/android/settings.gradle) - [project/gradle-wrapper.properties](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/example/android/gradle/wrapper/gradle-wrapper.properties) From 2760bc5ec6dfdc342004ea7082754a578ff7f886 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Thu, 20 Aug 2026 10:21:33 +0900 Subject: [PATCH 24/32] chore: upgrade shared_preferences from 2.5.3 to 2.5.5 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 52d3b267..5f291436 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter plugin_platform_interface: ^2.1.8 - shared_preferences: ^2.5.3 + shared_preferences: ^2.5.5 platform: ^3.1.6 dev_dependencies: From 3e40a51fc433092109fdda47155bbf802796114d Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Thu, 20 Aug 2026 10:24:18 +0900 Subject: [PATCH 25/32] release: 11.0.1 --- CHANGELOG.md | 5 +++++ README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba19b98..dd1e98d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 11.0.1 + +* [**CHORE**] Upgrade shared_preferences from 2.5.3 to 2.5.5 +* [**DOCS**] Update README + ## 11.0.0 * [**CHORE**] Bump minimum supported SDK version to `Flutter 3.44/Dart 3.12` diff --git a/README.md b/README.md index b2b6cd6e..15d45363 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^11.0.0 + flutter_foreground_task: ^11.0.1 ``` After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. diff --git a/pubspec.yaml b/pubspec.yaml index 5f291436..7a41b60d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 11.0.0 +version: 11.0.1 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: From 9a959f7c43a23ecae27287242548545bff37b164 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Fri, 4 Sep 2026 09:57:59 +0900 Subject: [PATCH 26/32] fix(ios): Add objective-c shim for FlutterForegroundTaskPlugin --- ios/flutter_foreground_task.podspec | 2 +- ios/flutter_foreground_task/Assets/.gitkeep | 0 .../Classes/FlutterForegroundTaskPlugin.h | 4 +++ .../Classes/FlutterForegroundTaskPlugin.m | 27 +++++++++++++++++++ .../SwiftFlutterForegroundTaskPlugin.swift | 2 +- 5 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 ios/flutter_foreground_task/Assets/.gitkeep create mode 100644 ios/flutter_foreground_task/Classes/FlutterForegroundTaskPlugin.h create mode 100644 ios/flutter_foreground_task/Classes/FlutterForegroundTaskPlugin.m diff --git a/ios/flutter_foreground_task.podspec b/ios/flutter_foreground_task.podspec index 6b1d3593..0efef775 100644 --- a/ios/flutter_foreground_task.podspec +++ b/ios/flutter_foreground_task.podspec @@ -13,7 +13,7 @@ A new Flutter plugin. s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } - s.source_files = 'flutter_foreground_task/Sources/flutter_foreground_task/**/*.swift' + s.source_files = 'flutter_foreground_task/Sources/flutter_foreground_task/**/*.swift', 'Classes/**/*.{h,m}' s.dependency 'Flutter' s.platform = :ios, '12.0' diff --git a/ios/flutter_foreground_task/Assets/.gitkeep b/ios/flutter_foreground_task/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/ios/flutter_foreground_task/Classes/FlutterForegroundTaskPlugin.h b/ios/flutter_foreground_task/Classes/FlutterForegroundTaskPlugin.h new file mode 100644 index 00000000..db8610b7 --- /dev/null +++ b/ios/flutter_foreground_task/Classes/FlutterForegroundTaskPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface FlutterForegroundTaskPlugin : NSObject +@end diff --git a/ios/flutter_foreground_task/Classes/FlutterForegroundTaskPlugin.m b/ios/flutter_foreground_task/Classes/FlutterForegroundTaskPlugin.m new file mode 100644 index 00000000..07c7e1ba --- /dev/null +++ b/ios/flutter_foreground_task/Classes/FlutterForegroundTaskPlugin.m @@ -0,0 +1,27 @@ +#import "FlutterForegroundTaskPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "flutter_foreground_task-Swift.h" +#endif + +@implementation FlutterForegroundTaskPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftFlutterForegroundTaskPlugin registerWithRegistrar:registrar]; +} ++ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { + [SwiftFlutterForegroundTaskPlugin setPluginRegistrantCallback:callback]; +} +@end + +@interface FlutterForegroundTaskEarlyRegistration : NSObject +@end + +@implementation FlutterForegroundTaskEarlyRegistration ++ (void)load { + [SwiftFlutterForegroundTaskPlugin registerAppRefreshForBackgroundLaunch]; +} +@end diff --git a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift index a4883127..33046730 100644 --- a/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift +++ b/ios/flutter_foreground_task/Sources/flutter_foreground_task/SwiftFlutterForegroundTaskPlugin.swift @@ -21,7 +21,7 @@ public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { } } - public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { + @objc public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { registerPlugins = callback } From f2adde7548ac12d7f5d1ffc50c3e49b7305b3794 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Fri, 4 Sep 2026 10:27:11 +0900 Subject: [PATCH 27/32] release: 11.0.2 --- CHANGELOG.md | 4 ++++ README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd1e98d8..147c4fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 11.0.2 + +* [**FIX-iOS**] Add objective-c shim for FlutterForegroundTaskPlugin + ## 11.0.1 * [**CHORE**] Upgrade shared_preferences from 2.5.3 to 2.5.5 diff --git a/README.md b/README.md index 15d45363..e3b8eaf7 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^11.0.1 + flutter_foreground_task: ^11.0.2 ``` After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. diff --git a/pubspec.yaml b/pubspec.yaml index 7a41b60d..e5a6f80f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 11.0.1 +version: 11.0.2 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: From d163b53fff77fd8aa2b02f26c73ced6bb8219a62 Mon Sep 17 00:00:00 2001 From: Bohdan Oshovskyi Date: Fri, 4 Sep 2026 13:09:34 +0300 Subject: [PATCH 28/32] fix(ios): include the Obj-C shim in the podspec source_files The shim added in 11.0.2 is never compiled under CocoaPods, so 11.0.2 still fails exactly like 11.0.1 (#395, #396): Semantic Issue (Xcode): Unknown receiver 'FlutterForegroundTaskPlugin'; did you mean 'SwiftFlutterForegroundTaskPlugin'? ios/Runner/GeneratedPluginRegistrant.m Both globs in source_files are resolved relative to the podspec's directory (ios/). The Swift glob carries the `flutter_foreground_task/` prefix, but the Obj-C one does not, so CocoaPods looks for ios/Classes/ while the files actually live in ios/flutter_foreground_task/Classes/. The glob matches nothing and the shim is silently dropped from the pod. Add the missing prefix so the shim is compiled. Verified on a CocoaPods (non-SPM) app, clean Pods/ and build/ each time, changing only this glob: before -> Unknown receiver ...; 0 references to FlutterForegroundTaskPlugin.m in the generated Pods.xcodeproj after -> builds and links; 4 references --- ios/flutter_foreground_task.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/flutter_foreground_task.podspec b/ios/flutter_foreground_task.podspec index 0efef775..bda3a883 100644 --- a/ios/flutter_foreground_task.podspec +++ b/ios/flutter_foreground_task.podspec @@ -13,7 +13,7 @@ A new Flutter plugin. s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } - s.source_files = 'flutter_foreground_task/Sources/flutter_foreground_task/**/*.swift', 'Classes/**/*.{h,m}' + s.source_files = 'flutter_foreground_task/Sources/flutter_foreground_task/**/*.swift', 'flutter_foreground_task/Classes/**/*.{h,m}' s.dependency 'Flutter' s.platform = :ios, '12.0' From e6d5bcb77bff1a68342e69515355127a5c1edbf0 Mon Sep 17 00:00:00 2001 From: Bohdan Oshovskyi Date: Fri, 4 Sep 2026 13:22:25 +0300 Subject: [PATCH 29/32] release: 11.0.3 Ship the podspec fix so CocoaPods users are unblocked; 11.0.2 still fails with "Unknown receiver 'FlutterForegroundTaskPlugin'" because the shim it added never reaches the pod. --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 147c4fdb..d1740c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 11.0.3 + +* [**FIX-iOS**] Include the objective-c shim in the podspec source_files so it is compiled under CocoaPods + ## 11.0.2 * [**FIX-iOS**] Add objective-c shim for FlutterForegroundTaskPlugin diff --git a/pubspec.yaml b/pubspec.yaml index e5a6f80f..ecb8f668 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_foreground_task description: This plugin is used to implement a foreground service on the Android platform. -version: 11.0.2 +version: 11.0.3 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: From f2acda8447dbf4e659be2782c010fe3dc6e8b187 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Mon, 7 Sep 2026 09:24:01 +0900 Subject: [PATCH 30/32] release: 11.0.3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3b8eaf7..1fe4f643 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^11.0.2 + flutter_foreground_task: ^11.0.3 ``` After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. From 305c25dc205bba85b959f529631326db3f401ae3 Mon Sep 17 00:00:00 2001 From: JCPistori <91566210+JCPistori@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:16:25 -0300 Subject: [PATCH 31/32] chore: para de versionar example/ios/Flutter/ephemeral Arquivos gerados pelo toolchain do Flutter, commitados por engano no fork. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 3 ++ .../Flutter/ephemeral/flutter_lldb_helper.py | 32 ------------------- .../ios/Flutter/ephemeral/flutter_lldbinit | 5 --- 3 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 example/ios/Flutter/ephemeral/flutter_lldb_helper.py delete mode 100644 example/ios/Flutter/ephemeral/flutter_lldbinit diff --git a/.gitignore b/.gitignore index 9245c580..0747de72 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ /pubspec.lock build/ + +# Arquivos gerados pelo Flutter no example (versionados por engano) +example/ios/Flutter/ephemeral/ diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py deleted file mode 100644 index a88caf99..00000000 --- a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Generated file, do not edit. -# - -import lldb - -def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): - """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" - base = frame.register["x0"].GetValueAsAddress() - page_len = frame.register["x1"].GetValueAsUnsigned() - - # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the - # first page to see if handled it correctly. This makes diagnosing - # misconfiguration (e.g. missing breakpoint) easier. - data = bytearray(page_len) - data[0:8] = b'IHELPED!' - - error = lldb.SBError() - frame.GetThread().GetProcess().WriteMemory(base, data, error) - if not error.Success(): - print(f'Failed to write into {base}[+{page_len}]', error) - return - -def __lldb_init_module(debugger: lldb.SBDebugger, _): - target = debugger.GetDummyTarget() - # Caveat: must use BreakpointCreateByRegEx here and not - # BreakpointCreateByName. For some reasons callback function does not - # get carried over from dummy target for the later. - bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") - bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) - bp.SetAutoContinue(True) - print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit deleted file mode 100644 index e3ba6fbe..00000000 --- a/example/ios/Flutter/ephemeral/flutter_lldbinit +++ /dev/null @@ -1,5 +0,0 @@ -# -# Generated file, do not edit. -# - -command script import --relative-to-command-file flutter_lldb_helper.py From 04ba95d5c3ffc63a4e96ba3278d97c2ac18d3d11 Mon Sep 17 00:00:00 2001 From: JCPistori <91566210+JCPistori@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:44:18 -0300 Subject: [PATCH 32/32] =?UTF-8?q?doc:=20simplifica=C3=A7=C3=A3o=20de=20com?= =?UTF-8?q?ent=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- .../service/ForegroundService.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt index e298d726..cbe727f4 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt @@ -133,18 +133,12 @@ class ForegroundService : Service() { isTimeout = false loadDataFromPreferences() - // [GAUDIUM] Vindo do upstream 9.2.0 (stopWithTask). ATENCAO: quando stopWithTask - // esta setado, este bloco derruba o servico assim que TODAS as activities pausam, - // ou seja, quando o app vai para background -- o oposto do que um servico de - // polling precisa. So fica ativo se o lado Dart passar stopWithTask - // explicitamente; com o default (null) o bloco nunca instala nada. - // Se algum dia passarmos stopWithTask: true, revisar este comportamento antes. + // Com stopWithTask ativo, o serviço é derrubado quando todas as activities pausam. val prefs = getSharedPreferences(PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) if (prefs.contains(PrefsKey.STOP_WITH_TASK) && prefs.getBoolean(PrefsKey.STOP_WITH_TASK, false)) { (application as? Application)?.let { TrackVisibilityUtils.install(it) { - // [GAUDIUM/OEM] upstream chama stopForegroundService() sem argumento; - // aqui precisa ser true para preservar o reattach de OEM. + // Passar true para preservar reattach de fabricante. stopForegroundService(true) } }