Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Long-running workers on Android devices #573

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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()
}
Expand All @@ -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<Result> {
startTime = System.currentTimeMillis()

Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -190,6 +203,34 @@ object Extractor {
}
}

fun parseSetForegroundCall(call: MethodCall): SetForeground {
val foregroundServiceType =
call.argument<Int>(SetForeground.KEYS.FOREGROUND_SERVICE_TYPE_KEY)!!
val notificationId = call.argument<Int>(SetForeground.KEYS.NOTIFICATION_ID_KEY)!!
val notificationChannelId =
call.argument<String>(SetForeground.KEYS.NOTIFICATION_CHANNEL_ID_KEY)!!
val notificationChannelName =
call.argument<String>(SetForeground.KEYS.NOTIFICATION_CHANNEL_NAME_KEY)!!
val notificationChannelDescription =
call.argument<String>(SetForeground.KEYS.NOTIFICATION_CHANNEL_DESCRIPTION_KEY)!!
val notificationChannelImportance =
call.argument<Int>(SetForeground.KEYS.NOTIFICATION_CHANNEL_IMPORTANCE_KEY)!!
val notificationTitle =
call.argument<String>(SetForeground.KEYS.NOTIFICATION_TITLE_KEY)!!
val notificationDescription =
call.argument<String>(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 -> {
Expand All @@ -202,6 +243,7 @@ object Extractor {
WorkManagerCall.Initialize(handle, inDebugMode)
}
}

PossibleWorkManagerCall.REGISTER_ONE_OFF_TASK -> {
WorkManagerCall.RegisterTask.OneOffTask(
isInDebugMode = call.argument<Boolean>(REGISTER_TASK_IS_IN_DEBUG_MODE_KEY)!!,
Expand All @@ -213,13 +255,14 @@ object Extractor {
constraintsConfig = extractConstraintConfigFromCall(call),
outOfQuotaPolicy = extractOutOfQuotaPolicyFromCall(call),
backoffPolicyConfig =
extractBackoffPolicyConfigFromCall(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ened is this formatting ok, or it should be reverted. To not add unncessary changed lines and simplify reviews single formatting should be used

call,
TaskType.ONE_OFF,
),
extractBackoffPolicyConfigFromCall(
call,
TaskType.ONE_OFF,
),
payload = extractPayload(call),
)
}

PossibleWorkManagerCall.REGISTER_PERIODIC_TASK -> {
WorkManagerCall.RegisterTask.PeriodicTask(
isInDebugMode = call.argument<Boolean>(REGISTER_TASK_IS_IN_DEBUG_MODE_KEY)!!,
Expand All @@ -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),
)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.fluttercommunity.workmanager

import android.content.Context
import android.content.pm.ServiceInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the import used?

import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
Expand Down
Loading
Loading