From 4c6b52587ca06f01becbcb01e42dde358c081c25 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Sat, 12 Apr 2025 23:46:20 +0900 Subject: [PATCH 1/5] feat: Add isTimeout param to the onDestroy callback --- README.md | 30 ++++++++++++++----- .../service/ForegroundService.kt | 14 +++++++-- .../service/ForegroundTask.kt | 4 +-- .../service/RestartReceiver.kt | 14 ++++++--- example/lib/main.dart | 4 +-- ...lutter_foreground_task_method_channel.dart | 3 +- lib/task_handler.dart | 2 +- test/task_handler_test.dart | 6 ++-- 8 files changed, 53 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 59695150..f49af148 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,23 @@ As mentioned in the Android guidelines, to start a FG service on Android 14+, yo android:exported="false" /> ``` -Check runtime requirements before starting the service. If this requirement is not met, the foreground service cannot be started. - - +> [!CAUTION] +> Check [runtime requirements](https://developer.android.com/about/versions/14/changes/fgs-types-required#system-runtime-checks) before starting the service. If this requirement is not met, the foreground service cannot be started. + +> [!CAUTION] +> Android 15 introduces a new timeout behavior to `dataSync` for apps targeting Android 15 (API level 35) or higher. +> The system permits an app's `dataSync` services to run for a total of 6 hours in a 24-hour period. +> However, if the user brings the app to the foreground, the timer resets and the app has 6 hours available. +> +> There are new restrictions on `BOOT_COMPLETED(autoRunOnBoot)` broadcast receivers launching foreground services. +> `BOOT_COMPLETED` receivers are not allowed to launch the following types of foreground services: +> - [dataSync](https://developer.android.com/develop/background-work/services/fg-service-types#data-sync) +> - [camera](https://developer.android.com/develop/background-work/services/fg-service-types#camera) +> - [mediaPlayback](https://developer.android.com/develop/background-work/services/fg-service-types#media) +> - [phoneCall](https://developer.android.com/develop/background-work/services/fg-service-types#phone-call) +> - [microphone](https://developer.android.com/about/versions/14/changes/fgs-types-required#microphone) +> +> You can find how to test this behavior and more details at this [link](https://developer.android.com/about/versions/15/behavior-changes-15#fgs-hardening). ### :baby_chick: iOS @@ -212,8 +226,8 @@ class MyTaskHandler extends TaskHandler { // Called when the task is destroyed. @override - Future onDestroy(DateTime timestamp) async { - print('onDestroy'); + Future onDestroy(DateTime timestamp, bool isTimeout) async { + print('onDestroy(isTimeout: $isTimeout)'); } // Called when data is sent using `FlutterForegroundTask.sendDataToTask`. @@ -428,7 +442,7 @@ class FirstTaskHandler extends TaskHandler { } @override - Future onDestroy(DateTime timestamp) async { + Future onDestroy(DateTime timestamp, bool isTimeout) async { // some code } } @@ -459,7 +473,7 @@ class SecondTaskHandler extends TaskHandler { } @override - Future onDestroy(DateTime timestamp) async { + Future onDestroy(DateTime timestamp, bool isTimeout) async { // some code } } @@ -554,7 +568,7 @@ class MyTaskHandler extends TaskHandler { } @override - Future onDestroy(DateTime timestamp) async { + Future onDestroy(DateTime timestamp, bool isTimeout) async { _streamSubscription?.cancel(); _streamSubscription = null; } 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 45da1041..8228b8ba 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 @@ -96,6 +96,8 @@ class ForegroundService : Service() { private var wakeLock: PowerManager.WakeLock? = null private var wifiLock: WifiManager.WifiLock? = null + private var isTimeout: Boolean = false + // A broadcast receiver that handles intents that occur in the foreground service. private var broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -125,6 +127,7 @@ class ForegroundService : Service() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + isTimeout = false loadDataFromPreferences() var action = foregroundServiceStatus.action @@ -195,7 +198,8 @@ class ForegroundService : Service() { override fun onDestroy() { super.onDestroy() - destroyForegroundTask() + val isTimeout = this.isTimeout + destroyForegroundTask(isTimeout) stopForegroundService() unregisterBroadcastReceiver() @@ -221,13 +225,17 @@ class ForegroundService : Service() { override fun onTimeout(startId: Int) { super.onTimeout(startId) + isTimeout = true stopForegroundService() + Log.e(TAG, "The service(id: $startId) timed out and was terminated by the system.") } @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) override fun onTimeout(startId: Int, fgsType: Int) { super.onTimeout(startId, fgsType) + isTimeout = true stopForegroundService() + Log.e(TAG, "The service(id: $startId) timed out and was terminated by the system.") } private fun loadDataFromPreferences() { @@ -480,8 +488,8 @@ class ForegroundService : Service() { task?.update(taskEventAction = foregroundTaskOptions.eventAction) } - private fun destroyForegroundTask() { - task?.destroy() + private fun destroyForegroundTask(isTimeout: Boolean = false) { + task?.destroy(isTimeout) task = null } diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTask.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTask.kt index 580b340f..11a5032b 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTask.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTask.kt @@ -152,7 +152,7 @@ class ForegroundTask( } } - fun destroy() { + fun destroy(isTimeout: Boolean) { runIfNotDestroyed { stopRepeatTask() @@ -161,7 +161,7 @@ class ForegroundTask( taskLifecycleListener.onEngineWillDestroy() flutterEngine.destroy() } else { - backgroundChannel.invokeMethod(ACTION_TASK_DESTROY, null) { + backgroundChannel.invokeMethod(ACTION_TASK_DESTROY, isTimeout) { flutterEngine.destroy() } taskLifecycleListener.onTaskDestroy() 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 bfb99b41..618010e5 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 @@ -85,13 +85,19 @@ class RestartReceiver : BroadcastReceiver() { val nIntent = Intent(context, ForegroundService::class.java) ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART) ContextCompat.startForegroundService(context, nIntent) - } catch (e: ForegroundServiceStartNotAllowedException){ + } catch (e: ForegroundServiceStartNotAllowedException) { Log.e(TAG, "Foreground service start not allowed exception: ${e.message}") + } catch (e: Exception) { + Log.e(TAG, e.toString()) } } else { - val nIntent = Intent(context, ForegroundService::class.java) - ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART) - ContextCompat.startForegroundService(context, nIntent) + try { + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART) + ContextCompat.startForegroundService(context, nIntent) + } catch (e: Exception) { + Log.e(TAG, e.toString()) + } } } } diff --git a/example/lib/main.dart b/example/lib/main.dart index b6dbee09..11db4db1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -49,8 +49,8 @@ class MyTaskHandler extends TaskHandler { // Called when the task is destroyed. @override - Future onDestroy(DateTime timestamp) async { - print('onDestroy'); + Future onDestroy(DateTime timestamp, bool isTimeout) async { + print('onDestroy(isTimeout: $isTimeout)'); } // Called when data is sent using `FlutterForegroundTask.sendDataToTask`. diff --git a/lib/flutter_foreground_task_method_channel.dart b/lib/flutter_foreground_task_method_channel.dart index 6a03c752..a13ad292 100644 --- a/lib/flutter_foreground_task_method_channel.dart +++ b/lib/flutter_foreground_task_method_channel.dart @@ -133,7 +133,8 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { handler.onRepeatEvent(timestamp); break; case 'onDestroy': - await handler.onDestroy(timestamp); + final bool isTimeout = call.arguments ?? false; + await handler.onDestroy(timestamp, isTimeout); break; case 'onReceiveData': dynamic data = call.arguments; diff --git a/lib/task_handler.dart b/lib/task_handler.dart index 010fab72..f99015bf 100644 --- a/lib/task_handler.dart +++ b/lib/task_handler.dart @@ -13,7 +13,7 @@ abstract class TaskHandler { void onRepeatEvent(DateTime timestamp); /// Called when the task is destroyed. - Future onDestroy(DateTime timestamp); + Future onDestroy(DateTime timestamp, bool isTimeout); /// Called when data is sent using [FlutterForegroundTask.sendDataToTask]. void onReceiveData(Object data) {} diff --git a/test/task_handler_test.dart b/test/task_handler_test.dart index 3b9f8c1d..4b011317 100644 --- a/test/task_handler_test.dart +++ b/test/task_handler_test.dart @@ -69,7 +69,7 @@ void main() { const String method = TaskEventMethod.onDestroy; await platformChannel.mBGChannel.invokeMethod(method); - expect(taskHandler.log.last, isTaskEvent(method)); + expect(taskHandler.log.last, isTaskEvent(method, false)); }); test('onReceiveData', () async { @@ -294,8 +294,8 @@ class TestTaskHandler extends TaskHandler { } @override - Future onDestroy(DateTime timestamp) async { - log.add(const TaskEvent(method: TaskEventMethod.onDestroy)); + Future onDestroy(DateTime timestamp, bool isTimeout) async { + log.add(TaskEvent(method: TaskEventMethod.onDestroy, data: isTimeout)); } @override From a1818433ae1d1beca9896b69277fed51c3aee736 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Sun, 13 Apr 2025 10:54:45 +0900 Subject: [PATCH 2/5] fix: Fix null object error #332 --- .../flutter_foreground_task/service/ForegroundService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8228b8ba..485f33cb 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 @@ -61,7 +61,7 @@ class ForegroundService : Service() { try { // Check if the given intent is a LaunchIntent. val isLaunchIntent = (intent.action == Intent.ACTION_MAIN) && - intent.categories.contains(Intent.CATEGORY_LAUNCHER) + (intent.categories?.contains(Intent.CATEGORY_LAUNCHER) == true) if (!isLaunchIntent) { // Log.d(TAG, "not LaunchIntent") return From 81e0258b5ca60dd764d9f31889c02f557c985896 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Sun, 13 Apr 2025 13:00:54 +0900 Subject: [PATCH 3/5] fix: Fix "Reply already submitted" error #330 --- .../MethodCallHandlerImpl.kt | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/MethodCallHandlerImpl.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/MethodCallHandlerImpl.kt index 87cfdbec..f8e8efd8 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/MethodCallHandlerImpl.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/MethodCallHandlerImpl.kt @@ -15,6 +15,7 @@ import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry +import java.util.UUID import kotlin.Exception /** MethodCallHandlerImpl */ @@ -25,7 +26,8 @@ class MethodCallHandlerImpl(private val context: Context, private val provider: private lateinit var channel: MethodChannel private var activity: Activity? = null - private var resultCallbacks: MutableMap = mutableMapOf() + private var methodCodes: MutableMap = mutableMapOf() + private var methodResults: MutableMap = mutableMapOf() override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { val args = call.arguments @@ -109,17 +111,19 @@ class MethodCallHandlerImpl(private val context: Context, private val provider: "openIgnoreBatteryOptimizationSettings" -> { checkActivityNull().let { - val reqCode = RequestCode.OPEN_IGNORE_BATTERY_OPTIMIZATION_SETTINGS - resultCallbacks[reqCode] = result - PluginUtils.openIgnoreBatteryOptimizationSettings(it, reqCode) + val requestCode = UUID.randomUUID().hashCode() and 0xFFFF + methodCodes[requestCode] = RequestCode.OPEN_IGNORE_BATTERY_OPTIMIZATION_SETTINGS + methodResults[requestCode] = result + PluginUtils.openIgnoreBatteryOptimizationSettings(it, requestCode) } } "requestIgnoreBatteryOptimization" -> { checkActivityNull().let { - val reqCode = RequestCode.REQUEST_IGNORE_BATTERY_OPTIMIZATION - resultCallbacks[reqCode] = result - PluginUtils.requestIgnoreBatteryOptimization(it, reqCode) + val requestCode = UUID.randomUUID().hashCode() and 0xFFFF + methodCodes[requestCode] = RequestCode.REQUEST_IGNORE_BATTERY_OPTIMIZATION + methodResults[requestCode] = result + PluginUtils.requestIgnoreBatteryOptimization(it, requestCode) } } @@ -127,9 +131,10 @@ class MethodCallHandlerImpl(private val context: Context, private val provider: "openSystemAlertWindowSettings" -> { checkActivityNull().let { - val reqCode = RequestCode.OPEN_SYSTEM_ALERT_WINDOW_SETTINGS - resultCallbacks[reqCode] = result - PluginUtils.openSystemAlertWindowSettings(it, reqCode) + val requestCode = UUID.randomUUID().hashCode() and 0xFFFF + methodCodes[requestCode] = RequestCode.OPEN_SYSTEM_ALERT_WINDOW_SETTINGS + methodResults[requestCode] = result + PluginUtils.openSystemAlertWindowSettings(it, requestCode) } } @@ -138,9 +143,10 @@ class MethodCallHandlerImpl(private val context: Context, private val provider: "openAlarmsAndRemindersSettings" -> { checkActivityNull().let { - val reqCode = RequestCode.OPEN_ALARMS_AND_REMINDER_SETTINGS - resultCallbacks[reqCode] = result - PluginUtils.openAlarmsAndRemindersSettings(it, reqCode) + val requestCode = UUID.randomUUID().hashCode() and 0xFFFF + methodCodes[requestCode] = RequestCode.OPEN_ALARMS_AND_REMINDER_SETTINGS + methodResults[requestCode] = result + PluginUtils.openAlarmsAndRemindersSettings(it, requestCode) } } @@ -152,19 +158,25 @@ class MethodCallHandlerImpl(private val context: Context, private val provider: } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - val resultCallback = resultCallbacks[requestCode] ?: return true + val methodCode = methodCodes[requestCode] + val methodResult = methodResults[requestCode] + methodCodes.remove(requestCode) + methodResults.remove(requestCode) - when (requestCode) { + if (methodCode == null || methodResult == null) { + return true + } + + when (methodCode) { RequestCode.OPEN_IGNORE_BATTERY_OPTIMIZATION_SETTINGS -> - resultCallback.success(PluginUtils.isIgnoringBatteryOptimizations(context)) + methodResult.success(PluginUtils.isIgnoringBatteryOptimizations(context)) RequestCode.REQUEST_IGNORE_BATTERY_OPTIMIZATION -> - resultCallback.success(PluginUtils.isIgnoringBatteryOptimizations(context)) + methodResult.success(PluginUtils.isIgnoringBatteryOptimizations(context)) RequestCode.OPEN_SYSTEM_ALERT_WINDOW_SETTINGS -> - resultCallback.success(PluginUtils.canDrawOverlays(context)) + methodResult.success(PluginUtils.canDrawOverlays(context)) RequestCode.OPEN_ALARMS_AND_REMINDER_SETTINGS -> - resultCallback.success(PluginUtils.canScheduleExactAlarms(context)) + methodResult.success(PluginUtils.canScheduleExactAlarms(context)) } - return true } From f960ee2bb22995cdf81a400110859ce78cb3cf50 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Sun, 13 Apr 2025 15:30:48 +0900 Subject: [PATCH 4/5] fix: Prevent crash by catching exceptions during foreground service start #318 #320 #331 --- .../service/ForegroundService.kt | 78 +++++++++---------- .../service/RebootReceiver.kt | 30 ++++++- .../service/RestartReceiver.kt | 4 +- 3 files changed, 63 insertions(+), 49 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 485f33cb..b61734ac 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 @@ -62,10 +62,7 @@ class ForegroundService : Service() { // Check if the given intent is a LaunchIntent. val isLaunchIntent = (intent.action == Intent.ACTION_MAIN) && (intent.categories?.contains(Intent.CATEGORY_LAUNCHER) == true) - if (!isLaunchIntent) { - // Log.d(TAG, "not LaunchIntent") - return - } + if (!isLaunchIntent) return val data = intent.getStringExtra(INTENT_DATA_NAME) if (data == ACTION_NOTIFICATION_PRESSED) { @@ -134,55 +131,47 @@ class ForegroundService : Service() { val isSetStopWithTaskFlag = ForegroundServiceUtils.isSetStopWithTaskFlag(this) if (action == ForegroundServiceAction.API_STOP) { - RestartReceiver.cancelRestartAlarm(this) stopForegroundService() return START_NOT_STICKY } - if (intent == null) { - ForegroundServiceStatus.setData(this, ForegroundServiceAction.RESTART) - foregroundServiceStatus = ForegroundServiceStatus.getData(this) - action = foregroundServiceStatus.action - } - - when (action) { - ForegroundServiceAction.API_START, - ForegroundServiceAction.API_RESTART -> { - startForegroundService() - createForegroundTask() + try { + if (intent == null) { + ForegroundServiceStatus.setData(this, ForegroundServiceAction.RESTART) + foregroundServiceStatus = ForegroundServiceStatus.getData(this) + action = foregroundServiceStatus.action } - ForegroundServiceAction.API_UPDATE -> { - updateNotification() - val prevCallbackHandle = prevForegroundTaskData?.callbackHandle - val currCallbackHandle = foregroundTaskData.callbackHandle - if (prevCallbackHandle != currCallbackHandle) { + + when (action) { + ForegroundServiceAction.API_START, + ForegroundServiceAction.API_RESTART -> { + startForegroundService() createForegroundTask() - } else { - val prevEventAction = prevForegroundTaskOptions?.eventAction - val currEventAction = foregroundTaskOptions.eventAction - if (prevEventAction != currEventAction) { - updateForegroundTask() - } } - } - ForegroundServiceAction.REBOOT, - ForegroundServiceAction.RESTART -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - try { - startForegroundService() + ForegroundServiceAction.API_UPDATE -> { + updateNotification() + val prevCallbackHandle = prevForegroundTaskData?.callbackHandle + val currCallbackHandle = foregroundTaskData.callbackHandle + if (prevCallbackHandle != currCallbackHandle) { createForegroundTask() - } catch (e: ForegroundServiceStartNotAllowedException) { - Log.e(TAG, - "Cannot run service as foreground: $e for notification channel ") - RestartReceiver.cancelRestartAlarm(this) - stopForegroundService() + } else { + val prevEventAction = prevForegroundTaskOptions?.eventAction + val currEventAction = foregroundTaskOptions.eventAction + if (prevEventAction != currEventAction) { + updateForegroundTask() + } } - } else { + } + ForegroundServiceAction.REBOOT, + ForegroundServiceAction.RESTART -> { startForegroundService() createForegroundTask() + Log.d(TAG, "The service has been restarted by Android OS.") } - Log.d(TAG, "The service has been restarted by Android OS.") } + } catch (e: Exception) { + Log.e(TAG, e.message, e) + stopForegroundService() } return if (isSetStopWithTaskFlag) { @@ -207,9 +196,8 @@ class ForegroundService : Service() { if (::foregroundServiceStatus.isInitialized) { isCorrectlyStopped = foregroundServiceStatus.isCorrectlyStopped() } - val isSetStopWithTaskFlag = ForegroundServiceUtils.isSetStopWithTaskFlag(this) - if (!isCorrectlyStopped && !isSetStopWithTaskFlag) { - Log.e(TAG, "The service was terminated due to an unexpected problem. The service will restart after 5 seconds.") + if (!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) } } @@ -281,6 +269,8 @@ class ForegroundService : Service() { @SuppressLint("WrongConstant", "SuspiciousIndentation") private fun startForegroundService() { + RestartReceiver.cancelRestartAlarm(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannel() } @@ -304,6 +294,8 @@ class ForegroundService : Service() { } private fun stopForegroundService() { + RestartReceiver.cancelRestartAlarm(this) + releaseLockMode() stopForeground(true) stopSelf() diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RebootReceiver.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RebootReceiver.kt index 715fd103..013ac9a7 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RebootReceiver.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RebootReceiver.kt @@ -1,8 +1,11 @@ package com.pravera.flutter_foreground_task.service +import android.app.ForegroundServiceStartNotAllowedException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Build +import android.util.Log import androidx.core.content.ContextCompat import com.pravera.flutter_foreground_task.models.ForegroundServiceAction import com.pravera.flutter_foreground_task.models.ForegroundServiceStatus @@ -16,6 +19,10 @@ import com.pravera.flutter_foreground_task.utils.ForegroundServiceUtils * @version 1.0 */ class RebootReceiver : BroadcastReceiver() { + companion object { + private val TAG = RebootReceiver::class.java.simpleName + } + override fun onReceive(context: Context?, intent: Intent?) { if (context == null || intent == null) return @@ -45,9 +52,24 @@ class RebootReceiver : BroadcastReceiver() { } private fun startForegroundService(context: Context) { - // Create an intent for calling the service and store the action to be executed - val nIntent = Intent(context, ForegroundService::class.java) - ForegroundServiceStatus.setData(context, ForegroundServiceAction.REBOOT) - ContextCompat.startForegroundService(context, nIntent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.REBOOT) + ContextCompat.startForegroundService(context, nIntent) + } catch (e: ForegroundServiceStartNotAllowedException) { + Log.e(TAG, "Foreground service start not allowed exception: ${e.message}") + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } else { + try { + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.REBOOT) + ContextCompat.startForegroundService(context, nIntent) + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } } } 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 618010e5..dbf12498 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 @@ -88,7 +88,7 @@ class RestartReceiver : BroadcastReceiver() { } catch (e: ForegroundServiceStartNotAllowedException) { Log.e(TAG, "Foreground service start not allowed exception: ${e.message}") } catch (e: Exception) { - Log.e(TAG, e.toString()) + Log.e(TAG, e.message, e) } } else { try { @@ -96,7 +96,7 @@ class RestartReceiver : BroadcastReceiver() { ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART) ContextCompat.startForegroundService(context, nIntent) } catch (e: Exception) { - Log.e(TAG, e.toString()) + Log.e(TAG, e.message, e) } } } From d42ff0d2b35d707aeae36318ad7978200a716a00 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Sun, 13 Apr 2025 17:00:29 +0900 Subject: [PATCH 5/5] release: 9.0.0 --- CHANGELOG.md | 10 ++++ README.md | 9 +++- documentation/migration_documentation.md | 63 ++++++++++++++++++++++++ pubspec.yaml | 2 +- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e52f420f..378dab46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 9.0.0 + +* [**CHORE**] Bump minimum supported SDK version to `Flutter 3.22/Dart 3.4` +* [**CHORE**] Bump `kotlin_version(1.7.10 -> 1.9.10)`, `gradle(7.3.0 -> 8.6.0)` for Android 15 +* [**FEAT**] Add `isTimeout` param to the onDestroy callback +* [**FIX**] Fix "null object" error [#332](https://github.com/Dev-hwang/flutter_foreground_task/issues/332) +* [**FIX**] Fix "Reply already submitted" error [#330](https://github.com/Dev-hwang/flutter_foreground_task/issues/330) +* [**FIX**] Prevent crash by catching exceptions during foreground service start +* Check [migration_documentation](./documentation/migration_documentation.md) for changes + ## 8.17.0 * [**FEAT**] Allow `onNotificationPressed` to trigger without `SYSTEM_ALERT_WINDOW` permission diff --git a/README.md b/README.md index f49af148..be5ebc18 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,20 @@ To use this plugin, add `flutter_foreground_task` as a [dependency in your pubsp ```yaml dependencies: - flutter_foreground_task: ^8.17.0 + flutter_foreground_task: ^9.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. ### :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. + +- [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) +- [app/build.gradle](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/example/android/app/build.gradle) +- [migration_documentation](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/documentation/migration_documentation.md) + Open the `AndroidManifest.xml` file and declare the service tag inside the `` tag as follows. If you want the foreground service to run only when the app is running, add `android:stopWithTask="true"`. diff --git a/documentation/migration_documentation.md b/documentation/migration_documentation.md index 2514252f..fc11b470 100644 --- a/documentation/migration_documentation.md +++ b/documentation/migration_documentation.md @@ -1,5 +1,68 @@ ## Migration +### ver 9.0.0 + +- chore: Bump minimum supported SDK version to `Flutter 3.22/Dart 3.4`. + +``` +environment: + // sdk: ">=3.0.0 <4.0.0" + // flutter: ">=3.10.0" + sdk: ^3.4.0 + flutter: ">=3.22.0" +``` + +- chore: Bump `kotlin_version(1.7.10 -> 1.9.10)`, `gradle(7.3.0 -> 8.6.0)` for Android 15. + +``` +[android/settings.gradle] +plugins { + // id "com.android.application" version "7.3.0" apply false + // id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.10" apply false +} + +[android/gradle/wrapper/gradle-wrapper.properties] +// distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip + +[android/app/build.gradle] +android { + // compileSdk 34 + compileSdk 35 + + compileOptions { + // sourceCompatibility JavaVersion.VERSION_1_8 + // targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + // jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_11 + } + + defaultConfig { + // targetSdkVersion 34 + targetSdkVersion 35 + } +} +``` + +- feat: Add `isTimeout` param to the onDestroy callback. + +```dart +// from +@override +Future onDestroy(DateTime timestamp) async {} + +// to +@override +Future onDestroy(DateTime timestamp, bool isTimeout) async {} +``` + ### ver 8.16.0 - Change `ServiceRequestResult` class to `sealed class` for improved code readability. diff --git a/pubspec.yaml b/pubspec.yaml index 2b30100f..38591c0b 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: 8.17.0 +version: 9.0.0 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: