From 2dfaa02851a6efadb1f6c5088ce0403b1dbd8adc Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Fri, 3 Jul 2026 16:26:09 +0300 Subject: [PATCH] Add notification progress support --- README.md | 3 ++ .../flutter_foreground_task/PreferencesKey.kt | 3 +- .../models/NotificationContent.kt | 24 ++++++++- .../models/NotificationProgress.kt | 27 ++++++++++ .../service/ForegroundService.kt | 16 +++++- example/lib/main.dart | 5 ++ .../service_already_started_exception.dart | 5 +- .../service_not_initialized_exception.dart | 7 +-- lib/errors/service_timeout_exception.dart | 7 +-- lib/flutter_foreground_task.dart | 6 +++ ...lutter_foreground_task_method_channel.dart | 30 +++++++---- ...er_foreground_task_platform_interface.dart | 30 +++++++---- lib/models/foreground_task_event_action.dart | 14 ++---- lib/models/foreground_task_options.dart | 9 ++-- lib/models/notification_button.dart | 12 +---- lib/models/notification_icon.dart | 11 ++-- lib/models/notification_options.dart | 10 +--- lib/models/notification_progress.dart | 50 +++++++++++++++++++ lib/models/service_options.dart | 17 +++++-- lib/ui/with_foreground_task.dart | 1 + lib/utils/color_extension.dart | 1 + test/dummy/service_dummy_data.dart | 22 +++++--- test/notification_progress_test.dart | 34 +++++++++++++ test/service_api_test.dart | 10 ++-- test/task_handler_test.dart | 41 +++++++-------- test/utility_test.dart | 18 +++---- 26 files changed, 301 insertions(+), 112 deletions(-) create mode 100644 android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationProgress.kt create mode 100644 lib/models/notification_progress.dart create mode 100644 test/notification_progress_test.dart diff --git a/README.md b/README.md index 0bd48a52..17d30128 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,7 @@ void initState() { * `notificationTitle`: The title to display in the notification. * `notificationText`: The text to display in the notification. * `notificationIcon`: The icon to display in the notification. Go to [this page](./documentation/customize_notification_icon.md) to customize. +* `notificationProgress`: The progress bar to display in the notification. Use `NotificationProgress.none()` to hide an existing progress bar. * `notificationButtons`: The buttons to display in the notification. (can add 0~3 buttons) * `notificationInitialRoute`: Initial route to be used when the app is launched via a notification. Works the same as the `launchApp` utility. * `callback`: A top-level function that calls the setTaskHandler function. @@ -387,6 +388,7 @@ Future _startService() async { notificationTitle: 'Foreground Service is running', notificationText: 'Tap to return to the app', notificationIcon: null, + notificationProgress: const NotificationProgress(max: 100, progress: 0), notificationButtons: [ const NotificationButton(id: 'btn_hello', text: 'hello'), ], @@ -443,6 +445,7 @@ class FirstTaskHandler extends TaskHandler { FlutterForegroundTask.updateService( notificationTitle: 'Hello FirstTaskHandler :)', notificationText: timestamp.toString(), + notificationProgress: NotificationProgress(max: 100, progress: _count), ); // Send data to main isolate. 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 af45a7a3..7f9243f3 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 @@ -40,6 +40,7 @@ object PreferencesKey { const val NOTIFICATION_CONTENT_TITLE = "notificationContentTitle" const val NOTIFICATION_CONTENT_TEXT = "notificationContentText" const val NOTIFICATION_CONTENT_ICON = "icon" + const val NOTIFICATION_CONTENT_PROGRESS = "progress" const val NOTIFICATION_CONTENT_BUTTONS = "buttons" const val NOTIFICATION_INITIAL_ROUTE = "initialRoute" @@ -57,4 +58,4 @@ object PreferencesKey { // 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/NotificationContent.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationContent.kt index abb5729f..e0ed2854 100644 --- a/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationContent.kt +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationContent.kt @@ -9,6 +9,7 @@ data class NotificationContent( val title: String, val text: String, val icon: NotificationIcon?, + val progress: NotificationProgress?, val buttons: List, val initialRoute: String? ) { @@ -26,6 +27,12 @@ data class NotificationContent( icon = NotificationIcon.fromJsonString(iconJsonString) } + val progressJsonString = prefs.getString(PrefsKey.NOTIFICATION_CONTENT_PROGRESS, null) + var progress: NotificationProgress? = null + if (progressJsonString != null) { + progress = NotificationProgress.fromJsonString(progressJsonString) + } + val buttonsJsonString = prefs.getString(PrefsKey.NOTIFICATION_CONTENT_BUTTONS, null) val buttons: MutableList = mutableListOf() if (buttonsJsonString != null) { @@ -42,6 +49,7 @@ data class NotificationContent( title = title, text = text, icon = icon, + progress = progress, buttons = buttons, initialRoute = initialRoute ) @@ -60,6 +68,12 @@ data class NotificationContent( iconJsonString = JSONObject(iconJson).toString() } + val progressJson = map?.get(PrefsKey.NOTIFICATION_CONTENT_PROGRESS) as? Map<*, *> + var progressJsonString: String? = null + if (progressJson != null) { + progressJsonString = JSONObject(progressJson).toString() + } + val buttonsJson = map?.get(PrefsKey.NOTIFICATION_CONTENT_BUTTONS) as? List<*> var buttonsJsonString: String? = null if (buttonsJson != null) { @@ -72,6 +86,7 @@ data class NotificationContent( putString(PrefsKey.NOTIFICATION_CONTENT_TITLE, title) putString(PrefsKey.NOTIFICATION_CONTENT_TEXT, text) putString(PrefsKey.NOTIFICATION_CONTENT_ICON, iconJsonString) + putString(PrefsKey.NOTIFICATION_CONTENT_PROGRESS, progressJsonString) putString(PrefsKey.NOTIFICATION_CONTENT_BUTTONS, buttonsJsonString) putString(PrefsKey.NOTIFICATION_INITIAL_ROUTE, initialRoute) commit() @@ -91,6 +106,12 @@ data class NotificationContent( iconJsonString = JSONObject(iconJson).toString() } + val progressJson = map?.get(PrefsKey.NOTIFICATION_CONTENT_PROGRESS) as? Map<*, *> + var progressJsonString: String? = null + if (progressJson != null) { + progressJsonString = JSONObject(progressJson).toString() + } + val buttonsJson = map?.get(PrefsKey.NOTIFICATION_CONTENT_BUTTONS) as? List<*> var buttonsJsonString: String? = null if (buttonsJson != null) { @@ -103,6 +124,7 @@ data class NotificationContent( title?.let { putString(PrefsKey.NOTIFICATION_CONTENT_TITLE, it) } text?.let { putString(PrefsKey.NOTIFICATION_CONTENT_TEXT, it) } iconJsonString?.let { putString(PrefsKey.NOTIFICATION_CONTENT_ICON, it) } + progressJsonString?.let { putString(PrefsKey.NOTIFICATION_CONTENT_PROGRESS, it) } buttonsJsonString?.let { putString(PrefsKey.NOTIFICATION_CONTENT_BUTTONS, it) } initialRoute?.let { putString(PrefsKey.NOTIFICATION_INITIAL_ROUTE, it) } commit() @@ -119,4 +141,4 @@ data class NotificationContent( } } } -} \ No newline at end of file +} diff --git a/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationProgress.kt b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationProgress.kt new file mode 100644 index 00000000..643cba99 --- /dev/null +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationProgress.kt @@ -0,0 +1,27 @@ +package com.pravera.flutter_foreground_task.models + +import org.json.JSONObject + +data class NotificationProgress( + val max: Int, + val progress: Int, + val indeterminate: Boolean, + val show: Boolean +) { + companion object { + fun fromJsonString(jsonString: String): NotificationProgress { + val jsonObj = JSONObject(jsonString) + val max = jsonObj.optInt("max", 0) + val progress = jsonObj.optInt("progress", 0) + val indeterminate = jsonObj.optBoolean("indeterminate", false) + val show = jsonObj.optBoolean("show", max > 0 || indeterminate) + + return NotificationProgress( + max = max.coerceAtLeast(0), + progress = progress.coerceIn(0, max.coerceAtLeast(0)), + indeterminate = indeterminate, + show = show + ) + } + } +} 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 5b4970ee..478b0025 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 @@ -362,6 +362,13 @@ class ForegroundService : Service() { builder.setContentTitle(notificationContent.title) builder.setContentText(notificationContent.text) builder.style = Notification.BigTextStyle() + notificationContent.progress?.let { progress -> + if (progress.show) { + builder.setProgress(progress.max, progress.progress, progress.indeterminate) + } else { + builder.setProgress(0, 0, false) + } + } builder.setVisibility(notificationOptions.visibility) builder.setOnlyAlertOnce(notificationOptions.onlyAlertOnce) if (iconBackgroundColor != null) { @@ -389,6 +396,13 @@ class ForegroundService : Service() { builder.setContentTitle(notificationContent.title) builder.setContentText(notificationContent.text) builder.setStyle(NotificationCompat.BigTextStyle().bigText(notificationContent.text)) + notificationContent.progress?.let { progress -> + if (progress.show) { + builder.setProgress(progress.max, progress.progress, progress.indeterminate) + } else { + builder.setProgress(0, 0, false) + } + } builder.setVisibility(notificationOptions.visibility) builder.setOnlyAlertOnce(notificationOptions.onlyAlertOnce) if (iconBackgroundColor != null) { @@ -613,4 +627,4 @@ class ForegroundService : Service() { return actions } -} \ No newline at end of file +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 2d9ac570..73f103cb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -23,11 +23,15 @@ class MyTaskHandler extends TaskHandler { void _incrementCount() { _count++; + final int progress = _count % 10; // Update notification content. FlutterForegroundTask.updateService( notificationTitle: 'Hello MyTaskHandler :)', notificationText: 'count: $_count', + notificationProgress: progress == 0 + ? const NotificationProgress.none() + : NotificationProgress(max: 10, progress: progress), ); // Send data to main isolate. @@ -176,6 +180,7 @@ class _ExamplePageState extends State { notificationTitle: 'Foreground Service is running', notificationText: 'Tap to return to the app', notificationIcon: null, + notificationProgress: const NotificationProgress(max: 10, progress: 0), notificationButtons: [ const NotificationButton(id: 'btn_hello', text: 'hello'), ], diff --git a/lib/errors/service_already_started_exception.dart b/lib/errors/service_already_started_exception.dart index 852b7545..cb5d5e5b 100644 --- a/lib/errors/service_already_started_exception.dart +++ b/lib/errors/service_already_started_exception.dart @@ -1,6 +1,7 @@ class ServiceAlreadyStartedException implements Exception { - ServiceAlreadyStartedException( - [this.message = 'The service has already started.']); + ServiceAlreadyStartedException([ + this.message = 'The service has already started.', + ]); final String message; diff --git a/lib/errors/service_not_initialized_exception.dart b/lib/errors/service_not_initialized_exception.dart index 5994c0d5..bf84e657 100644 --- a/lib/errors/service_not_initialized_exception.dart +++ b/lib/errors/service_not_initialized_exception.dart @@ -1,7 +1,8 @@ class ServiceNotInitializedException implements Exception { - ServiceNotInitializedException( - [this.message = - 'Not initialized. Please call this function after calling the init function.']); + ServiceNotInitializedException([ + this.message = + 'Not initialized. Please call this function after calling the init function.', + ]); final String message; diff --git a/lib/errors/service_timeout_exception.dart b/lib/errors/service_timeout_exception.dart index dea8a972..3d88484f 100644 --- a/lib/errors/service_timeout_exception.dart +++ b/lib/errors/service_timeout_exception.dart @@ -1,7 +1,8 @@ class ServiceTimeoutException implements Exception { - ServiceTimeoutException( - [this.message = - 'The service request timed out. (ref: https://developer.android.com/guide/components/services#StartingAService)']); + ServiceTimeoutException([ + this.message = + 'The service request timed out. (ref: https://developer.android.com/guide/components/services#StartingAService)', + ]); final String message; diff --git a/lib/flutter_foreground_task.dart b/lib/flutter_foreground_task.dart index 2fedb617..4cc2bbe5 100644 --- a/lib/flutter_foreground_task.dart +++ b/lib/flutter_foreground_task.dart @@ -16,6 +16,7 @@ import 'models/notification_button.dart'; import 'models/notification_icon.dart'; import 'models/notification_options.dart'; import 'models/notification_permission.dart'; +import 'models/notification_progress.dart'; import 'models/service_request_result.dart'; import 'task_handler.dart'; import 'utils/utility.dart'; @@ -33,6 +34,7 @@ export 'models/notification_icon.dart'; export 'models/notification_options.dart'; export 'models/notification_permission.dart'; export 'models/notification_priority.dart'; +export 'models/notification_progress.dart'; export 'models/notification_visibility.dart'; export 'models/service_request_result.dart'; export 'ui/with_foreground_task.dart'; @@ -102,6 +104,7 @@ class FlutterForegroundTask { required String notificationTitle, required String notificationText, NotificationIcon? notificationIcon, + NotificationProgress? notificationProgress, List? notificationButtons, String? notificationInitialRoute, Function? callback, @@ -124,6 +127,7 @@ class FlutterForegroundTask { notificationTitle: notificationTitle, notificationText: notificationText, notificationIcon: notificationIcon, + notificationProgress: notificationProgress, notificationButtons: notificationButtons, notificationInitialRoute: notificationInitialRoute, callback: callback, @@ -160,6 +164,7 @@ class FlutterForegroundTask { String? notificationTitle, String? notificationText, NotificationIcon? notificationIcon, + NotificationProgress? notificationProgress, List? notificationButtons, String? notificationInitialRoute, Function? callback, @@ -174,6 +179,7 @@ class FlutterForegroundTask { notificationText: notificationText, notificationTitle: notificationTitle, notificationIcon: notificationIcon, + notificationProgress: notificationProgress, notificationButtons: notificationButtons, notificationInitialRoute: notificationInitialRoute, callback: callback, diff --git a/lib/flutter_foreground_task_method_channel.dart b/lib/flutter_foreground_task_method_channel.dart index 434888d8..fdf12387 100644 --- a/lib/flutter_foreground_task_method_channel.dart +++ b/lib/flutter_foreground_task_method_channel.dart @@ -14,18 +14,21 @@ import 'models/notification_button.dart'; import 'models/notification_icon.dart'; import 'models/notification_options.dart'; import 'models/notification_permission.dart'; +import 'models/notification_progress.dart'; import 'models/service_options.dart'; import 'task_handler.dart'; /// An implementation of [FlutterForegroundTaskPlatform] that uses method channels. class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { @visibleForTesting - final MethodChannel mMDChannel = - const MethodChannel('flutter_foreground_task/methods'); + final MethodChannel mMDChannel = const MethodChannel( + 'flutter_foreground_task/methods', + ); @visibleForTesting - final MethodChannel mBGChannel = - const MethodChannel('flutter_foreground_task/background'); + final MethodChannel mBGChannel = const MethodChannel( + 'flutter_foreground_task/background', + ); @visibleForTesting Platform platform = const LocalPlatform(); @@ -42,6 +45,7 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { required String notificationTitle, required String notificationText, NotificationIcon? notificationIcon, + NotificationProgress? notificationProgress, List? notificationButtons, String? notificationInitialRoute, Function? callback, @@ -55,6 +59,7 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { notificationContentTitle: notificationTitle, notificationContentText: notificationText, notificationIcon: notificationIcon, + notificationProgress: notificationProgress, notificationButtons: notificationButtons, notificationInitialRoute: notificationInitialRoute, callback: callback, @@ -74,6 +79,7 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { String? notificationTitle, String? notificationText, NotificationIcon? notificationIcon, + NotificationProgress? notificationProgress, List? notificationButtons, String? notificationInitialRoute, Function? callback, @@ -83,6 +89,7 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { notificationContentTitle: notificationTitle, notificationContentText: notificationText, notificationIcon: notificationIcon, + notificationProgress: notificationProgress, notificationButtons: notificationButtons, notificationInitialRoute: notificationInitialRoute, callback: callback, @@ -214,8 +221,9 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { @override Future openIgnoreBatteryOptimizationSettings() async { if (platform.isAndroid) { - return await mMDChannel - .invokeMethod('openIgnoreBatteryOptimizationSettings'); + return await mMDChannel.invokeMethod( + 'openIgnoreBatteryOptimizationSettings', + ); } return true; } @@ -246,15 +254,17 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { @override Future checkNotificationPermission() async { - final int result = - await mMDChannel.invokeMethod('checkNotificationPermission'); + final int result = await mMDChannel.invokeMethod( + 'checkNotificationPermission', + ); return NotificationPermission.fromIndex(result); } @override Future requestNotificationPermission() async { - final int result = - await mMDChannel.invokeMethod('requestNotificationPermission'); + final int result = await mMDChannel.invokeMethod( + 'requestNotificationPermission', + ); return NotificationPermission.fromIndex(result); } diff --git a/lib/flutter_foreground_task_platform_interface.dart b/lib/flutter_foreground_task_platform_interface.dart index 5d3974ff..2fcdf5d2 100644 --- a/lib/flutter_foreground_task_platform_interface.dart +++ b/lib/flutter_foreground_task_platform_interface.dart @@ -6,6 +6,7 @@ import 'models/foreground_task_options.dart'; import 'models/notification_button.dart'; import 'models/notification_icon.dart'; import 'models/notification_options.dart'; +import 'models/notification_progress.dart'; import 'models/notification_permission.dart'; import 'task_handler.dart'; @@ -42,6 +43,7 @@ abstract class FlutterForegroundTaskPlatform extends PlatformInterface { required String notificationTitle, required String notificationText, NotificationIcon? notificationIcon, + NotificationProgress? notificationProgress, List? notificationButtons, String? notificationInitialRoute, Function? callback, @@ -58,6 +60,7 @@ abstract class FlutterForegroundTaskPlatform extends PlatformInterface { String? notificationTitle, String? notificationText, NotificationIcon? notificationIcon, + NotificationProgress? notificationProgress, List? notificationButtons, String? notificationInitialRoute, Function? callback, @@ -99,7 +102,8 @@ abstract class FlutterForegroundTaskPlatform extends PlatformInterface { void setOnLockScreenVisibility(bool isVisible) { throw UnimplementedError( - 'setOnLockScreenVisibility() has not been implemented.'); + 'setOnLockScreenVisibility() has not been implemented.', + ); } Future get isAppOnForeground { @@ -112,17 +116,20 @@ abstract class FlutterForegroundTaskPlatform extends PlatformInterface { Future get isIgnoringBatteryOptimizations { throw UnimplementedError( - 'isIgnoringBatteryOptimizations has not been implemented.'); + 'isIgnoringBatteryOptimizations has not been implemented.', + ); } Future openIgnoreBatteryOptimizationSettings() { throw UnimplementedError( - 'openIgnoreBatteryOptimizationSettings() has not been implemented.'); + 'openIgnoreBatteryOptimizationSettings() has not been implemented.', + ); } Future requestIgnoreBatteryOptimization() { throw UnimplementedError( - 'requestIgnoreBatteryOptimization() has not been implemented.'); + 'requestIgnoreBatteryOptimization() has not been implemented.', + ); } Future get canDrawOverlays { @@ -131,26 +138,31 @@ abstract class FlutterForegroundTaskPlatform extends PlatformInterface { Future openSystemAlertWindowSettings() { throw UnimplementedError( - 'openSystemAlertWindowSettings() has not been implemented.'); + 'openSystemAlertWindowSettings() has not been implemented.', + ); } Future checkNotificationPermission() { throw UnimplementedError( - 'checkNotificationPermission() has not been implemented.'); + 'checkNotificationPermission() has not been implemented.', + ); } Future requestNotificationPermission() { throw UnimplementedError( - 'requestNotificationPermission() has not been implemented.'); + 'requestNotificationPermission() has not been implemented.', + ); } Future get canScheduleExactAlarms { throw UnimplementedError( - 'canScheduleExactAlarms has not been implemented.'); + 'canScheduleExactAlarms has not been implemented.', + ); } Future openAlarmsAndRemindersSettings() { throw UnimplementedError( - 'openAlarmsAndRemindersSettings() has not been implemented.'); + 'openAlarmsAndRemindersSettings() has not been implemented.', + ); } } diff --git a/lib/models/foreground_task_event_action.dart b/lib/models/foreground_task_event_action.dart index 65c7cb02..0b88dfbb 100644 --- a/lib/models/foreground_task_event_action.dart +++ b/lib/models/foreground_task_event_action.dart @@ -1,9 +1,6 @@ /// A class that defines the action of onRepeatEvent in [TaskHandler]. class ForegroundTaskEventAction { - ForegroundTaskEventAction._private({ - required this.type, - this.interval, - }); + ForegroundTaskEventAction._private({required this.type, this.interval}); /// Not use onRepeatEvent callback. factory ForegroundTaskEventAction.nothing() => @@ -16,7 +13,9 @@ class ForegroundTaskEventAction { /// Call onRepeatEvent at milliseconds [interval]. factory ForegroundTaskEventAction.repeat(int interval) => ForegroundTaskEventAction._private( - type: ForegroundTaskEventType.repeat, interval: interval); + type: ForegroundTaskEventType.repeat, + interval: interval, + ); /// The type for [ForegroundTaskEventAction]. final ForegroundTaskEventType type; @@ -26,10 +25,7 @@ class ForegroundTaskEventAction { /// Returns the data fields of [ForegroundTaskEventAction] in JSON format. Map toJson() { - return { - 'taskEventType': type.value, - 'taskEventInterval': interval, - }; + return {'taskEventType': type.value, 'taskEventInterval': interval}; } } diff --git a/lib/models/foreground_task_options.dart b/lib/models/foreground_task_options.dart index 814bb16a..70235748 100644 --- a/lib/models/foreground_task_options.dart +++ b/lib/models/foreground_task_options.dart @@ -71,10 +71,13 @@ class ForegroundTaskOptions { ForegroundTaskOptions( eventAction: eventAction ?? this.eventAction, autoRunOnBoot: autoRunOnBoot ?? this.autoRunOnBoot, - autoRunOnMyPackageReplaced: autoRunOnMyPackageReplaced ?? this.autoRunOnMyPackageReplaced, + autoRunOnMyPackageReplaced: + autoRunOnMyPackageReplaced ?? this.autoRunOnMyPackageReplaced, allowWakeLock: allowWakeLock ?? this.allowWakeLock, allowWifiLock: allowWifiLock ?? this.allowWifiLock, allowAutoRestart: allowAutoRestart ?? this.allowAutoRestart, - stopWithTask: identical(stopWithTask, _unset) ? this.stopWithTask : stopWithTask as bool?, + stopWithTask: identical(stopWithTask, _unset) + ? this.stopWithTask + : stopWithTask as bool?, ); -} \ No newline at end of file +} diff --git a/lib/models/notification_button.dart b/lib/models/notification_button.dart index b857ca2e..ed4d8a62 100644 --- a/lib/models/notification_button.dart +++ b/lib/models/notification_button.dart @@ -23,19 +23,11 @@ class NotificationButton { /// Returns the data fields of [NotificationButton] in JSON format. Map toJson() { - return { - 'id': id, - 'text': text, - 'textColorRgb': textColor?.toRgbString, - }; + return {'id': id, 'text': text, 'textColorRgb': textColor?.toRgbString}; } /// Creates a copy of the object replaced with new values. - NotificationButton copyWith({ - String? id, - String? text, - Color? textColor, - }) => + NotificationButton copyWith({String? id, String? text, Color? textColor}) => NotificationButton( id: id ?? this.id, text: text ?? this.text, diff --git a/lib/models/notification_icon.dart b/lib/models/notification_icon.dart index c688f4e3..f7500bb8 100644 --- a/lib/models/notification_icon.dart +++ b/lib/models/notification_icon.dart @@ -5,10 +5,8 @@ import 'package:flutter_foreground_task/utils/color_extension.dart'; /// A data class for dynamically changing the notification icon. class NotificationIcon { /// Constructs an instance of [NotificationIcon]. - const NotificationIcon({ - required this.metaDataName, - this.backgroundColor, - }) : assert(metaDataName.length > 0); + const NotificationIcon({required this.metaDataName, this.backgroundColor}) + : assert(metaDataName.length > 0); /// The name of the meta-data in the manifest that contains the drawable icon resource identifier. final String metaDataName; @@ -25,10 +23,7 @@ class NotificationIcon { } /// Creates a copy of the object replaced with new values. - NotificationIcon copyWith({ - String? metaDataName, - Color? backgroundColor, - }) => + NotificationIcon copyWith({String? metaDataName, Color? backgroundColor}) => NotificationIcon( metaDataName: metaDataName ?? this.metaDataName, backgroundColor: backgroundColor ?? this.backgroundColor, diff --git a/lib/models/notification_options.dart b/lib/models/notification_options.dart index 56951620..bf3638dc 100644 --- a/lib/models/notification_options.dart +++ b/lib/models/notification_options.dart @@ -144,17 +144,11 @@ class IOSNotificationOptions { /// Returns the data fields of [IOSNotificationOptions] in JSON format. Map toJson() { - return { - 'showNotification': showNotification, - 'playSound': playSound, - }; + return {'showNotification': showNotification, 'playSound': playSound}; } /// Creates a copy of the object replaced with new values. - IOSNotificationOptions copyWith({ - bool? showNotification, - bool? playSound, - }) => + IOSNotificationOptions copyWith({bool? showNotification, bool? playSound}) => IOSNotificationOptions( showNotification: showNotification ?? this.showNotification, playSound: playSound ?? this.playSound, diff --git a/lib/models/notification_progress.dart b/lib/models/notification_progress.dart new file mode 100644 index 00000000..0c81e353 --- /dev/null +++ b/lib/models/notification_progress.dart @@ -0,0 +1,50 @@ +/// Progress bar configuration for the foreground service notification. +class NotificationProgress { + /// Constructs an instance of [NotificationProgress]. + const NotificationProgress({ + required this.max, + required this.progress, + this.indeterminate = false, + }) : assert(max >= 0), + assert(progress >= 0); + + /// Hides the progress bar on the foreground service notification. + const NotificationProgress.none() + : max = 0, + progress = 0, + indeterminate = false; + + /// Maximum progress value. + final int max; + + /// Current progress value. + final int progress; + + /// Whether the progress bar should be indeterminate. + final bool indeterminate; + + /// Whether the progress bar should be displayed. + bool get show => max > 0 || indeterminate; + + /// Returns the data fields of [NotificationProgress] in JSON format. + Map toJson() { + return { + 'max': max, + 'progress': progress.clamp(0, max), + 'indeterminate': indeterminate, + 'show': show, + }; + } + + /// Creates a copy of the object replaced with new values. + NotificationProgress copyWith({ + int? max, + int? progress, + bool? indeterminate, + }) => + NotificationProgress( + max: max ?? this.max, + progress: progress ?? this.progress, + indeterminate: indeterminate ?? this.indeterminate, + ); +} diff --git a/lib/models/service_options.dart b/lib/models/service_options.dart index 4e4f391a..96c77f61 100644 --- a/lib/models/service_options.dart +++ b/lib/models/service_options.dart @@ -7,6 +7,7 @@ import 'foreground_task_options.dart'; import 'notification_button.dart'; import 'notification_icon.dart'; import 'notification_options.dart'; +import 'notification_progress.dart'; class ServiceStartOptions { const ServiceStartOptions({ @@ -18,6 +19,7 @@ class ServiceStartOptions { required this.notificationContentTitle, required this.notificationContentText, this.notificationIcon, + this.notificationProgress, this.notificationButtons, this.notificationInitialRoute, this.callback, @@ -31,6 +33,7 @@ class ServiceStartOptions { final String notificationContentTitle; final String notificationContentText; final NotificationIcon? notificationIcon; + final NotificationProgress? notificationProgress; final List? notificationButtons; final String? notificationInitialRoute; final Function? callback; @@ -43,6 +46,7 @@ class ServiceStartOptions { 'notificationContentTitle': notificationContentTitle, 'notificationContentText': notificationContentText, 'icon': notificationIcon?.toJson(), + 'progress': notificationProgress?.toJson(), 'buttons': notificationButtons?.map((e) => e.toJson()).toList(), 'initialRoute': notificationInitialRoute, }; @@ -54,8 +58,9 @@ class ServiceStartOptions { } if (callback != null) { - json['callbackHandle'] = - PluginUtilities.getCallbackHandle(callback!)?.toRawHandle(); + json['callbackHandle'] = PluginUtilities.getCallbackHandle( + callback!, + )?.toRawHandle(); } return json; @@ -68,6 +73,7 @@ class ServiceUpdateOptions { required this.notificationContentTitle, required this.notificationContentText, this.notificationIcon, + this.notificationProgress, this.notificationButtons, this.notificationInitialRoute, this.callback, @@ -77,6 +83,7 @@ class ServiceUpdateOptions { final String? notificationContentTitle; final String? notificationContentText; final NotificationIcon? notificationIcon; + final NotificationProgress? notificationProgress; final List? notificationButtons; final String? notificationInitialRoute; final Function? callback; @@ -86,6 +93,7 @@ class ServiceUpdateOptions { 'notificationContentTitle': notificationContentTitle, 'notificationContentText': notificationContentText, 'icon': notificationIcon?.toJson(), + 'progress': notificationProgress?.toJson(), 'buttons': notificationButtons?.map((e) => e.toJson()).toList(), 'initialRoute': notificationInitialRoute, }; @@ -95,8 +103,9 @@ class ServiceUpdateOptions { } if (callback != null) { - json['callbackHandle'] = - PluginUtilities.getCallbackHandle(callback!)?.toRawHandle(); + json['callbackHandle'] = PluginUtilities.getCallbackHandle( + callback!, + )?.toRawHandle(); } return json; diff --git a/lib/ui/with_foreground_task.dart b/lib/ui/with_foreground_task.dart index 87a3ff84..dccae316 100644 --- a/lib/ui/with_foreground_task.dart +++ b/lib/ui/with_foreground_task.dart @@ -28,5 +28,6 @@ class _WithForegroundTaskState extends State { @override Widget build(BuildContext context) => + // ignore: deprecated_member_use WillPopScope(onWillPop: _onWillPop, child: widget.child); } diff --git a/lib/utils/color_extension.dart b/lib/utils/color_extension.dart index 412a07cd..228adf4c 100644 --- a/lib/utils/color_extension.dart +++ b/lib/utils/color_extension.dart @@ -1,5 +1,6 @@ import 'dart:ui'; extension ColorExtension on Color { + // ignore: deprecated_member_use String get toRgbString => '$red,$green,$blue'; } diff --git a/test/dummy/service_dummy_data.dart b/test/dummy/service_dummy_data.dart index 9e1e7eeb..9a0c4a47 100644 --- a/test/dummy/service_dummy_data.dart +++ b/test/dummy/service_dummy_data.dart @@ -24,10 +24,7 @@ class ServiceDummyData { ); final IOSNotificationOptions iosNotificationOptions = - const IOSNotificationOptions( - showNotification: false, - playSound: false, - ); + const IOSNotificationOptions(showNotification: false, playSound: false); final ForegroundTaskOptions foregroundTaskOptions = ForegroundTaskOptions( eventAction: ForegroundTaskEventAction.repeat(1000), @@ -48,11 +45,22 @@ class ServiceDummyData { backgroundColor: Colors.orange, ); + final NotificationProgress notificationProgress = const NotificationProgress( + max: 100, + progress: 42, + ); + final List notificationButtons = [ const NotificationButton( - id: 'id_test1', text: 'test1', textColor: Colors.purple), + id: 'id_test1', + text: 'test1', + textColor: Colors.purple, + ), const NotificationButton( - id: 'id_test2', text: 'test2', textColor: Colors.green), + id: 'id_test2', + text: 'test2', + textColor: Colors.green, + ), ]; Map getStartServiceArgs(Platform platform) { @@ -64,6 +72,7 @@ class ServiceDummyData { notificationContentTitle: notificationTitle, notificationContentText: notificationText, notificationIcon: notificationIcon, + notificationProgress: notificationProgress, notificationButtons: notificationButtons, callback: testCallback, ).toJson(platform); @@ -75,6 +84,7 @@ class ServiceDummyData { notificationContentTitle: notificationTitle, notificationContentText: notificationText, notificationIcon: notificationIcon, + notificationProgress: notificationProgress, notificationButtons: notificationButtons, callback: testCallback, ).toJson(platform); diff --git a/test/notification_progress_test.dart b/test/notification_progress_test.dart new file mode 100644 index 00000000..98774b38 --- /dev/null +++ b/test/notification_progress_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('NotificationProgress', () { + test('serializes visible progress', () { + const progress = NotificationProgress(max: 100, progress: 42); + + expect(progress.toJson(), { + 'max': 100, + 'progress': 42, + 'indeterminate': false, + 'show': true, + }); + }); + + test('clamps serialized progress to max', () { + const progress = NotificationProgress(max: 100, progress: 120); + + expect(progress.toJson()['progress'], 100); + }); + + test('serializes none as hidden progress', () { + const progress = NotificationProgress.none(); + + expect(progress.toJson(), { + 'max': 0, + 'progress': 0, + 'indeterminate': false, + 'show': false, + }); + }); + }); +} diff --git a/test/service_api_test.dart b/test/service_api_test.dart index 13a0196a..124152d3 100644 --- a/test/service_api_test.dart +++ b/test/service_api_test.dart @@ -21,8 +21,9 @@ void main() { FlutterForegroundTaskPlatform.instance = platformChannel; FlutterForegroundTask.resetStatic(); - methodCallHandler = - ServiceApiMethodCallHandler(() => platformChannel.platform); + methodCallHandler = ServiceApiMethodCallHandler( + () => platformChannel.platform, + ); // method channel TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -516,6 +517,7 @@ Future _startService(ServiceDummyData dummyData) { notificationTitle: dummyData.notificationTitle, notificationText: dummyData.notificationText, notificationIcon: dummyData.notificationIcon, + notificationProgress: dummyData.notificationProgress, notificationButtons: dummyData.notificationButtons, callback: testCallback, ); @@ -531,6 +533,7 @@ Future _updateService(ServiceDummyData dummyData) { notificationTitle: dummyData.notificationTitle, notificationText: dummyData.notificationText, notificationIcon: dummyData.notificationIcon, + notificationProgress: dummyData.notificationProgress, notificationButtons: dummyData.notificationButtons, callback: testCallback, ); @@ -592,7 +595,8 @@ class ServiceApiMethodCallHandler { final Platform platform = _platformGetter(); if (!ServiceApiMethod.getImplementation(platform).contains(method)) { throw UnimplementedError( - 'Unimplemented method on ${platform.operatingSystem}: $method'); + 'Unimplemented method on ${platform.operatingSystem}: $method', + ); } } diff --git a/test/task_handler_test.dart b/test/task_handler_test.dart index 4b011317..c713a939 100644 --- a/test/task_handler_test.dart +++ b/test/task_handler_test.dart @@ -19,27 +19,27 @@ void main() { // method channel TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - platformChannel.mMDChannel, - (MethodCall methodCall) async { - final String method = methodCall.method; - if (method == 'sendData') { - final dynamic data = methodCall.arguments; - platformChannel.mBGChannel - .invokeMethod(TaskEventMethod.onReceiveData, data); - } - return; - }, - ); + .setMockMethodCallHandler(platformChannel.mMDChannel, ( + MethodCall methodCall, + ) async { + final String method = methodCall.method; + if (method == 'sendData') { + final dynamic data = methodCall.arguments; + platformChannel.mBGChannel.invokeMethod( + TaskEventMethod.onReceiveData, + data, + ); + } + return; + }); // background channel TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - platformChannel.mBGChannel, - (MethodCall methodCall) { - return platformChannel.onBackgroundChannel(methodCall, taskHandler); - }, - ); + .setMockMethodCallHandler(platformChannel.mBGChannel, ( + MethodCall methodCall, + ) { + return platformChannel.onBackgroundChannel(methodCall, taskHandler); + }); }); tearDown(() { @@ -305,8 +305,9 @@ class TestTaskHandler extends TaskHandler { @override void onNotificationButtonPressed(String id) { - log.add(TaskEvent( - method: TaskEventMethod.onNotificationButtonPressed, data: id)); + log.add( + TaskEvent(method: TaskEventMethod.onNotificationButtonPressed, data: id), + ); } @override diff --git a/test/utility_test.dart b/test/utility_test.dart index 6bc668d7..a2bf0206 100644 --- a/test/utility_test.dart +++ b/test/utility_test.dart @@ -17,8 +17,9 @@ void main() { FlutterForegroundTaskPlatform.instance = platformChannel; FlutterForegroundTask.resetStatic(); - methodCallHandler = - UtilityMethodCallHandler(() => platformChannel.platform); + methodCallHandler = UtilityMethodCallHandler( + () => platformChannel.platform, + ); // method channel TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -68,19 +69,13 @@ void main() { FlutterForegroundTask.setOnLockScreenVisibility(true); expect( methodCallHandler.log.last, - isMethodCall( - UtilityMethod.setOnLockScreenVisibility, - arguments: true, - ), + isMethodCall(UtilityMethod.setOnLockScreenVisibility, arguments: true), ); FlutterForegroundTask.setOnLockScreenVisibility(false); expect( methodCallHandler.log.last, - isMethodCall( - UtilityMethod.setOnLockScreenVisibility, - arguments: false, - ), + isMethodCall(UtilityMethod.setOnLockScreenVisibility, arguments: false), ); }); @@ -443,7 +438,8 @@ class UtilityMethodCallHandler { final Platform platform = _platformGetter(); if (!UtilityMethod.getImplementation(platform).contains(method)) { throw UnimplementedError( - 'Unimplemented method on ${platform.operatingSystem}: $method'); + 'Unimplemented method on ${platform.operatingSystem}: $method', + ); } }