diff --git a/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt b/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt index 08367891..b12dafd9 100644 --- a/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt +++ b/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt @@ -1,10 +1,15 @@ package dev.fluttercommunity.workmanager +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context +import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log import androidx.concurrent.futures.CallbackToFutureAdapter +import androidx.core.app.NotificationCompat +import androidx.work.ForegroundInfo import androidx.work.ListenableWorker import androidx.work.WorkerParameters import com.google.common.util.concurrent.ListenableFuture @@ -14,7 +19,8 @@ import io.flutter.embedding.engine.loader.FlutterLoader import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.view.FlutterCallbackInformation -import java.util.Random +import java.util.* + /*** * A simple worker that will post your input back to your Flutter application. @@ -37,6 +43,7 @@ class BackgroundWorker( const val BACKGROUND_CHANNEL_NAME = "be.tramckrijte.workmanager/background_channel_work_manager" const val BACKGROUND_CHANNEL_INITIALIZED = "backgroundChannelInitialized" + const val SET_FOREGROUND = "setForeground" private val flutterLoader = FlutterLoader() } @@ -63,6 +70,42 @@ class BackgroundWorker( null } + private fun createForegroundInfo( + setForegroundOptions: SetForeground + ): ForegroundInfo { + // Create a Notification channel if necessary + createNotificationChannel( + setForegroundOptions.notificationChannelId, + setForegroundOptions.notificationChannelName, + setForegroundOptions.notificationChannelDescription, + setForegroundOptions.notificationChannelImportance + ) + val notification = NotificationCompat.Builder(applicationContext, setForegroundOptions.notificationChannelId) + .setContentTitle(setForegroundOptions.notificationTitle) + .setTicker(setForegroundOptions.notificationTitle) + .setContentText(setForegroundOptions.notificationDescription) + .setOngoing(true) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .build() + + return ForegroundInfo( + setForegroundOptions.notificationId, + notification, + setForegroundOptions.foregroundServiceType + ) + } + + private fun createNotificationChannel(id: String, name: String, description: String, importance: Int) { + // Create a Notification channel + // Notification channels are only available in OREO and higher. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val mChannel = NotificationChannel(id, name, importance) + mChannel.description = description + val notificationManager = applicationContext.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(mChannel) + } + } + override fun startWork(): ListenableFuture { startTime = System.currentTimeMillis() @@ -141,35 +184,46 @@ class BackgroundWorker( } } + private fun onBackgroundChannelInitialized() { + backgroundChannel.invokeMethod( + "onResultSend", + mapOf(DART_TASK_KEY to dartTask, PAYLOAD_KEY to payload), + object : MethodChannel.Result { + override fun notImplemented() { + stopEngine(Result.failure()) + } + + override fun error( + errorCode: String, + errorMessage: String?, + errorDetails: Any?, + ) { + Log.e(TAG, "errorCode: $errorCode, errorMessage: $errorMessage") + stopEngine(Result.failure()) + } + + override fun success(receivedResult: Any?) { + val wasSuccessFul = receivedResult?.let { it as Boolean? } == true + stopEngine(if (wasSuccessFul) Result.success() else Result.retry()) + } + }, + ) + } + + private fun onSetForeground(setForegroundOptions: SetForeground) { + setForegroundAsync(createForegroundInfo(setForegroundOptions)) + } + override fun onMethodCall( call: MethodCall, r: MethodChannel.Result, ) { when (call.method) { BACKGROUND_CHANNEL_INITIALIZED -> - backgroundChannel.invokeMethod( - "onResultSend", - mapOf(DART_TASK_KEY to dartTask, PAYLOAD_KEY to payload), - object : MethodChannel.Result { - override fun notImplemented() { - stopEngine(Result.failure()) - } - - override fun error( - errorCode: String, - errorMessage: String?, - errorDetails: Any?, - ) { - Log.e(TAG, "errorCode: $errorCode, errorMessage: $errorMessage") - stopEngine(Result.failure()) - } - - override fun success(receivedResult: Any?) { - val wasSuccessFul = receivedResult?.let { it as Boolean? } == true - stopEngine(if (wasSuccessFul) Result.success() else Result.retry()) - } - }, - ) + onBackgroundChannelInitialized() + + SET_FOREGROUND -> onSetForeground(Extractor.parseSetForegroundCall(call)) } + r.success(null) } } diff --git a/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/Extractor.kt b/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/Extractor.kt index 4ac69596..f356af62 100644 --- a/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/Extractor.kt +++ b/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/Extractor.kt @@ -2,14 +2,7 @@ package dev.fluttercommunity.workmanager import android.os.Build import androidx.annotation.VisibleForTesting -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OutOfQuotaPolicy -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkRequest +import androidx.work.* import dev.fluttercommunity.workmanager.WorkManagerCall.CancelTask.ByTag.KEYS.UNREGISTER_TASK_TAG_KEY import dev.fluttercommunity.workmanager.WorkManagerCall.CancelTask.ByUniqueName.KEYS.UNREGISTER_TASK_UNIQUE_NAME_KEY import dev.fluttercommunity.workmanager.WorkManagerCall.Initialize.KEYS.INITIALIZE_TASK_CALL_HANDLE_KEY @@ -159,7 +152,27 @@ sealed class WorkManagerCall { class Failed(val code: String) : WorkManagerCall() } - +data class SetForeground( + val foregroundServiceType: Int, + val notificationId: Int, + val notificationChannelId: String, + val notificationChannelName: String, + val notificationChannelDescription: String, + val notificationChannelImportance: Int, + val notificationTitle: String, + val notificationDescription: String, +) { + companion object KEYS { + const val FOREGROUND_SERVICE_TYPE_KEY = "foregroundServiceType" + const val NOTIFICATION_ID_KEY = "notificationId" + const val NOTIFICATION_CHANNEL_ID_KEY = "notificationChannelId" + const val NOTIFICATION_CHANNEL_NAME_KEY = "notificationChannelName" + const val NOTIFICATION_CHANNEL_DESCRIPTION_KEY = "notificationChannelDescription" + const val NOTIFICATION_CHANNEL_IMPORTANCE_KEY = "notificationChannelImportance" + const val NOTIFICATION_TITLE_KEY = "notificationTitle" + const val NOTIFICATION_DESCRIPTION_KEY = "notificationDescription" + } +} private enum class TaskType(val minimumBackOffDelay: Long) { ONE_OFF(WorkRequest.MIN_BACKOFF_MILLIS), PERIODIC(WorkRequest.MIN_BACKOFF_MILLIS), @@ -190,6 +203,34 @@ object Extractor { } } + fun parseSetForegroundCall(call: MethodCall): SetForeground { + val foregroundServiceType = + call.argument(SetForeground.KEYS.FOREGROUND_SERVICE_TYPE_KEY)!! + val notificationId = call.argument(SetForeground.KEYS.NOTIFICATION_ID_KEY)!! + val notificationChannelId = + call.argument(SetForeground.KEYS.NOTIFICATION_CHANNEL_ID_KEY)!! + val notificationChannelName = + call.argument(SetForeground.KEYS.NOTIFICATION_CHANNEL_NAME_KEY)!! + val notificationChannelDescription = + call.argument(SetForeground.KEYS.NOTIFICATION_CHANNEL_DESCRIPTION_KEY)!! + val notificationChannelImportance = + call.argument(SetForeground.KEYS.NOTIFICATION_CHANNEL_IMPORTANCE_KEY)!! + val notificationTitle = + call.argument(SetForeground.KEYS.NOTIFICATION_TITLE_KEY)!! + val notificationDescription = + call.argument(SetForeground.KEYS.NOTIFICATION_DESCRIPTION_KEY)!! + return SetForeground( + foregroundServiceType, + notificationId, + notificationChannelId, + notificationChannelName, + notificationChannelDescription, + notificationChannelImportance, + notificationTitle, + notificationDescription, + ) + } + fun extractWorkManagerCallFromRawMethodName(call: MethodCall): WorkManagerCall = when (PossibleWorkManagerCall.fromRawMethodName(call.method)) { PossibleWorkManagerCall.INITIALIZE -> { @@ -202,6 +243,7 @@ object Extractor { WorkManagerCall.Initialize(handle, inDebugMode) } } + PossibleWorkManagerCall.REGISTER_ONE_OFF_TASK -> { WorkManagerCall.RegisterTask.OneOffTask( isInDebugMode = call.argument(REGISTER_TASK_IS_IN_DEBUG_MODE_KEY)!!, @@ -213,13 +255,14 @@ object Extractor { constraintsConfig = extractConstraintConfigFromCall(call), outOfQuotaPolicy = extractOutOfQuotaPolicyFromCall(call), backoffPolicyConfig = - extractBackoffPolicyConfigFromCall( - call, - TaskType.ONE_OFF, - ), + extractBackoffPolicyConfigFromCall( + call, + TaskType.ONE_OFF, + ), payload = extractPayload(call), ) } + PossibleWorkManagerCall.REGISTER_PERIODIC_TASK -> { WorkManagerCall.RegisterTask.PeriodicTask( isInDebugMode = call.argument(REGISTER_TASK_IS_IN_DEBUG_MODE_KEY)!!, @@ -232,10 +275,10 @@ object Extractor { initialDelaySeconds = extractInitialDelayFromCall(call), constraintsConfig = extractConstraintConfigFromCall(call), backoffPolicyConfig = - extractBackoffPolicyConfigFromCall( - call, - TaskType.PERIODIC, - ), + extractBackoffPolicyConfigFromCall( + call, + TaskType.PERIODIC, + ), outOfQuotaPolicy = extractOutOfQuotaPolicyFromCall(call), payload = extractPayload(call), ) @@ -251,12 +294,14 @@ object Extractor { WorkManagerCall.CancelTask.ByUniqueName( call.argument(UNREGISTER_TASK_UNIQUE_NAME_KEY)!!, ) + PossibleWorkManagerCall.CANCEL_TASK_BY_TAG -> WorkManagerCall.CancelTask.ByTag( call.argument( UNREGISTER_TASK_TAG_KEY, )!!, ) + PossibleWorkManagerCall.CANCEL_ALL -> WorkManagerCall.CancelTask.All PossibleWorkManagerCall.UNKNOWN -> WorkManagerCall.Unknown diff --git a/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerCallHandler.kt b/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerCallHandler.kt index 3b12de86..b4beefb3 100644 --- a/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerCallHandler.kt +++ b/workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerCallHandler.kt @@ -1,6 +1,7 @@ package dev.fluttercommunity.workmanager import android.content.Context +import android.content.pm.ServiceInfo import androidx.work.Constraints import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy diff --git a/workmanager/lib/src/workmanager.dart b/workmanager/lib/src/workmanager.dart index e52c0327..d558a90e 100644 --- a/workmanager/lib/src/workmanager.dart +++ b/workmanager/lib/src/workmanager.dart @@ -75,8 +75,8 @@ typedef BackgroundTaskHandler = Future Function( class Workmanager { factory Workmanager() => _instance; - Workmanager._internal( - MethodChannel backgroundChannel, MethodChannel foregroundChannel) + Workmanager._internal(MethodChannel backgroundChannel, + MethodChannel foregroundChannel) : _backgroundChannel = backgroundChannel, _foregroundChannel = foregroundChannel; @@ -150,14 +150,13 @@ class Workmanager { /// [callbackDispatcher] is a top level function which will be invoked by /// Android or iOS. See the discussion on [BackgroundTaskHandler] for details. /// [isInDebugMode] true will post debug notifications with information about when a task should have run - Future initialize( - final Function callbackDispatcher, { + Future initialize(final Function callbackDispatcher, { final bool isInDebugMode = false, }) async { Workmanager._isInDebugMode = isInDebugMode; final callback = PluginUtilities.getCallbackHandle(callbackDispatcher); assert(callback != null, - "The callbackDispatcher needs to be either a static function or a top level function to be accessible as a Flutter entry point."); + "The callbackDispatcher needs to be either a static function or a top level function to be accessible as a Flutter entry point."); if (callback != null) { final int handle = callback.toRawHandle(); await _foregroundChannel.invokeMethod( @@ -179,31 +178,32 @@ class Workmanager { /// The [taskName] is the value that will be returned in the [BackgroundTaskHandler] /// The [inputData] is the input data for task. Valid value types are: int, bool, double, String and their list Future registerOneOffTask( - /// Only supported on Android. - final String uniqueName, - /// Only supported on Android. - final String taskName, { - /// Only supported on Android. - final String? tag, + /// Only supported on Android. + final String uniqueName, - /// Only supported on Android. - final ExistingWorkPolicy? existingWorkPolicy, + /// Only supported on Android. + final String taskName, { + /// Only supported on Android. + final String? tag, - /// Configures a initial delay. - /// - /// The delay configured here is not guaranteed. The underlying system may - /// decide to schedule the ask a lot later. - final Duration initialDelay = Duration.zero, + /// Only supported on Android. + final ExistingWorkPolicy? existingWorkPolicy, - /// Fully supported on Android, but only partially supported on iOS. - /// See [Constraints] for details. - final Constraints? constraints, - final BackoffPolicy? backoffPolicy, - final Duration backoffPolicyDelay = Duration.zero, - final OutOfQuotaPolicy? outOfQuotaPolicy, - final Map? inputData, - }) async => + /// Configures a initial delay. + /// + /// The delay configured here is not guaranteed. The underlying system may + /// decide to schedule the ask a lot later. + final Duration initialDelay = Duration.zero, + + /// Fully supported on Android, but only partially supported on iOS. + /// See [Constraints] for details. + final Constraints? constraints, + final BackoffPolicy? backoffPolicy, + final Duration backoffPolicyDelay = Duration.zero, + final OutOfQuotaPolicy? outOfQuotaPolicy, + final Map? inputData, + }) async => await _foregroundChannel.invokeMethod( "registerOneOffTask", JsonMapperHelper.toRegisterMethodArgument( @@ -242,20 +242,19 @@ class Workmanager { /// [iOS 13+ Using background tasks to update your app](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app/) /// /// [iOS 13+ BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask/) - Future registerPeriodicTask( - final String uniqueName, - final String taskName, { - final Duration? frequency, - final Duration? flexInterval, - final String? tag, - final ExistingWorkPolicy? existingWorkPolicy, - final Duration initialDelay = Duration.zero, - final Constraints? constraints, - final BackoffPolicy? backoffPolicy, - final Duration backoffPolicyDelay = Duration.zero, - final OutOfQuotaPolicy? outOfQuotaPolicy, - final Map? inputData, - }) async => + Future registerPeriodicTask(final String uniqueName, + final String taskName, { + final Duration? frequency, + final Duration? flexInterval, + final String? tag, + final ExistingWorkPolicy? existingWorkPolicy, + final Duration initialDelay = Duration.zero, + final Constraints? constraints, + final BackoffPolicy? backoffPolicy, + final Duration backoffPolicyDelay = Duration.zero, + final OutOfQuotaPolicy? outOfQuotaPolicy, + final Map? inputData, + }) async => await _foregroundChannel.invokeMethod( "registerPeriodicTask", JsonMapperHelper.toRegisterMethodArgument( @@ -299,15 +298,14 @@ class Workmanager { /// [iOS 13+ Using background tasks to update your app](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app/) /// /// [iOS 13+ BGProcessingTask](https://developer.apple.com/documentation/backgroundtasks/bgprocessingtask/) - Future registerProcessingTask( - final String uniqueName, - final String taskName, { - final Duration initialDelay = Duration.zero, + Future registerProcessingTask(final String uniqueName, + final String taskName, { + final Duration initialDelay = Duration.zero, - /// Only partially supported on iOS. - /// See [Constraints] for details. - final Constraints? constraints, - }) async => + /// Only partially supported on iOS. + /// See [Constraints] for details. + final Constraints? constraints, + }) async => await _foregroundChannel.invokeMethod( "registerProcessingTask", JsonMapperHelper.toRegisterMethodArgument( @@ -337,6 +335,10 @@ class Workmanager { Future cancelAll() async => await _foregroundChannel.invokeMethod("cancelAllTasks"); + /// Sets the foreground options for the task. + Future setForeground(SetForegroundOptions options) async => + await _backgroundChannel.invokeMethod("setForeground", options.toMap()); + /// Prints details of un-executed scheduled tasks to console. To be used during /// development/debugging. /// @@ -416,5 +418,98 @@ class JsonMapperHelper { } static String? _enumToString(final dynamic enumeration) => - enumeration?.toString().split('.').last; + enumeration + ?.toString() + .split('.') + .last; +} + +class SetForegroundOptions { + final int foregroundServiceType; + final int notificationId; + + final String notificationChannelId; + final String notificationChannelName; + final String notificationChannelDescription; + final int notificationChannelImportance; + + final String notificationTitle; + final String notificationDescription; + + SetForegroundOptions({required this.foregroundServiceType, + required this.notificationId, + required this.notificationChannelId, + required this.notificationChannelName, + required this.notificationChannelDescription, + required this.notificationChannelImportance, + required this.notificationTitle, + required this.notificationDescription}); + + Map toMap() { + return { + "foregroundServiceType": foregroundServiceType, + "notificationId": notificationId, + "notificationChannelId": notificationChannelId, + "notificationChannelName": notificationChannelName, + "notificationChannelDescription": notificationChannelDescription, + "notificationChannelImportance": notificationChannelImportance, + "notificationTitle": notificationTitle, + "notificationDescription": notificationDescription, + }; + } + + SetForegroundOptions copyWith({ + int? foregroundServiceType, + int? notificationId, + String? notificationChannelId, + String? notificationChannelName, + String? notificationChannelDescription, + int? notificationChannelImportance, + String? notificationTitle, + String? notificationDescription, + }) { + return SetForegroundOptions( + foregroundServiceType: + foregroundServiceType ?? this.foregroundServiceType, + notificationId: notificationId ?? this.notificationId, + notificationChannelId: + notificationChannelId ?? this.notificationChannelId, + notificationChannelName: + notificationChannelName ?? this.notificationChannelName, + notificationChannelDescription: + notificationChannelDescription ?? this.notificationChannelDescription, + notificationChannelImportance: + notificationChannelImportance ?? this.notificationChannelImportance, + notificationTitle: notificationTitle ?? this.notificationTitle, + notificationDescription: + notificationDescription ?? this.notificationDescription, + ); + } +} + +class ForegroundServiceType { + static const int dataSync = 1 << 0; + static const int mediaPlayback = 1 << 1; + static const int phoneCall = 1 << 2; + static const int location = 1 << 3; + static const int connectedDevice = 1 << 4; + static const int mediaProjection = 1 << 5; + static const int camera = 1 << 6; + static const int microphone = 1 << 7; + static const int health = 1 << 8; + static const int remoteMessaging = 1 << 9; + static const int systemExempted = 1 << 10; + static const int shortService = 1 << 11; + static const int fileManagement = 1 << 12; + static const int specialUse = 1 << 30; + static const int manifest = -1; +} + +class NotificationImportance { + static const int none = 0; + static const int min = 1; + static const int low = 2; + static const int defaultImportance = 3; + static const int high = 4; + static const int max = 5; } diff --git a/workmanager/lib/workmanager.dart b/workmanager/lib/workmanager.dart index d7b8de19..074f6c56 100644 --- a/workmanager/lib/workmanager.dart +++ b/workmanager/lib/workmanager.dart @@ -2,3 +2,6 @@ library workmanager; export 'src/options.dart'; export 'src/workmanager.dart' show Workmanager; +export 'src/workmanager.dart' show SetForegroundOptions; +export 'src/workmanager.dart' show ForegroundServiceType; +export 'src/workmanager.dart' show NotificationImportance;