Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 `<application>` 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 `<application>` 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)
Expand Down Expand Up @@ -379,6 +377,12 @@ Future<ServiceRequestResult> _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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ class _ExamplePageState extends State<ExamplePage> {
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',
Expand Down
4 changes: 4 additions & 0 deletions lib/flutter_foreground_task.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -96,6 +98,7 @@ class FlutterForegroundTask {
/// Start the foreground service.
static Future<ServiceRequestResult> startService({
int? serviceId,
List<ForegroundServiceTypes>? serviceTypes,
required String notificationTitle,
required String notificationText,
NotificationIcon? notificationIcon,
Expand All @@ -117,6 +120,7 @@ class FlutterForegroundTask {
iosNotificationOptions: iosNotificationOptions!,
foregroundTaskOptions: foregroundTaskOptions!,
serviceId: serviceId,
serviceTypes: serviceTypes,
notificationTitle: notificationTitle,
notificationText: notificationText,
notificationIcon: notificationIcon,
Expand Down
3 changes: 3 additions & 0 deletions lib/flutter_foreground_task_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,6 +38,7 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform {
required IOSNotificationOptions iosNotificationOptions,
required ForegroundTaskOptions foregroundTaskOptions,
int? serviceId,
List<ForegroundServiceTypes>? serviceTypes,
required String notificationTitle,
required String notificationText,
NotificationIcon? notificationIcon,
Expand All @@ -46,6 +48,7 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform {
}) async {
final Map<String, dynamic> optionsJson = ServiceStartOptions(
serviceId: serviceId,
serviceTypes: serviceTypes,
androidNotificationOptions: androidNotificationOptions,
iosNotificationOptions: iosNotificationOptions,
foregroundTaskOptions: foregroundTaskOptions,
Expand Down
2 changes: 2 additions & 0 deletions lib/flutter_foreground_task_platform_interface.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -37,6 +38,7 @@ abstract class FlutterForegroundTaskPlatform extends PlatformInterface {
required IOSNotificationOptions iosNotificationOptions,
required ForegroundTaskOptions foregroundTaskOptions,
int? serviceId,
List<ForegroundServiceTypes>? serviceTypes,
required String notificationTitle,
required String notificationText,
NotificationIcon? notificationIcon,
Expand Down
54 changes: 54 additions & 0 deletions lib/models/foreground_service_types.dart
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions lib/models/service_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -22,6 +24,7 @@ class ServiceStartOptions {
});

final int? serviceId;
final List<ForegroundServiceTypes>? serviceTypes;
final AndroidNotificationOptions androidNotificationOptions;
final IOSNotificationOptions iosNotificationOptions;
final ForegroundTaskOptions foregroundTaskOptions;
Expand All @@ -35,6 +38,7 @@ class ServiceStartOptions {
Map<String, dynamic> toJson(Platform platform) {
final Map<String, dynamic> json = {
'serviceId': serviceId,
'serviceTypes': serviceTypes?.map((e) => e.rawValue).toList(),
...foregroundTaskOptions.toJson(),
'notificationContentTitle': notificationContentTitle,
'notificationContentText': notificationContentText,
Expand Down
Loading