From b141694121ab1a96dfeef0f70461057a2c4d18b3 Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Mon, 14 Apr 2025 22:08:09 +0900 Subject: [PATCH 1/2] feat: Support manual foregroundServiceType via serviceTypes in startService #328 --- .../flutter_foreground_task/PreferencesKey.kt | 4 + .../models/ForegroundServiceTypes.kt | 77 +++++++++++++++++++ .../service/ForegroundService.kt | 31 ++------ .../service/ForegroundServiceManager.kt | 3 + lib/flutter_foreground_task.dart | 4 + ...lutter_foreground_task_method_channel.dart | 3 + ...er_foreground_task_platform_interface.dart | 2 + lib/models/foreground_service_types.dart | 54 +++++++++++++ lib/models/service_options.dart | 4 + 9 files changed, 158 insertions(+), 24 deletions(-) create mode 100644 android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt create mode 100644 lib/models/foreground_service_types.dart 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 729b6ea6..c399100e 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 @@ -16,6 +16,10 @@ object PreferencesKey { const val FOREGROUND_SERVICE_STATUS_PREFS = prefix + "FOREGROUND_SERVICE_STATUS" const val FOREGROUND_SERVICE_ACTION = "foregroundServiceAction" + // service types + const val FOREGROUND_SERVICE_TYPES_PREFS = prefix + "FOREGROUND_SERVICE_TYPES" + const val FOREGROUND_SERVICE_TYPES = "serviceTypes" + // notification options const val NOTIFICATION_OPTIONS_PREFS = prefix + "NOTIFICATION_OPTIONS" const val SERVICE_ID = "serviceId" 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 new file mode 100644 index 00000000..315bca08 --- /dev/null +++ b/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt @@ -0,0 +1,77 @@ +package com.pravera.flutter_foreground_task.models + +import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build +import com.pravera.flutter_foreground_task.PreferencesKey + +data class ForegroundServiceTypes(val value: Int) { + companion object { + fun getData(context: Context): ForegroundServiceTypes { + val prefs = context.getSharedPreferences( + PreferencesKey.FOREGROUND_SERVICE_TYPES_PREFS, Context.MODE_PRIVATE) + + val value = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + prefs.getInt(PreferencesKey.FOREGROUND_SERVICE_TYPES, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) + } else { + prefs.getInt(PreferencesKey.FOREGROUND_SERVICE_TYPES, 0) // none + } + + return ForegroundServiceTypes(value = value) + } + + fun setData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PreferencesKey.FOREGROUND_SERVICE_TYPES_PREFS, Context.MODE_PRIVATE) + + var value = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val serviceTypes = map?.get(PreferencesKey.FOREGROUND_SERVICE_TYPES) as? List<*> + if (serviceTypes != null) { + for (serviceType in serviceTypes) { + getForegroundServiceTypeFlag(serviceType)?.let { + value = value or it + } + } + } + } + + // not none type + if (value > 0) { + with(prefs.edit()) { + putInt(PreferencesKey.FOREGROUND_SERVICE_TYPES, value) + commit() + } + } + } + + fun clearData(context: Context) { + val prefs = context.getSharedPreferences( + PreferencesKey.FOREGROUND_SERVICE_TYPES_PREFS, Context.MODE_PRIVATE) + + with(prefs.edit()) { + clear() + commit() + } + } + + private fun getForegroundServiceTypeFlag(type: Any?): Int? { + return when (type) { + 0 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA else null + 1 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE else null + 2 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else null + 3 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH else null + 4 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION else null + 5 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK else null + 6 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION else null + 7 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE else null + 8 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL else null + 9 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING else null + 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 + else -> 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 b61734ac..f225ba20 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 @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.app.* import android.content.* import android.content.pm.PackageManager -import android.content.pm.ServiceInfo import android.graphics.Color import android.net.wifi.WifiManager import android.os.* @@ -19,11 +18,9 @@ import com.pravera.flutter_foreground_task.FlutterForegroundTaskLifecycleListene import com.pravera.flutter_foreground_task.RequestCode import com.pravera.flutter_foreground_task.models.* import com.pravera.flutter_foreground_task.utils.ForegroundServiceUtils -import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import java.util.* /** * A service class for implementing foreground service. @@ -81,6 +78,7 @@ class ForegroundService : Service() { } private lateinit var foregroundServiceStatus: ForegroundServiceStatus + private lateinit var foregroundServiceTypes: ForegroundServiceTypes private lateinit var foregroundTaskOptions: ForegroundTaskOptions private lateinit var foregroundTaskData: ForegroundTaskData private lateinit var notificationOptions: NotificationOptions @@ -228,25 +226,14 @@ class ForegroundService : Service() { private fun loadDataFromPreferences() { foregroundServiceStatus = ForegroundServiceStatus.getData(applicationContext) - - if (::foregroundTaskOptions.isInitialized) { - prevForegroundTaskOptions = foregroundTaskOptions - } + foregroundServiceTypes = ForegroundServiceTypes.getData(applicationContext) + if (::foregroundTaskOptions.isInitialized) { prevForegroundTaskOptions = foregroundTaskOptions } foregroundTaskOptions = ForegroundTaskOptions.getData(applicationContext) - - if (::foregroundTaskData.isInitialized) { - prevForegroundTaskData = foregroundTaskData - } + if (::foregroundTaskData.isInitialized) { prevForegroundTaskData = foregroundTaskData } foregroundTaskData = ForegroundTaskData.getData(applicationContext) - - if (::notificationOptions.isInitialized) { - prevNotificationOptions = notificationOptions - } + if (::notificationOptions.isInitialized) { prevNotificationOptions = notificationOptions } notificationOptions = NotificationOptions.getData(applicationContext) - - if (::notificationContent.isInitialized) { - prevNotificationContent = notificationContent - } + if (::notificationContent.isInitialized) { prevNotificationContent = notificationContent } notificationContent = NotificationContent.getData(applicationContext) } @@ -278,11 +265,7 @@ class ForegroundService : Service() { val serviceId = notificationOptions.serviceId val notification = createNotification() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground( - serviceId, - notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST - ) + startForeground(serviceId, notification, foregroundServiceTypes.value) } else { startForeground(serviceId, notification) } 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 4db8fa2c..18ad0df1 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 @@ -7,6 +7,7 @@ import com.pravera.flutter_foreground_task.errors.ServiceAlreadyStartedException import com.pravera.flutter_foreground_task.errors.ServiceNotStartedException import com.pravera.flutter_foreground_task.models.ForegroundServiceAction import com.pravera.flutter_foreground_task.models.ForegroundServiceStatus +import com.pravera.flutter_foreground_task.models.ForegroundServiceTypes import com.pravera.flutter_foreground_task.models.ForegroundTaskData import com.pravera.flutter_foreground_task.models.ForegroundTaskOptions import com.pravera.flutter_foreground_task.models.NotificationContent @@ -28,6 +29,7 @@ class ForegroundServiceManager { val nIntent = Intent(context, ForegroundService::class.java) val argsMap = arguments as? Map<*, *> ForegroundServiceStatus.setData(context, ForegroundServiceAction.API_START) + ForegroundServiceTypes.setData(context, argsMap) NotificationOptions.setData(context, argsMap) ForegroundTaskOptions.setData(context, argsMap) ForegroundTaskData.setData(context, argsMap) @@ -69,6 +71,7 @@ class ForegroundServiceManager { val nIntent = Intent(context, ForegroundService::class.java) ForegroundServiceStatus.setData(context, ForegroundServiceAction.API_STOP) + ForegroundServiceTypes.clearData(context) NotificationOptions.clearData(context) ForegroundTaskOptions.clearData(context) ForegroundTaskData.clearData(context) diff --git a/lib/flutter_foreground_task.dart b/lib/flutter_foreground_task.dart index bc4e77e5..2fedb617 100644 --- a/lib/flutter_foreground_task.dart +++ b/lib/flutter_foreground_task.dart @@ -10,6 +10,7 @@ import 'errors/service_already_started_exception.dart'; import 'errors/service_not_initialized_exception.dart'; import 'errors/service_not_started_exception.dart'; import 'errors/service_timeout_exception.dart'; +import 'models/foreground_service_types.dart'; import 'models/foreground_task_options.dart'; import 'models/notification_button.dart'; import 'models/notification_icon.dart'; @@ -23,6 +24,7 @@ export 'errors/service_already_started_exception.dart'; export 'errors/service_not_initialized_exception.dart'; export 'errors/service_not_started_exception.dart'; export 'errors/service_timeout_exception.dart'; +export 'models/foreground_service_types.dart'; export 'models/foreground_task_event_action.dart'; export 'models/foreground_task_options.dart'; export 'models/notification_button.dart'; @@ -96,6 +98,7 @@ class FlutterForegroundTask { /// Start the foreground service. static Future startService({ int? serviceId, + List? serviceTypes, required String notificationTitle, required String notificationText, NotificationIcon? notificationIcon, @@ -117,6 +120,7 @@ class FlutterForegroundTask { iosNotificationOptions: iosNotificationOptions!, foregroundTaskOptions: foregroundTaskOptions!, serviceId: serviceId, + serviceTypes: serviceTypes, notificationTitle: notificationTitle, notificationText: notificationText, notificationIcon: notificationIcon, diff --git a/lib/flutter_foreground_task_method_channel.dart b/lib/flutter_foreground_task_method_channel.dart index a13ad292..434888d8 100644 --- a/lib/flutter_foreground_task_method_channel.dart +++ b/lib/flutter_foreground_task_method_channel.dart @@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; import 'package:platform/platform.dart'; import 'flutter_foreground_task_platform_interface.dart'; +import 'models/foreground_service_types.dart'; import 'models/foreground_task_options.dart'; import 'models/notification_button.dart'; import 'models/notification_icon.dart'; @@ -37,6 +38,7 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { required IOSNotificationOptions iosNotificationOptions, required ForegroundTaskOptions foregroundTaskOptions, int? serviceId, + List? serviceTypes, required String notificationTitle, required String notificationText, NotificationIcon? notificationIcon, @@ -46,6 +48,7 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { }) async { final Map optionsJson = ServiceStartOptions( serviceId: serviceId, + serviceTypes: serviceTypes, androidNotificationOptions: androidNotificationOptions, iosNotificationOptions: iosNotificationOptions, foregroundTaskOptions: foregroundTaskOptions, diff --git a/lib/flutter_foreground_task_platform_interface.dart b/lib/flutter_foreground_task_platform_interface.dart index a8d402b8..5d3974ff 100644 --- a/lib/flutter_foreground_task_platform_interface.dart +++ b/lib/flutter_foreground_task_platform_interface.dart @@ -1,6 +1,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'flutter_foreground_task_method_channel.dart'; +import 'models/foreground_service_types.dart'; import 'models/foreground_task_options.dart'; import 'models/notification_button.dart'; import 'models/notification_icon.dart'; @@ -37,6 +38,7 @@ abstract class FlutterForegroundTaskPlatform extends PlatformInterface { required IOSNotificationOptions iosNotificationOptions, required ForegroundTaskOptions foregroundTaskOptions, int? serviceId, + List? serviceTypes, required String notificationTitle, required String notificationText, NotificationIcon? notificationIcon, diff --git a/lib/models/foreground_service_types.dart b/lib/models/foreground_service_types.dart new file mode 100644 index 00000000..97327dbd --- /dev/null +++ b/lib/models/foreground_service_types.dart @@ -0,0 +1,54 @@ +/// https://developer.android.com/about/versions/14/changes/fgs-types-required#system-exempted +class ForegroundServiceTypes { + /// Constructs an instance of [ForegroundServiceTypes]. + const ForegroundServiceTypes(this.rawValue); + + /// Continue to access the camera from the background, such as video chat apps that allow for multitasking. + static const camera = ForegroundServiceTypes(0); + + /// Interactions with external devices that require a Bluetooth, NFC, IR, USB, or network connection. + static const connectedDevice = ForegroundServiceTypes(1); + + /// Data transfer operations, such as the following: + /// + /// * Data upload or download + /// * Backup-and-restore operations + /// * Import or export operations + /// * Fetch data + /// * Local file processing + /// * Transfer data between a device and the cloud over a network + static const dataSync = ForegroundServiceTypes(2); + + /// Any long-running use cases to support apps in the fitness category such as exercise trackers. + static const health = ForegroundServiceTypes(3); + + /// Long-running use cases that require location access, such as navigation and location sharing. + static const location = ForegroundServiceTypes(4); + + /// Continue audio or video playback from the background. Support Digital Video Recording (DVR) functionality on Android TV. + static const mediaPlayback = ForegroundServiceTypes(5); + + /// Project content to non-primary display or external device using the MediaProjection APIs. This content doesn't have to be exclusively media content. + static const mediaProjection = ForegroundServiceTypes(6); + + /// Continue microphone capture from the background, such as voice recorders or communication apps. + static const microphone = ForegroundServiceTypes(7); + + /// Continue an ongoing call using the ConnectionService APIs. + static const phoneCall = ForegroundServiceTypes(8); + + /// Transfer text messages from one device to another. Assists with continuity of a user's messaging tasks when they switch devices. + static const remoteMessaging = ForegroundServiceTypes(9); + + /// Quickly finish critical work that cannot be interrupted or postponed. + static const shortService = ForegroundServiceTypes(10); + + /// Covers any valid foreground service use cases that aren't covered by the other foreground service types. + static const specialUse = ForegroundServiceTypes(11); + + /// Reserved for system applications and specific system integrations, to continue to use foreground services. + static const systemExempted = ForegroundServiceTypes(12); + + /// The raw value of [ForegroundServiceTypes]. + final int rawValue; +} diff --git a/lib/models/service_options.dart b/lib/models/service_options.dart index 32e4e975..4e4f391a 100644 --- a/lib/models/service_options.dart +++ b/lib/models/service_options.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:platform/platform.dart'; +import 'foreground_service_types.dart'; import 'foreground_task_options.dart'; import 'notification_button.dart'; import 'notification_icon.dart'; @@ -10,6 +11,7 @@ import 'notification_options.dart'; class ServiceStartOptions { const ServiceStartOptions({ this.serviceId, + this.serviceTypes, required this.androidNotificationOptions, required this.iosNotificationOptions, required this.foregroundTaskOptions, @@ -22,6 +24,7 @@ class ServiceStartOptions { }); final int? serviceId; + final List? serviceTypes; final AndroidNotificationOptions androidNotificationOptions; final IOSNotificationOptions iosNotificationOptions; final ForegroundTaskOptions foregroundTaskOptions; @@ -35,6 +38,7 @@ class ServiceStartOptions { Map toJson(Platform platform) { final Map json = { 'serviceId': serviceId, + 'serviceTypes': serviceTypes?.map((e) => e.rawValue).toList(), ...foregroundTaskOptions.toJson(), 'notificationContentTitle': notificationContentTitle, 'notificationContentText': notificationContentText, From 6928a3401163344dbc121a29c23e9a8a9ed437ff Mon Sep 17 00:00:00 2001 From: Dev-hwang Date: Mon, 14 Apr 2025 22:47:11 +0900 Subject: [PATCH 2/2] release: 9.1.0 --- CHANGELOG.md | 4 ++++ README.md | 14 +++++++++----- example/lib/main.dart | 6 ++++++ pubspec.yaml | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 378dab46..8d8c4aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.1.0 + +* [**FEAT**] Support manual foregroundServiceType via serviceTypes in startService + ## 9.0.0 * [**CHORE**] Bump minimum supported SDK version to `Flutter 3.22/Dart 3.4` diff --git a/README.md b/README.md index be5ebc18..90c89a7b 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.0.0 + flutter_foreground_task: ^9.1.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. @@ -41,10 +41,8 @@ This plugin requires `Kotlin version 1.9.10+` and `Gradle version 8.6.0+`. Pleas - [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"`. - +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"`. As mentioned in the Android guidelines, to start a FG service on Android 14+, you must declare `android:foregroundServiceType`. * [`camera`](https://developer.android.com/about/versions/14/changes/fgs-types-required#camera) @@ -379,6 +377,12 @@ Future _startService() async { return FlutterForegroundTask.restartService(); } else { return FlutterForegroundTask.startService( + // You can manually specify the foregroundServiceType for the service + // to be started, as shown in the comment below. + // serviceTypes: [ + // ForegroundServiceTypes.dataSync, + // ForegroundServiceTypes.remoteMessaging, + // ], serviceId: 256, notificationTitle: 'Foreground Service is running', notificationText: 'Tap to return to the app', diff --git a/example/lib/main.dart b/example/lib/main.dart index 11db4db1..2d9ac570 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -166,6 +166,12 @@ class _ExamplePageState extends State { return FlutterForegroundTask.restartService(); } else { return FlutterForegroundTask.startService( + // You can manually specify the foregroundServiceType for the service + // to be started, as shown in the comment below. + // serviceTypes: [ + // ForegroundServiceTypes.dataSync, + // ForegroundServiceTypes.remoteMessaging, + // ], serviceId: 256, notificationTitle: 'Foreground Service is running', notificationText: 'Tap to return to the app', diff --git a/pubspec.yaml b/pubspec.yaml index 38591c0b..54431ee2 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.0.0 +version: 9.1.0 homepage: https://github.com/Dev-hwang/flutter_foreground_task environment: