diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d7cd05f3..70aa6a17 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -41,7 +41,7 @@ SPEC CHECKSUMS: path_provider_foundation: 608fcb11be570ce83519b076ab6a1fffe2474f05 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - workmanager_apple: f540d652595dfe5c8b8200c4c85ba622d6fb5c5b + workmanager_apple: f073c5f57af569af5c2dab83ae031bd4396c8a95 PODFILE CHECKSUM: 4225ca2ac155c3e63d4d416fa6b1b890e2563502 diff --git a/melos.yaml b/melos.yaml index 64dece96..8fad648e 100644 --- a/melos.yaml +++ b/melos.yaml @@ -15,3 +15,7 @@ scripts: generate:dart: run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs" description: Build all generated files for Dart packages in this project. + + generate:pigeon: + run: cd workmanager_platform_interface && dart run pigeon --input pigeons/workmanager_api.dart + description: Generate Pigeon type-safe platform channel code for workmanager_platform_interface. diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/Extractor.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/Extractor.kt deleted file mode 100644 index 6af4564d..00000000 --- a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/Extractor.kt +++ /dev/null @@ -1,404 +0,0 @@ -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 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 -import dev.fluttercommunity.workmanager.WorkManagerCall.Initialize.KEYS.INITIALIZE_TASK_IS_IN_DEBUG_MODE_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.IsScheduled.ByUniqueName.KEYS.IS_SCHEDULED_UNIQUE_NAME_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_BACK_OFF_POLICY_DELAY_MILLIS_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_BACK_OFF_POLICY_TYPE_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_CONSTRAINTS_BATTERY_NOT_LOW_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_CONSTRAINTS_CHARGING_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_CONSTRAINTS_DEVICE_IDLE_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_CONSTRAINTS_NETWORK_TYPE_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_CONSTRAINTS_STORAGE_NOT_LOW_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_EXISTING_WORK_POLICY_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_INITIAL_DELAY_SECONDS_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_IS_IN_DEBUG_MODE_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_NAME_VALUE_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_OUT_OF_QUOTA_POLICY_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_PAYLOAD_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_TAG_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.KEYS.REGISTER_TASK_UNIQUE_NAME_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.PeriodicTask.KEYS.PERIODIC_FLEX_INTERVAL_SECONDS_KEY -import dev.fluttercommunity.workmanager.WorkManagerCall.RegisterTask.PeriodicTask.KEYS.PERIODIC_TASK_FREQUENCY_SECONDS_KEY -import io.flutter.plugin.common.MethodCall -import kotlin.math.max - -val defaultBackOffPolicy = BackoffPolicy.EXPONENTIAL -val defaultNetworkType = NetworkType.NOT_REQUIRED -val defaultOutOfQuotaPolicy: OutOfQuotaPolicy? = null -val defaultOneOffExistingWorkPolicy = ExistingWorkPolicy.KEEP -val defaultPeriodExistingWorkPolicy = ExistingPeriodicWorkPolicy.KEEP -val defaultConstraints: Constraints = Constraints.NONE -const val DEFAULT_INITIAL_DELAY_SECONDS = 0L -const val DEFAULT_REQUESTED_BACKOFF_DELAY = 0L -const val DEFAULT_PERIODIC_REFRESH_FREQUENCY_SECONDS = - PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS / 1000 -const val DEFAULT_FLEX_INTERVAL_SECONDS = - PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS / 1000 -const val LOG_TAG = "Extractor" - -data class BackoffPolicyTaskConfig( - val backoffPolicy: BackoffPolicy, - private val requestedBackoffDelay: Long, - private val minBackoffInMillis: Long, - val backoffDelay: Long = max(minBackoffInMillis, requestedBackoffDelay), -) - -sealed class WorkManagerCall { - data class Initialize( - val callbackDispatcherHandleKey: Long, - val isInDebugMode: Boolean, - ) : WorkManagerCall() { - companion object KEYS { - const val INITIALIZE_TASK_IS_IN_DEBUG_MODE_KEY = "isInDebugMode" - const val INITIALIZE_TASK_CALL_HANDLE_KEY = "callbackHandle" - } - } - - sealed class RegisterTask : WorkManagerCall() { - abstract val isInDebugMode: Boolean - abstract val uniqueName: String - abstract val taskName: String - abstract val tag: String? - abstract val initialDelaySeconds: Long - abstract val constraintsConfig: Constraints? - abstract val payload: Map? - - companion object KEYS { - const val REGISTER_TASK_IS_IN_DEBUG_MODE_KEY = "isInDebugMode" - const val REGISTER_TASK_UNIQUE_NAME_KEY = "uniqueName" - const val REGISTER_TASK_NAME_VALUE_KEY = "taskName" - const val REGISTER_TASK_TAG_KEY = "tag" - const val REGISTER_TASK_EXISTING_WORK_POLICY_KEY = "existingWorkPolicy" - - const val REGISTER_TASK_CONSTRAINTS_NETWORK_TYPE_KEY = "networkType" - const val REGISTER_TASK_CONSTRAINTS_BATTERY_NOT_LOW_KEY = "requiresBatteryNotLow" - const val REGISTER_TASK_CONSTRAINTS_CHARGING_KEY = "requiresCharging" - const val REGISTER_TASK_CONSTRAINTS_DEVICE_IDLE_KEY = "requiresDeviceIdle" - const val REGISTER_TASK_CONSTRAINTS_STORAGE_NOT_LOW_KEY = "requiresStorageNotLow" - - const val REGISTER_TASK_INITIAL_DELAY_SECONDS_KEY = "initialDelaySeconds" - - const val REGISTER_TASK_BACK_OFF_POLICY_TYPE_KEY = "backoffPolicyType" - const val REGISTER_TASK_BACK_OFF_POLICY_DELAY_MILLIS_KEY = "backoffDelayInMilliseconds" - const val REGISTER_TASK_OUT_OF_QUOTA_POLICY_KEY = "outOfQuotaPolicy" - const val REGISTER_TASK_PAYLOAD_KEY = "inputData" - } - - data class OneOffTask( - override val isInDebugMode: Boolean, - override val uniqueName: String, - override val taskName: String, - override val tag: String? = null, - val existingWorkPolicy: ExistingWorkPolicy, - override val initialDelaySeconds: Long, - override val constraintsConfig: Constraints, - val backoffPolicyConfig: BackoffPolicyTaskConfig?, - val outOfQuotaPolicy: OutOfQuotaPolicy?, - override val payload: Map? = null, - ) : RegisterTask() - - data class PeriodicTask( - override val isInDebugMode: Boolean, - override val uniqueName: String, - override val taskName: String, - override val tag: String? = null, - val existingWorkPolicy: ExistingPeriodicWorkPolicy, - val frequencyInSeconds: Long, - val flexIntervalInSeconds: Long, - override val initialDelaySeconds: Long, - override val constraintsConfig: Constraints, - val backoffPolicyConfig: BackoffPolicyTaskConfig?, - val outOfQuotaPolicy: OutOfQuotaPolicy?, - override val payload: Map? = null, - ) : RegisterTask() { - companion object KEYS { - const val PERIODIC_TASK_FREQUENCY_SECONDS_KEY = "frequency" - const val PERIODIC_FLEX_INTERVAL_SECONDS_KEY = "flexInterval" - } - } - } - - sealed class IsScheduled : WorkManagerCall() { - data class ByUniqueName( - val uniqueName: String, - ) : IsScheduled() { - companion object KEYS { - const val IS_SCHEDULED_UNIQUE_NAME_KEY = "uniqueName" - } - } - } - - sealed class CancelTask : WorkManagerCall() { - data class ByUniqueName( - val uniqueName: String, - ) : CancelTask() { - companion object KEYS { - const val UNREGISTER_TASK_UNIQUE_NAME_KEY = "uniqueName" - } - } - - data class ByTag( - val tag: String, - ) : CancelTask() { - companion object KEYS { - const val UNREGISTER_TASK_TAG_KEY = "tag" - } - } - - object All : CancelTask() - } - - object Unknown : WorkManagerCall() - - class Failed( - val code: String, - ) : WorkManagerCall() -} - -private enum class TaskType( - val minimumBackOffDelay: Long, -) { - ONE_OFF(WorkRequest.MIN_BACKOFF_MILLIS), - PERIODIC(WorkRequest.MIN_BACKOFF_MILLIS), -} - -object Extractor { - private enum class PossibleWorkManagerCall( - val rawMethodName: String?, - ) { - INITIALIZE("initialize"), - - REGISTER_ONE_OFF_TASK("registerOneOffTask"), - REGISTER_PERIODIC_TASK("registerPeriodicTask"), - - IS_SCHEDULED_BY_UNIQUE_NAME("isScheduledByUniqueName"), - - CANCEL_TASK_BY_UNIQUE_NAME("cancelTaskByUniqueName"), - CANCEL_TASK_BY_TAG("cancelTaskByTag"), - CANCEL_ALL("cancelAllTasks"), - - UNKNOWN(null), - ; - - companion object { - fun fromRawMethodName(methodName: String): PossibleWorkManagerCall = - values() - .filter { !it.rawMethodName.isNullOrEmpty() } - .firstOrNull { it.rawMethodName == methodName } - ?: UNKNOWN - } - } - - fun extractWorkManagerCallFromRawMethodName(call: MethodCall): WorkManagerCall = - when (PossibleWorkManagerCall.fromRawMethodName(call.method)) { - PossibleWorkManagerCall.INITIALIZE -> { - val handle = call.argument(INITIALIZE_TASK_CALL_HANDLE_KEY)?.toLong() - val inDebugMode = call.argument(INITIALIZE_TASK_IS_IN_DEBUG_MODE_KEY) - - if (handle == null || inDebugMode == null) { - WorkManagerCall.Failed("Invalid parameters passed") - } else { - WorkManagerCall.Initialize(handle, inDebugMode) - } - } - PossibleWorkManagerCall.REGISTER_ONE_OFF_TASK -> { - WorkManagerCall.RegisterTask.OneOffTask( - isInDebugMode = call.argument(REGISTER_TASK_IS_IN_DEBUG_MODE_KEY) ?: false, - uniqueName = call.argument(REGISTER_TASK_UNIQUE_NAME_KEY)!!, - taskName = call.argument(REGISTER_TASK_NAME_VALUE_KEY)!!, - tag = call.argument(REGISTER_TASK_TAG_KEY), - existingWorkPolicy = extractExistingWorkPolicyFromCall(call), - initialDelaySeconds = extractInitialDelayFromCall(call), - constraintsConfig = extractConstraintConfigFromCall(call), - outOfQuotaPolicy = extractOutOfQuotaPolicyFromCall(call), - backoffPolicyConfig = - 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) ?: false, - uniqueName = call.argument(REGISTER_TASK_UNIQUE_NAME_KEY)!!, - taskName = call.argument(REGISTER_TASK_NAME_VALUE_KEY)!!, - frequencyInSeconds = extractFrequencySecondsFromCall(call), - tag = call.argument(REGISTER_TASK_TAG_KEY), - flexIntervalInSeconds = extractFlexIntervalSecondsFromCall(call), - existingWorkPolicy = extractExistingPeriodicWorkPolicyFromCall(call), - initialDelaySeconds = extractInitialDelayFromCall(call), - constraintsConfig = extractConstraintConfigFromCall(call), - backoffPolicyConfig = - extractBackoffPolicyConfigFromCall( - call, - TaskType.PERIODIC, - ), - outOfQuotaPolicy = extractOutOfQuotaPolicyFromCall(call), - payload = extractPayload(call), - ) - } - - PossibleWorkManagerCall.IS_SCHEDULED_BY_UNIQUE_NAME -> { - WorkManagerCall.IsScheduled.ByUniqueName( - call.argument(IS_SCHEDULED_UNIQUE_NAME_KEY)!!, - ) - } - - PossibleWorkManagerCall.CANCEL_TASK_BY_UNIQUE_NAME -> - 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 - } - - private fun extractExistingWorkPolicyFromCall(call: MethodCall): ExistingWorkPolicy = - try { - ExistingWorkPolicy.valueOf( - call.argument(REGISTER_TASK_EXISTING_WORK_POLICY_KEY)!!.uppercase(), - ) - } catch (ignored: Exception) { - defaultOneOffExistingWorkPolicy - } - - private fun extractExistingPeriodicWorkPolicyFromCall(call: MethodCall) = - try { - ExistingPeriodicWorkPolicy.valueOf( - call - .argument( - REGISTER_TASK_EXISTING_WORK_POLICY_KEY, - )!! - .uppercase(), - ) - } catch (ignored: Exception) { - defaultPeriodExistingWorkPolicy - } - - private fun extractFrequencySecondsFromCall(call: MethodCall) = - call.argument(PERIODIC_TASK_FREQUENCY_SECONDS_KEY)?.toLong() - ?: DEFAULT_PERIODIC_REFRESH_FREQUENCY_SECONDS - - private fun extractFlexIntervalSecondsFromCall(call: MethodCall) = - call.argument(PERIODIC_FLEX_INTERVAL_SECONDS_KEY)?.toLong() - ?: DEFAULT_FLEX_INTERVAL_SECONDS - - private fun extractInitialDelayFromCall(call: MethodCall) = - call.argument(REGISTER_TASK_INITIAL_DELAY_SECONDS_KEY)?.toLong() - ?: DEFAULT_INITIAL_DELAY_SECONDS - - private fun extractBackoffPolicyConfigFromCall( - call: MethodCall, - taskType: TaskType, - ): BackoffPolicyTaskConfig? { - if (call.argument(REGISTER_TASK_BACK_OFF_POLICY_TYPE_KEY) == null) { - return null - } - - val backoffPolicy = - try { - BackoffPolicy.valueOf( - call.argument(REGISTER_TASK_BACK_OFF_POLICY_TYPE_KEY)!!.uppercase(), - ) - } catch (ignored: Exception) { - defaultBackOffPolicy - } - - val requestedBackoffDelay = - call.argument(REGISTER_TASK_BACK_OFF_POLICY_DELAY_MILLIS_KEY)?.toLong() - ?: DEFAULT_REQUESTED_BACKOFF_DELAY - val minimumBackOffDelay = taskType.minimumBackOffDelay - - return BackoffPolicyTaskConfig( - backoffPolicy, - requestedBackoffDelay, - minimumBackOffDelay, - ) - } - - @VisibleForTesting - fun extractOutOfQuotaPolicyFromCall(call: MethodCall): OutOfQuotaPolicy? { - try { - val dartValue = call.argument(REGISTER_TASK_OUT_OF_QUOTA_POLICY_KEY)!! - // Map camelCase Dart enum values to Android enum names - val androidValue = - when (dartValue) { - "runAsNonExpeditedWorkRequest" -> "RUN_AS_NON_EXPEDITED_WORK_REQUEST" - "dropWorkRequest" -> "DROP_WORK_REQUEST" - else -> dartValue.uppercase() - } - return OutOfQuotaPolicy.valueOf(androidValue) - } catch (ignored: Exception) { - return defaultOutOfQuotaPolicy - } - } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun extractConstraintConfigFromCall(call: MethodCall): Constraints { - fun extractNetworkTypeFromCall(call: MethodCall) = - try { - val dartValue = call.argument(REGISTER_TASK_CONSTRAINTS_NETWORK_TYPE_KEY)!! - // Map camelCase Dart enum values to Android enum names - val androidValue = - when (dartValue) { - "notRequired" -> "NOT_REQUIRED" - "notRoaming" -> "NOT_ROAMING" - "temporarilyUnmetered" -> "TEMPORARILY_UNMETERED" - "runAsNonExpeditedWorkRequest" -> "RUN_AS_NON_EXPEDITED_WORK_REQUEST" - "dropWorkRequest" -> "DROP_WORK_REQUEST" - else -> dartValue.uppercase() - } - NetworkType.valueOf(androidValue) - } catch (ignored: Exception) { - defaultNetworkType - } - - val requestedNetworkType = extractNetworkTypeFromCall(call) - val requiresBatteryNotLow = - call.argument(REGISTER_TASK_CONSTRAINTS_BATTERY_NOT_LOW_KEY) - ?: false - val requiresCharging = - call.argument(REGISTER_TASK_CONSTRAINTS_CHARGING_KEY) - ?: false - val requiresDeviceIdle = - call.argument(REGISTER_TASK_CONSTRAINTS_DEVICE_IDLE_KEY) - ?: false - val requiresStorageNotLow = - call.argument(REGISTER_TASK_CONSTRAINTS_STORAGE_NOT_LOW_KEY) - ?: false - return Constraints - .Builder() - .setRequiredNetworkType(requestedNetworkType) - .setRequiresBatteryNotLow(requiresBatteryNotLow) - .setRequiresCharging(requiresCharging) - .setRequiresStorageNotLow(requiresStorageNotLow) - .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setRequiresDeviceIdle(requiresDeviceIdle) - } - }.build() - } - - private fun extractPayload(call: MethodCall): Map? = call.argument>(REGISTER_TASK_PAYLOAD_KEY) -} diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/SharedPreferenceHelper.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/SharedPreferenceHelper.kt index ced13d55..4c6d3695 100644 --- a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/SharedPreferenceHelper.kt +++ b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/SharedPreferenceHelper.kt @@ -1,25 +1,45 @@ package dev.fluttercommunity.workmanager import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit -object SharedPreferenceHelper { - private const val SHARED_PREFS_FILE_NAME = "flutter_workmanager_plugin" - private const val CALLBACK_DISPATCHER_HANDLE_KEY = "dev.fluttercommunity.workmanager.CALLBACK_DISPATCHER_HANDLE_KEY" - - private fun Context.prefs() = getSharedPreferences(SHARED_PREFS_FILE_NAME, Context.MODE_PRIVATE) +class SharedPreferenceHelper( + private val context: Context, + private val dispatcherHandleListener: DispatcherHandleListener +) { + // Interface to listen for changes in the dispatcher handle. + // This allows the plugin to react when the dispatcher handle is updated. + interface DispatcherHandleListener { + // Called when the dispatcher handle changes. + fun onDispatcherHandleChanged(handle: Long) + } - fun saveCallbackDispatcherHandleKey( - ctx: Context, - callbackHandle: Long, - ) { - ctx - .prefs() - .edit() - .putLong(CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle) - .apply() + companion object { + private const val SHARED_PREFS_FILE_NAME = "flutter_workmanager_plugin" + private const val CALLBACK_DISPATCHER_HANDLE_KEY = + "dev.fluttercommunity.workmanager.CALLBACK_DISPATCHER_HANDLE_KEY" } - fun getCallbackHandle(ctx: Context): Long = ctx.prefs().getLong(CALLBACK_DISPATCHER_HANDLE_KEY, -1L) + private val preferences: SharedPreferences + get() = context.getSharedPreferences(SHARED_PREFS_FILE_NAME, Context.MODE_PRIVATE) + + private val preferenceListener: (sharedPreferences: SharedPreferences, key: String?) -> Unit = + { preferences, key -> + if (key == CALLBACK_DISPATCHER_HANDLE_KEY) { + dispatcherHandleListener.onDispatcherHandleChanged( + preferences.getLong(CALLBACK_DISPATCHER_HANDLE_KEY, -1L) + ) + } + } - fun hasCallbackHandle(ctx: Context) = ctx.prefs().contains(CALLBACK_DISPATCHER_HANDLE_KEY) + init { + preferences.registerOnSharedPreferenceChangeListener(preferenceListener) + } + + fun saveCallbackDispatcherHandleKey(callbackHandle: Long) { + preferences.edit { + putLong(CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle) + } + } } diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkManagerUtils.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkManagerUtils.kt new file mode 100644 index 00000000..7924e42f --- /dev/null +++ b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkManagerUtils.kt @@ -0,0 +1,258 @@ +package dev.fluttercommunity.workmanager + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.OutOfQuotaPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import dev.fluttercommunity.workmanager.BackgroundWorker.Companion.DART_TASK_KEY +import dev.fluttercommunity.workmanager.BackgroundWorker.Companion.IS_IN_DEBUG_MODE_KEY +import java.util.concurrent.TimeUnit + +// Constants +const val DEFAULT_INITIAL_DELAY_SECONDS = 0L +const val DEFAULT_FLEX_INTERVAL_SECONDS = + PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS / 1000 + +// Default values +val defaultOneOffExistingWorkPolicy = ExistingWorkPolicy.KEEP +val defaultPeriodExistingWorkPolicy = ExistingPeriodicWorkPolicy.KEEP +val defaultConstraints: Constraints = Constraints.NONE +val defaultOutOfQuotaPolicy: OutOfQuotaPolicy? = null + + +// Extension functions to convert Pigeon types to Android WorkManager types +private fun dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.toAndroidWorkPolicy(): ExistingWorkPolicy { + return when (this) { + dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.APPEND -> ExistingWorkPolicy.APPEND_OR_REPLACE + dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.KEEP -> ExistingWorkPolicy.KEEP + dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.REPLACE -> ExistingWorkPolicy.REPLACE + dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.UPDATE -> ExistingWorkPolicy.APPEND_OR_REPLACE + } +} + +private fun dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.toAndroidPeriodicWorkPolicy(): ExistingPeriodicWorkPolicy { + return when (this) { + dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.APPEND -> ExistingPeriodicWorkPolicy.REPLACE + dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.KEEP -> ExistingPeriodicWorkPolicy.KEEP + dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.REPLACE -> ExistingPeriodicWorkPolicy.REPLACE + dev.fluttercommunity.workmanager.pigeon.ExistingWorkPolicy.UPDATE -> ExistingPeriodicWorkPolicy.UPDATE + } +} + +private fun dev.fluttercommunity.workmanager.pigeon.OutOfQuotaPolicy.toAndroidOutOfQuotaPolicy(): OutOfQuotaPolicy { + return when (this) { + dev.fluttercommunity.workmanager.pigeon.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST -> OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST + dev.fluttercommunity.workmanager.pigeon.OutOfQuotaPolicy.DROP_WORK_REQUEST -> OutOfQuotaPolicy.DROP_WORK_REQUEST + } +} + +private fun dev.fluttercommunity.workmanager.pigeon.Constraints.toAndroidConstraints(): Constraints { + val builder = Constraints.Builder() + + networkType?.let { builder.setRequiredNetworkType(it.toAndroidNetworkType()) } + requiresBatteryNotLow?.let { builder.setRequiresBatteryNotLow(it) } + requiresCharging?.let { builder.setRequiresCharging(it) } + requiresDeviceIdle?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.setRequiresDeviceIdle(it) + } + } + requiresStorageNotLow?.let { builder.setRequiresStorageNotLow(it) } + + return builder.build() +} + +private fun dev.fluttercommunity.workmanager.pigeon.NetworkType.toAndroidNetworkType(): NetworkType { + return when (this) { + dev.fluttercommunity.workmanager.pigeon.NetworkType.CONNECTED -> NetworkType.CONNECTED + dev.fluttercommunity.workmanager.pigeon.NetworkType.METERED -> NetworkType.METERED + dev.fluttercommunity.workmanager.pigeon.NetworkType.NOT_REQUIRED -> NetworkType.NOT_REQUIRED + dev.fluttercommunity.workmanager.pigeon.NetworkType.NOT_ROAMING -> NetworkType.NOT_ROAMING + dev.fluttercommunity.workmanager.pigeon.NetworkType.UNMETERED -> NetworkType.UNMETERED + dev.fluttercommunity.workmanager.pigeon.NetworkType.TEMPORARILY_UNMETERED -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + NetworkType.TEMPORARILY_UNMETERED + } else { + NetworkType.UNMETERED + } + } + } +} + + +private fun dev.fluttercommunity.workmanager.pigeon.BackoffPolicy.toAndroidBackoffPolicy(): BackoffPolicy { + return when (this) { + dev.fluttercommunity.workmanager.pigeon.BackoffPolicy.EXPONENTIAL -> BackoffPolicy.EXPONENTIAL + dev.fluttercommunity.workmanager.pigeon.BackoffPolicy.LINEAR -> BackoffPolicy.LINEAR + } +} + +// Helper function to filter out null keys from Map +private fun Map.filterNotNullKeys(): Map { + return this.mapNotNull { (key, value) -> + if (key != null && value != null) key to value else null + }.toMap() +} + +class WorkManagerWrapper(val context: Context) { + private val workManager = WorkManager.getInstance(context) + + fun enqueueOneOffTask( + request: dev.fluttercommunity.workmanager.pigeon.OneOffTaskRequest, + isInDebugMode: Boolean = false, + ) { + try { + val oneOffTaskRequest = + OneTimeWorkRequest + .Builder(BackgroundWorker::class.java) + .setInputData( + buildTaskInputData( + request.taskName, + isInDebugMode, + request.inputData?.filterNotNullKeys() + ) + ) + .setInitialDelay( + request.initialDelaySeconds ?: DEFAULT_INITIAL_DELAY_SECONDS, + TimeUnit.SECONDS + ) + .setConstraints( + request.constraints?.toAndroidConstraints() ?: defaultConstraints + ) + .apply { + request.backoffPolicy?.let { backoffConfig -> + if (backoffConfig.backoffPolicy != null && backoffConfig.backoffDelayMillis != null) { + setBackoffCriteria( + backoffConfig.backoffPolicy.toAndroidBackoffPolicy(), + backoffConfig.backoffDelayMillis.toLong(), + TimeUnit.MILLISECONDS, + ) + } + } + }.apply { + request.tag?.let(::addTag) + request.outOfQuotaPolicy?.toAndroidOutOfQuotaPolicy()?.let(::setExpedited) + }.build() + workManager.enqueueUniqueWork( + request.uniqueName, + request.existingWorkPolicy?.toAndroidWorkPolicy() + ?: defaultOneOffExistingWorkPolicy, + oneOffTaskRequest + ) + } catch (e: Exception) { + throw e + } + } + + fun enqueuePeriodicTask( + request: dev.fluttercommunity.workmanager.pigeon.PeriodicTaskRequest, + isInDebugMode: Boolean = false, + ) { + val periodicTaskRequest = + PeriodicWorkRequest + .Builder( + BackgroundWorker::class.java, + request.frequencySeconds, + TimeUnit.SECONDS, + request.flexIntervalSeconds ?: DEFAULT_FLEX_INTERVAL_SECONDS, + TimeUnit.SECONDS, + ).setInputData( + buildTaskInputData( + request.taskName, + isInDebugMode, + request.inputData?.filterNotNullKeys() + ) + ) + .setInitialDelay( + request.initialDelaySeconds ?: DEFAULT_INITIAL_DELAY_SECONDS, + TimeUnit.SECONDS + ) + .setConstraints(request.constraints?.toAndroidConstraints() ?: defaultConstraints) + .apply { + request.backoffPolicy?.let { backoffConfig -> + if (backoffConfig.backoffPolicy != null && backoffConfig.backoffDelayMillis != null) { + setBackoffCriteria( + backoffConfig.backoffPolicy.toAndroidBackoffPolicy(), + backoffConfig.backoffDelayMillis.toLong(), + TimeUnit.MILLISECONDS, + ) + } + } + }.apply { + request.tag?.let(::addTag) + // Note: outOfQuotaPolicy is not supported for periodic tasks + }.build() + workManager.enqueueUniquePeriodicWork( + request.uniqueName, + request.existingWorkPolicy?.toAndroidPeriodicWorkPolicy() + ?: defaultPeriodExistingWorkPolicy, + periodicTaskRequest + ) + } + + private fun buildTaskInputData( + dartTask: String, + isInDebugMode: Boolean, + payload: Map?, + ): Data { + val builder = + Data + .Builder() + .putString(DART_TASK_KEY, dartTask) + .putBoolean(IS_IN_DEBUG_MODE_KEY, isInDebugMode) + + // Add payload data if provided + payload?.forEach { (key, value) -> + when (value) { + is String -> builder.putString("payload_$key", value) + is Boolean -> builder.putBoolean("payload_$key", value) + is Int -> builder.putInt("payload_$key", value) + is Long -> builder.putLong("payload_$key", value) + is Float -> builder.putFloat("payload_$key", value) + is Double -> builder.putDouble("payload_$key", value) + is Array<*> -> + builder.putStringArray( + "payload_$key", + value.filterIsInstance().toTypedArray(), + ) + + is List<*> -> + builder.putStringArray( + "payload_$key", + value.filterIsInstance().toTypedArray(), + ) + + is ByteArray -> builder.putByteArray("payload_$key", value) + + else -> { + throw IllegalArgumentException( + "Unsupported payload type for key '$key': ${value::class.java.simpleName}. " + + "Consider converting it to a supported type.", + ) + } + } + } + + return builder.build() + } + + fun getWorkInfoByUniqueName(uniqueWorkName: String) = + workManager.getWorkInfosForUniqueWork(uniqueWorkName) + + fun cancelByUniqueName(uniqueWorkName: String) = + workManager.cancelUniqueWork(uniqueWorkName) + + fun cancelByTag(tag: String) = + workManager.cancelAllWorkByTag(tag) + + fun cancelAll() = workManager.cancelAllWork() +} \ No newline at end of file diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerCallHandler.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerCallHandler.kt deleted file mode 100644 index 1881a47e..00000000 --- a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerCallHandler.kt +++ /dev/null @@ -1,374 +0,0 @@ -package dev.fluttercommunity.workmanager - -import android.content.Context -import androidx.work.Constraints -import androidx.work.Data -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkInfo -import androidx.work.WorkManager -import dev.fluttercommunity.workmanager.BackgroundWorker.Companion.DART_TASK_KEY -import dev.fluttercommunity.workmanager.BackgroundWorker.Companion.IS_IN_DEBUG_MODE_KEY -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import java.util.concurrent.TimeUnit - -private fun Context.workManager() = WorkManager.getInstance(this) - -private fun MethodChannel.Result.success() = success(true) - -private interface CallHandler { - fun handle( - context: Context, - convertedCall: T, - result: MethodChannel.Result, - ) -} - -class WorkmanagerCallHandler( - private val ctx: Context, -) : MethodChannel.MethodCallHandler { - override fun onMethodCall( - call: MethodCall, - result: MethodChannel.Result, - ) { - when (val extractedCall = Extractor.extractWorkManagerCallFromRawMethodName(call)) { - is WorkManagerCall.Initialize -> - InitializeHandler.handle( - ctx, - extractedCall, - result, - ) - - is WorkManagerCall.RegisterTask -> - RegisterTaskHandler.handle( - ctx, - extractedCall, - result, - ) - - is WorkManagerCall.IsScheduled -> - IsScheduledHandler.handle( - ctx, - extractedCall, - result, - ) - - is WorkManagerCall.CancelTask -> - UnregisterTaskHandler.handle( - ctx, - extractedCall, - result, - ) - - is WorkManagerCall.Failed -> - FailedTaskHandler(extractedCall.code).handle( - ctx, - extractedCall, - result, - ) - - is WorkManagerCall.Unknown -> UnknownTaskHandler.handle(ctx, extractedCall, result) - } - } -} - -private object InitializeHandler : CallHandler { - override fun handle( - context: Context, - convertedCall: WorkManagerCall.Initialize, - result: MethodChannel.Result, - ) { - SharedPreferenceHelper.saveCallbackDispatcherHandleKey( - context, - convertedCall.callbackDispatcherHandleKey, - ) - result.success() - } -} - -private object RegisterTaskHandler : CallHandler { - override fun handle( - context: Context, - convertedCall: WorkManagerCall.RegisterTask, - result: MethodChannel.Result, - ) { - if (!SharedPreferenceHelper.hasCallbackHandle(context)) { - result.error( - "1", - "You have not properly initialized the Flutter WorkManager Package. " + - "You should ensure you have called the 'initialize' function first! " + - "Example: \n" + - "\n" + - "`Workmanager().initialize(\n" + - " callbackDispatcher,\n" + - " )`" + - "\n" + - "\n" + - "The `callbackDispatcher` is a top level function. See example in repository.", - null, - ) - return - } - - when (convertedCall) { - is WorkManagerCall.RegisterTask.OneOffTask -> enqueueOneOffTask(context, convertedCall) - is WorkManagerCall.RegisterTask.PeriodicTask -> - enqueuePeriodicTask( - context, - convertedCall, - ) - } - result.success() - } - - private fun enqueuePeriodicTask( - context: Context, - convertedCall: WorkManagerCall.RegisterTask.PeriodicTask, - ) { - WM.enqueuePeriodicTask( - context = context, - uniqueName = convertedCall.uniqueName, - dartTask = convertedCall.taskName, - tag = convertedCall.tag, - flexIntervalInSeconds = convertedCall.flexIntervalInSeconds, - frequencyInSeconds = convertedCall.frequencyInSeconds, - isInDebugMode = convertedCall.isInDebugMode, - existingWorkPolicy = convertedCall.existingWorkPolicy, - initialDelaySeconds = convertedCall.initialDelaySeconds, - constraintsConfig = convertedCall.constraintsConfig, - backoffPolicyConfig = convertedCall.backoffPolicyConfig, - outOfQuotaPolicy = convertedCall.outOfQuotaPolicy, - payload = convertedCall.payload, - ) - } - - private fun enqueueOneOffTask( - context: Context, - convertedCall: WorkManagerCall.RegisterTask.OneOffTask, - ) { - WM.enqueueOneOffTask( - context = context, - uniqueName = convertedCall.uniqueName, - dartTask = convertedCall.taskName, - tag = convertedCall.tag, - isInDebugMode = convertedCall.isInDebugMode, - existingWorkPolicy = convertedCall.existingWorkPolicy, - initialDelaySeconds = convertedCall.initialDelaySeconds, - constraintsConfig = convertedCall.constraintsConfig, - backoffPolicyConfig = convertedCall.backoffPolicyConfig, - outOfQuotaPolicy = convertedCall.outOfQuotaPolicy, - payload = convertedCall.payload, - ) - } -} - -private object IsScheduledHandler : CallHandler { - override fun handle( - context: Context, - convertedCall: WorkManagerCall.IsScheduled, - result: MethodChannel.Result, - ) { - when (convertedCall) { - is WorkManagerCall.IsScheduled.ByUniqueName -> { - val workInfos = WM.getWorkInfoByUniqueName(context, convertedCall.uniqueName).get() - val scheduled = - workInfos.isNotEmpty() && - workInfos.all { it.state == WorkInfo.State.ENQUEUED || it.state == WorkInfo.State.RUNNING } - return result.success(scheduled) - } - } - } -} - -private object UnregisterTaskHandler : CallHandler { - override fun handle( - context: Context, - convertedCall: WorkManagerCall.CancelTask, - result: MethodChannel.Result, - ) { - when (convertedCall) { - is WorkManagerCall.CancelTask.ByUniqueName -> - WM.cancelByUniqueName( - context, - convertedCall.uniqueName, - ) - - is WorkManagerCall.CancelTask.ByTag -> WM.cancelByTag(context, convertedCall.tag) - WorkManagerCall.CancelTask.All -> WM.cancelAll(context) - } - result.success() - } -} - -class FailedTaskHandler( - private val code: String, -) : CallHandler { - override fun handle( - context: Context, - convertedCall: WorkManagerCall.Failed, - result: MethodChannel.Result, - ) { - result.error(code, null, null) - } -} - -private object UnknownTaskHandler : CallHandler { - override fun handle( - context: Context, - convertedCall: WorkManagerCall.Unknown, - result: MethodChannel.Result, - ) { - result.notImplemented() - } -} - -object WM { - fun enqueueOneOffTask( - context: Context, - uniqueName: String, - dartTask: String, - payload: Map? = null, - tag: String? = null, - isInDebugMode: Boolean = false, - existingWorkPolicy: ExistingWorkPolicy = defaultOneOffExistingWorkPolicy, - initialDelaySeconds: Long = DEFAULT_INITIAL_DELAY_SECONDS, - constraintsConfig: Constraints = defaultConstraints, - outOfQuotaPolicy: OutOfQuotaPolicy? = defaultOutOfQuotaPolicy, - backoffPolicyConfig: BackoffPolicyTaskConfig?, - ) { - try { - val oneOffTaskRequest = - OneTimeWorkRequest - .Builder(BackgroundWorker::class.java) - .setInputData(buildTaskInputData(dartTask, isInDebugMode, payload)) - .setInitialDelay(initialDelaySeconds, TimeUnit.SECONDS) - .setConstraints(constraintsConfig) - .apply { - if (backoffPolicyConfig != null) { - setBackoffCriteria( - backoffPolicyConfig.backoffPolicy, - backoffPolicyConfig.backoffDelay, - TimeUnit.MILLISECONDS, - ) - } - }.apply { - tag?.let(::addTag) - outOfQuotaPolicy?.let(::setExpedited) - }.build() - context - .workManager() - .enqueueUniqueWork(uniqueName, existingWorkPolicy, oneOffTaskRequest) - } catch (e: Exception) { - throw e - } - } - - fun enqueuePeriodicTask( - context: Context, - uniqueName: String, - dartTask: String, - payload: Map? = null, - tag: String? = null, - frequencyInSeconds: Long = DEFAULT_PERIODIC_REFRESH_FREQUENCY_SECONDS, - flexIntervalInSeconds: Long = DEFAULT_FLEX_INTERVAL_SECONDS, - isInDebugMode: Boolean = false, - existingWorkPolicy: ExistingPeriodicWorkPolicy = defaultPeriodExistingWorkPolicy, - initialDelaySeconds: Long = DEFAULT_INITIAL_DELAY_SECONDS, - constraintsConfig: Constraints = defaultConstraints, - outOfQuotaPolicy: OutOfQuotaPolicy? = defaultOutOfQuotaPolicy, - backoffPolicyConfig: BackoffPolicyTaskConfig?, - ) { - val periodicTaskRequest = - PeriodicWorkRequest - .Builder( - BackgroundWorker::class.java, - frequencyInSeconds, - TimeUnit.SECONDS, - flexIntervalInSeconds, - TimeUnit.SECONDS, - ).setInputData(buildTaskInputData(dartTask, isInDebugMode, payload)) - .setInitialDelay(initialDelaySeconds, TimeUnit.SECONDS) - .setConstraints(constraintsConfig) - .apply { - if (backoffPolicyConfig != null) { - setBackoffCriteria( - backoffPolicyConfig.backoffPolicy, - backoffPolicyConfig.backoffDelay, - TimeUnit.MILLISECONDS, - ) - } - }.apply { - tag?.let(::addTag) - outOfQuotaPolicy?.let(::setExpedited) - }.build() - context - .workManager() - .enqueueUniquePeriodicWork(uniqueName, existingWorkPolicy, periodicTaskRequest) - } - - private fun buildTaskInputData( - dartTask: String, - isInDebugMode: Boolean, - payload: Map?, - ): Data { - val builder = - Data - .Builder() - .putString(DART_TASK_KEY, dartTask) - .putBoolean(IS_IN_DEBUG_MODE_KEY, isInDebugMode) - - // Add payload data if provided - payload?.forEach { (key, value) -> - when (value) { - is String -> builder.putString("payload_$key", value) - is Boolean -> builder.putBoolean("payload_$key", value) - is Int -> builder.putInt("payload_$key", value) - is Long -> builder.putLong("payload_$key", value) - is Float -> builder.putFloat("payload_$key", value) - is Double -> builder.putDouble("payload_$key", value) - is Array<*> -> - builder.putStringArray( - "payload_$key", - value.filterIsInstance().toTypedArray(), - ) - is List<*> -> - builder.putStringArray( - "payload_$key", - value.filterIsInstance().toTypedArray(), - ) - - is ByteArray -> builder.putByteArray("payload_$key", value) - - else -> { - throw IllegalArgumentException( - "Unsupported payload type for key '$key': ${value::class.java.simpleName}. " + - "Consider converting it to a supported type.", - ) - } - } - } - - return builder.build() - } - - fun getWorkInfoByUniqueName( - context: Context, - uniqueWorkName: String, - ) = context.workManager().getWorkInfosForUniqueWork(uniqueWorkName) - - fun cancelByUniqueName( - context: Context, - uniqueWorkName: String, - ) = context.workManager().cancelUniqueWork(uniqueWorkName) - - fun cancelByTag( - context: Context, - tag: String, - ) = context.workManager().cancelAllWorkByTag(tag) - - fun cancelAll(context: Context) = context.workManager().cancelAllWork() -} diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt index ff08f49d..4498312c 100644 --- a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt +++ b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt @@ -1,39 +1,140 @@ package dev.fluttercommunity.workmanager -import android.content.Context +import dev.fluttercommunity.workmanager.pigeon.InitializeRequest +import dev.fluttercommunity.workmanager.pigeon.OneOffTaskRequest +import dev.fluttercommunity.workmanager.pigeon.PeriodicTaskRequest +import dev.fluttercommunity.workmanager.pigeon.ProcessingTaskRequest +import dev.fluttercommunity.workmanager.pigeon.WorkmanagerHostApi import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodChannel + +private const val initRequired = + "You have not properly initialized the Flutter WorkManager Package. " + + "You should ensure you have called the 'initialize' function first!" /** - * A Flutter plugin that provides a foreground channel for workmanager operations. - * - * This implementation uses Flutter's v2 embedding API. + * Pigeon-based implementation of WorkmanagerHostApi for Android. + * Replaces the manual method channel and data extraction approach. */ -class WorkmanagerPlugin : FlutterPlugin { - private var methodChannel: MethodChannel? = null - private var workmanagerCallHandler: WorkmanagerCallHandler? = null +class WorkmanagerPlugin : FlutterPlugin, WorkmanagerHostApi { + private var workManagerWrapper: WorkManagerWrapper? = null + private lateinit var preferenceManager: SharedPreferenceHelper; + + private var currentDispatcherHandle: Long = -1L + private var isInDebugMode: Boolean = false override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) + preferenceManager = + SharedPreferenceHelper( + binding.applicationContext, + object : SharedPreferenceHelper.DispatcherHandleListener { + override fun onDispatcherHandleChanged(handle: Long) { + currentDispatcherHandle = handle + } + }) + workManagerWrapper = WorkManagerWrapper(binding.applicationContext) + WorkmanagerHostApi.setUp(binding.binaryMessenger, this) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + WorkmanagerHostApi.setUp(binding.binaryMessenger, null) + workManagerWrapper = null } - private fun onAttachedToEngine( - context: Context, - messenger: BinaryMessenger, + override fun initialize(request: InitializeRequest, callback: (Result) -> Unit) { + try { + preferenceManager.saveCallbackDispatcherHandleKey(request.callbackHandle) + isInDebugMode = request.isInDebugMode + callback(Result.success(Unit)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun registerOneOffTask(request: OneOffTaskRequest, callback: (Result) -> Unit) { + if (currentDispatcherHandle == -1L) { + callback(Result.failure(Exception(initRequired))) + return + } + + try { + workManagerWrapper!!.enqueueOneOffTask( + request = request, + isInDebugMode = isInDebugMode + ) + callback(Result.success(Unit)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun registerPeriodicTask( + request: PeriodicTaskRequest, + callback: (Result) -> Unit ) { - workmanagerCallHandler = WorkmanagerCallHandler(context) - methodChannel = MethodChannel(messenger, "dev.fluttercommunity.workmanager/foreground_channel_work_manager") - methodChannel?.setMethodCallHandler(workmanagerCallHandler) + if (currentDispatcherHandle == -1L) { + callback(Result.failure(Exception(initRequired))) + return + } + + try { + workManagerWrapper!!.enqueuePeriodicTask( + request = request, + isInDebugMode = isInDebugMode + ) + callback(Result.success(Unit)) + } catch (e: Exception) { + callback(Result.failure(e)) + } } - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onDetachedFromEngine() + override fun registerProcessingTask( + request: ProcessingTaskRequest, + callback: (Result) -> Unit + ) { + // Processing tasks are iOS-specific + callback(Result.failure(UnsupportedOperationException("Processing tasks are not supported on Android"))) + } + + override fun cancelByUniqueName(uniqueName: String, callback: (Result) -> Unit) { + try { + workManagerWrapper!!.cancelByUniqueName(uniqueName) + callback(Result.success(Unit)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun cancelByTag(tag: String, callback: (Result) -> Unit) { + try { + workManagerWrapper!!.cancelByTag(tag) + callback(Result.success(Unit)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun cancelAll(callback: (Result) -> Unit) { + try { + workManagerWrapper!!.cancelAll() + callback(Result.success(Unit)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun isScheduledByUniqueName(uniqueName: String, callback: (Result) -> Unit) { + try { + val workInfos = workManagerWrapper!!.getWorkInfoByUniqueName(uniqueName).get() + val scheduled = workInfos.isNotEmpty() && + workInfos.all { it.state == androidx.work.WorkInfo.State.ENQUEUED || it.state == androidx.work.WorkInfo.State.RUNNING } + callback(Result.success(scheduled)) + } catch (e: Exception) { + callback(Result.failure(e)) + } } - private fun onDetachedFromEngine() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - workmanagerCallHandler = null + override fun printScheduledTasks(callback: (Result) -> Unit) { + // Not supported on Android + callback(Result.failure(UnsupportedOperationException("printScheduledTasks is not supported on Android"))) } } diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt new file mode 100644 index 00000000..8460e109 --- /dev/null +++ b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt @@ -0,0 +1,686 @@ +// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved. +// // Use of this source code is governed by a MIT-style license that can be +// // found in the LICENSE file. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package dev.fluttercommunity.workmanager.pigeon + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private fun wrapResult(result: Any?): List { + return listOf(result) +} + +private fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } +} + +private fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "")} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** + * An enumeration of various network types that can be used as Constraints for work. + * + * Fully supported on Android. + * + * On iOS, this enumeration is used to define whether a piece of work requires + * internet connectivity, by checking for either [NetworkType.connected] or + * [NetworkType.metered]. + */ +enum class NetworkType(val raw: Int) { + /** Any working network connection is required for this work. */ + CONNECTED(0), + /** A metered network connection is required for this work. */ + METERED(1), + /** Default value. A network is not required for this work. */ + NOT_REQUIRED(2), + /** A non-roaming network connection is required for this work. */ + NOT_ROAMING(3), + /** An unmetered network connection is required for this work. */ + UNMETERED(4), + /** + * A temporarily unmetered Network. This capability will be set for + * networks that are generally metered, but are currently unmetered. + * + * Android API 30+ + */ + TEMPORARILY_UNMETERED(5); + + companion object { + fun ofRaw(raw: Int): NetworkType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * An enumeration of backoff policies when retrying work. + * These policies are used when you have a return ListenableWorker.Result.retry() from a worker to determine the correct backoff time. + * Backoff policies are set in WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit) or one of its variants. + */ +enum class BackoffPolicy(val raw: Int) { + /** Used to indicate that WorkManager should increase the backoff time exponentially */ + EXPONENTIAL(0), + /** Used to indicate that WorkManager should increase the backoff time linearly */ + LINEAR(1); + + companion object { + fun ofRaw(raw: Int): BackoffPolicy? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** An enumeration of the conflict resolution policies in case of a collision. */ +enum class ExistingWorkPolicy(val raw: Int) { + /** If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as a child of all the leaves of that work sequence. */ + APPEND(0), + /** If there is existing pending (uncompleted) work with the same unique name, do nothing. */ + KEEP(1), + /** If there is existing pending (uncompleted) work with the same unique name, cancel and delete it. */ + REPLACE(2), + /** + * If there is existing pending (uncompleted) work with the same unique name, it will be updated the new specification. + * Note: This maps to appendOrReplace in the native implementation. + */ + UPDATE(3); + + companion object { + fun ofRaw(raw: Int): ExistingWorkPolicy? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * An enumeration of policies that help determine out of quota behavior for expedited jobs. + * + * Only supported on Android. + */ +enum class OutOfQuotaPolicy(val raw: Int) { + /** + * When the app does not have any expedited job quota, the expedited work request will + * fallback to a regular work request. + */ + RUN_AS_NON_EXPEDITED_WORK_REQUEST(0), + /** + * When the app does not have any expedited job quota, the expedited work request will + * we dropped and no work requests are enqueued. + */ + DROP_WORK_REQUEST(1); + + companion object { + fun ofRaw(raw: Int): OutOfQuotaPolicy? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class Constraints ( + val networkType: NetworkType? = null, + val requiresBatteryNotLow: Boolean? = null, + val requiresCharging: Boolean? = null, + val requiresDeviceIdle: Boolean? = null, + val requiresStorageNotLow: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): Constraints { + val networkType = pigeonVar_list[0] as NetworkType? + val requiresBatteryNotLow = pigeonVar_list[1] as Boolean? + val requiresCharging = pigeonVar_list[2] as Boolean? + val requiresDeviceIdle = pigeonVar_list[3] as Boolean? + val requiresStorageNotLow = pigeonVar_list[4] as Boolean? + return Constraints(networkType, requiresBatteryNotLow, requiresCharging, requiresDeviceIdle, requiresStorageNotLow) + } + } + fun toList(): List { + return listOf( + networkType, + requiresBatteryNotLow, + requiresCharging, + requiresDeviceIdle, + requiresStorageNotLow, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class BackoffPolicyConfig ( + val backoffPolicy: BackoffPolicy? = null, + val backoffDelayMillis: Long? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): BackoffPolicyConfig { + val backoffPolicy = pigeonVar_list[0] as BackoffPolicy? + val backoffDelayMillis = pigeonVar_list[1] as Long? + return BackoffPolicyConfig(backoffPolicy, backoffDelayMillis) + } + } + fun toList(): List { + return listOf( + backoffPolicy, + backoffDelayMillis, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class InitializeRequest ( + val callbackHandle: Long, + val isInDebugMode: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): InitializeRequest { + val callbackHandle = pigeonVar_list[0] as Long + val isInDebugMode = pigeonVar_list[1] as Boolean + return InitializeRequest(callbackHandle, isInDebugMode) + } + } + fun toList(): List { + return listOf( + callbackHandle, + isInDebugMode, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class OneOffTaskRequest ( + val uniqueName: String, + val taskName: String, + val inputData: Map? = null, + val initialDelaySeconds: Long? = null, + val constraints: Constraints? = null, + val backoffPolicy: BackoffPolicyConfig? = null, + val tag: String? = null, + val existingWorkPolicy: ExistingWorkPolicy? = null, + val outOfQuotaPolicy: OutOfQuotaPolicy? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): OneOffTaskRequest { + val uniqueName = pigeonVar_list[0] as String + val taskName = pigeonVar_list[1] as String + val inputData = pigeonVar_list[2] as Map? + val initialDelaySeconds = pigeonVar_list[3] as Long? + val constraints = pigeonVar_list[4] as Constraints? + val backoffPolicy = pigeonVar_list[5] as BackoffPolicyConfig? + val tag = pigeonVar_list[6] as String? + val existingWorkPolicy = pigeonVar_list[7] as ExistingWorkPolicy? + val outOfQuotaPolicy = pigeonVar_list[8] as OutOfQuotaPolicy? + return OneOffTaskRequest(uniqueName, taskName, inputData, initialDelaySeconds, constraints, backoffPolicy, tag, existingWorkPolicy, outOfQuotaPolicy) + } + } + fun toList(): List { + return listOf( + uniqueName, + taskName, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + outOfQuotaPolicy, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PeriodicTaskRequest ( + val uniqueName: String, + val taskName: String, + val frequencySeconds: Long, + val flexIntervalSeconds: Long? = null, + val inputData: Map? = null, + val initialDelaySeconds: Long? = null, + val constraints: Constraints? = null, + val backoffPolicy: BackoffPolicyConfig? = null, + val tag: String? = null, + val existingWorkPolicy: ExistingWorkPolicy? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): PeriodicTaskRequest { + val uniqueName = pigeonVar_list[0] as String + val taskName = pigeonVar_list[1] as String + val frequencySeconds = pigeonVar_list[2] as Long + val flexIntervalSeconds = pigeonVar_list[3] as Long? + val inputData = pigeonVar_list[4] as Map? + val initialDelaySeconds = pigeonVar_list[5] as Long? + val constraints = pigeonVar_list[6] as Constraints? + val backoffPolicy = pigeonVar_list[7] as BackoffPolicyConfig? + val tag = pigeonVar_list[8] as String? + val existingWorkPolicy = pigeonVar_list[9] as ExistingWorkPolicy? + return PeriodicTaskRequest(uniqueName, taskName, frequencySeconds, flexIntervalSeconds, inputData, initialDelaySeconds, constraints, backoffPolicy, tag, existingWorkPolicy) + } + } + fun toList(): List { + return listOf( + uniqueName, + taskName, + frequencySeconds, + flexIntervalSeconds, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class ProcessingTaskRequest ( + val uniqueName: String, + val taskName: String, + val inputData: Map? = null, + val initialDelaySeconds: Long? = null, + val networkType: NetworkType? = null, + val requiresCharging: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): ProcessingTaskRequest { + val uniqueName = pigeonVar_list[0] as String + val taskName = pigeonVar_list[1] as String + val inputData = pigeonVar_list[2] as Map? + val initialDelaySeconds = pigeonVar_list[3] as Long? + val networkType = pigeonVar_list[4] as NetworkType? + val requiresCharging = pigeonVar_list[5] as Boolean? + return ProcessingTaskRequest(uniqueName, taskName, inputData, initialDelaySeconds, networkType, requiresCharging) + } + } + fun toList(): List { + return listOf( + uniqueName, + taskName, + inputData, + initialDelaySeconds, + networkType, + requiresCharging, + ) + } +} +private open class WorkmanagerApiPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + NetworkType.ofRaw(it.toInt()) + } + } + 130.toByte() -> { + return (readValue(buffer) as Long?)?.let { + BackoffPolicy.ofRaw(it.toInt()) + } + } + 131.toByte() -> { + return (readValue(buffer) as Long?)?.let { + ExistingWorkPolicy.ofRaw(it.toInt()) + } + } + 132.toByte() -> { + return (readValue(buffer) as Long?)?.let { + OutOfQuotaPolicy.ofRaw(it.toInt()) + } + } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { + Constraints.fromList(it) + } + } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + BackoffPolicyConfig.fromList(it) + } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { + InitializeRequest.fromList(it) + } + } + 136.toByte() -> { + return (readValue(buffer) as? List)?.let { + OneOffTaskRequest.fromList(it) + } + } + 137.toByte() -> { + return (readValue(buffer) as? List)?.let { + PeriodicTaskRequest.fromList(it) + } + } + 138.toByte() -> { + return (readValue(buffer) as? List)?.let { + ProcessingTaskRequest.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is NetworkType -> { + stream.write(129) + writeValue(stream, value.raw) + } + is BackoffPolicy -> { + stream.write(130) + writeValue(stream, value.raw) + } + is ExistingWorkPolicy -> { + stream.write(131) + writeValue(stream, value.raw) + } + is OutOfQuotaPolicy -> { + stream.write(132) + writeValue(stream, value.raw) + } + is Constraints -> { + stream.write(133) + writeValue(stream, value.toList()) + } + is BackoffPolicyConfig -> { + stream.write(134) + writeValue(stream, value.toList()) + } + is InitializeRequest -> { + stream.write(135) + writeValue(stream, value.toList()) + } + is OneOffTaskRequest -> { + stream.write(136) + writeValue(stream, value.toList()) + } + is PeriodicTaskRequest -> { + stream.write(137) + writeValue(stream, value.toList()) + } + is ProcessingTaskRequest -> { + stream.write(138) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface WorkmanagerHostApi { + fun initialize(request: InitializeRequest, callback: (Result) -> Unit) + fun registerOneOffTask(request: OneOffTaskRequest, callback: (Result) -> Unit) + fun registerPeriodicTask(request: PeriodicTaskRequest, callback: (Result) -> Unit) + fun registerProcessingTask(request: ProcessingTaskRequest, callback: (Result) -> Unit) + fun cancelByUniqueName(uniqueName: String, callback: (Result) -> Unit) + fun cancelByTag(tag: String, callback: (Result) -> Unit) + fun cancelAll(callback: (Result) -> Unit) + fun isScheduledByUniqueName(uniqueName: String, callback: (Result) -> Unit) + fun printScheduledTasks(callback: (Result) -> Unit) + + companion object { + /** The codec used by WorkmanagerHostApi. */ + val codec: MessageCodec by lazy { + WorkmanagerApiPigeonCodec() + } + /** Sets up an instance of `WorkmanagerHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: WorkmanagerHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.initialize$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as InitializeRequest + api.initialize(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerOneOffTask$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as OneOffTaskRequest + api.registerOneOffTask(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerPeriodicTask$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as PeriodicTaskRequest + api.registerPeriodicTask(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerProcessingTask$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as ProcessingTaskRequest + api.registerProcessingTask(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByUniqueName$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val uniqueNameArg = args[0] as String + api.cancelByUniqueName(uniqueNameArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByTag$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tagArg = args[0] as String + api.cancelByTag(tagArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelAll$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.cancelAll{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.isScheduledByUniqueName$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val uniqueNameArg = args[0] as String + api.isScheduledByUniqueName(uniqueNameArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.printScheduledTasks$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.printScheduledTasks{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class WorkmanagerFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by WorkmanagerFlutterApi. */ + val codec: MessageCodec by lazy { + WorkmanagerApiPigeonCodec() + } + } + fun backgroundChannelInitialized(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.backgroundChannelInitialized$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun executeTask(taskNameArg: String, inputDataArg: Map?, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.executeTask$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(taskNameArg, inputDataArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else if (it[0] == null) { + callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", ""))) + } else { + val output = it[0] as Boolean + callback(Result.success(output)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } +} diff --git a/workmanager_android/lib/workmanager_android.dart b/workmanager_android/lib/workmanager_android.dart index bdafbfe2..6ffe4d4f 100644 --- a/workmanager_android/lib/workmanager_android.dart +++ b/workmanager_android/lib/workmanager_android.dart @@ -1,13 +1,10 @@ import 'dart:ui'; -import 'package:flutter/services.dart'; import 'package:workmanager_platform_interface/workmanager_platform_interface.dart'; /// Android implementation of [WorkmanagerPlatform]. class WorkmanagerAndroid extends WorkmanagerPlatform { - /// The method channel used to interact with the native platform. - static const MethodChannel _channel = MethodChannel( - 'dev.fluttercommunity.workmanager/foreground_channel_work_manager', - ); + /// The Pigeon API instance for type-safe communication. + final WorkmanagerHostApi _api = WorkmanagerHostApi(); /// Constructs an AndroidWorkmanager. WorkmanagerAndroid() : super(); @@ -23,10 +20,10 @@ class WorkmanagerAndroid extends WorkmanagerPlatform { bool isInDebugMode = false, }) async { final callback = PluginUtilities.getCallbackHandle(callbackDispatcher); - await _channel.invokeMethod('initialize', { - 'callbackHandle': callback!.toRawHandle(), - 'isInDebugMode': isInDebugMode, - }); + await _api.initialize(InitializeRequest( + callbackHandle: callback!.toRawHandle(), + isInDebugMode: isInDebugMode, + )); } @override @@ -42,22 +39,22 @@ class WorkmanagerAndroid extends WorkmanagerPlatform { String? tag, OutOfQuotaPolicy? outOfQuotaPolicy, }) async { - await _channel.invokeMethod('registerOneOffTask', { - 'uniqueName': uniqueName, - 'taskName': taskName, - 'inputData': inputData, - 'initialDelaySeconds': initialDelay?.inSeconds, - 'networkType': constraints?.networkType.name, - 'requiresBatteryNotLow': constraints?.requiresBatteryNotLow, - 'requiresCharging': constraints?.requiresCharging, - 'requiresDeviceIdle': constraints?.requiresDeviceIdle, - 'requiresStorageNotLow': constraints?.requiresStorageNotLow, - 'existingWorkPolicy': existingWorkPolicy?.name, - 'backoffPolicy': backoffPolicy?.name, - 'backoffDelayInMilliseconds': backoffPolicyDelay?.inMilliseconds, - 'tag': tag, - 'outOfQuotaPolicy': outOfQuotaPolicy?.name, - }); + await _api.registerOneOffTask(OneOffTaskRequest( + uniqueName: uniqueName, + taskName: taskName, + inputData: inputData?.cast(), + initialDelaySeconds: initialDelay?.inSeconds, + constraints: constraints, + existingWorkPolicy: existingWorkPolicy, + backoffPolicy: backoffPolicyDelay != null && backoffPolicy != null + ? BackoffPolicyConfig( + backoffPolicy: backoffPolicy, + backoffDelayMillis: backoffPolicyDelay.inMilliseconds, + ) + : null, + tag: tag, + outOfQuotaPolicy: outOfQuotaPolicy, + )); } @override @@ -74,23 +71,23 @@ class WorkmanagerAndroid extends WorkmanagerPlatform { Duration? backoffPolicyDelay, String? tag, }) async { - await _channel.invokeMethod('registerPeriodicTask', { - 'uniqueName': uniqueName, - 'taskName': taskName, - 'inputData': inputData, - 'frequencySeconds': frequency?.inSeconds, - 'flexIntervalSeconds': flexInterval?.inSeconds, - 'initialDelaySeconds': initialDelay?.inSeconds, - 'networkType': constraints?.networkType.name, - 'requiresBatteryNotLow': constraints?.requiresBatteryNotLow, - 'requiresCharging': constraints?.requiresCharging, - 'requiresDeviceIdle': constraints?.requiresDeviceIdle, - 'requiresStorageNotLow': constraints?.requiresStorageNotLow, - 'existingWorkPolicy': existingWorkPolicy?.name, - 'backoffPolicy': backoffPolicy?.name, - 'backoffDelayInMilliseconds': backoffPolicyDelay?.inMilliseconds, - 'tag': tag, - }); + await _api.registerPeriodicTask(PeriodicTaskRequest( + uniqueName: uniqueName, + taskName: taskName, + frequencySeconds: frequency?.inSeconds ?? 900, // Default 15 minutes + flexIntervalSeconds: flexInterval?.inSeconds, + inputData: inputData?.cast(), + initialDelaySeconds: initialDelay?.inSeconds, + constraints: constraints, + existingWorkPolicy: existingWorkPolicy, + backoffPolicy: backoffPolicyDelay != null && backoffPolicy != null + ? BackoffPolicyConfig( + backoffPolicy: backoffPolicy, + backoffDelayMillis: backoffPolicyDelay.inMilliseconds, + ) + : null, + tag: tag, + )); } @override @@ -107,30 +104,22 @@ class WorkmanagerAndroid extends WorkmanagerPlatform { @override Future cancelByUniqueName(String uniqueName) async { - await _channel.invokeMethod('cancelTaskByUniqueName', { - 'uniqueName': uniqueName, - }); + await _api.cancelByUniqueName(uniqueName); } @override Future cancelByTag(String tag) async { - await _channel.invokeMethod('cancelTaskByTag', { - 'tag': tag, - }); + await _api.cancelByTag(tag); } @override Future cancelAll() async { - await _channel.invokeMethod('cancelAllTasks'); + await _api.cancelAll(); } @override Future isScheduledByUniqueName(String uniqueName) async { - final result = - await _channel.invokeMethod('isScheduledByUniqueName', { - 'uniqueName': uniqueName, - }); - return result ?? false; + return await _api.isScheduledByUniqueName(uniqueName); } @override diff --git a/workmanager_apple/ios/Classes/BackgroundTaskOperation.swift b/workmanager_apple/ios/Classes/BackgroundTaskOperation.swift index 3c6828d8..0abfcb67 100644 --- a/workmanager_apple/ios/Classes/BackgroundTaskOperation.swift +++ b/workmanager_apple/ios/Classes/BackgroundTaskOperation.swift @@ -7,7 +7,15 @@ import Foundation -class BackgroundTaskOperation: Operation { +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + +class BackgroundTaskOperation: Operation, @unchecked Sendable { private let identifier: String private let flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? diff --git a/workmanager_apple/ios/Classes/BackgroundWorker.swift b/workmanager_apple/ios/Classes/BackgroundWorker.swift index fa6a7dea..57f20c26 100644 --- a/workmanager_apple/ios/Classes/BackgroundWorker.swift +++ b/workmanager_apple/ios/Classes/BackgroundWorker.swift @@ -7,6 +7,14 @@ import Foundation +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + enum BackgroundMode { case backgroundFetch case backgroundProcessingTask(identifier: String) @@ -16,26 +24,26 @@ enum BackgroundMode { var flutterThreadlabelPrefix: String { switch self { case .backgroundFetch: - return "\(SwiftWorkmanagerPlugin.identifier).BackgroundFetch" + return "\(WorkmanagerPlugin.identifier).BackgroundFetch" case .backgroundProcessingTask: - return "\(SwiftWorkmanagerPlugin.identifier).BackgroundProcessingTask" + return "\(WorkmanagerPlugin.identifier).BackgroundProcessingTask" case .backgroundPeriodicTask: - return "\(SwiftWorkmanagerPlugin.identifier).BackgroundPeriodicTask" + return "\(WorkmanagerPlugin.identifier).BackgroundPeriodicTask" case .backgroundOneOffTask: - return "\(SwiftWorkmanagerPlugin.identifier).OneOffTask" + return "\(WorkmanagerPlugin.identifier).OneOffTask" } } var onResultSendArguments: [String: String] { switch self { case .backgroundFetch: - return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSPerformFetch"] + return ["\(WorkmanagerPlugin.identifier).DART_TASK": "iOSPerformFetch"] case let .backgroundProcessingTask(identifier): - return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] + return ["\(WorkmanagerPlugin.identifier).DART_TASK": identifier] case let .backgroundPeriodicTask(identifier): - return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] + return ["\(WorkmanagerPlugin.identifier).DART_TASK": identifier] case let .backgroundOneOffTask(identifier): - return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] + return ["\(WorkmanagerPlugin.identifier).DART_TASK": identifier] } } } @@ -56,7 +64,7 @@ class BackgroundWorker { } private struct BackgroundChannel { - static let name = "\(SwiftWorkmanagerPlugin.identifier)/background_channel_work_manager" + static let name = "\(WorkmanagerPlugin.identifier)/background_channel_work_manager" static let initialized = "backgroundChannelInitialized" static let onResultSendCommand = "onResultSend" } diff --git a/workmanager_apple/ios/Classes/DebugNotificationHelper.swift b/workmanager_apple/ios/Classes/DebugNotificationHelper.swift index 66ea0a9a..9e1b223e 100644 --- a/workmanager_apple/ios/Classes/DebugNotificationHelper.swift +++ b/workmanager_apple/ios/Classes/DebugNotificationHelper.swift @@ -8,6 +8,14 @@ import Foundation import UserNotifications +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + class DebugNotificationHelper { private let identifier: UUID @@ -63,7 +71,7 @@ class DebugNotificationHelper { UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) { (_, _) in } let notificationRequest = createNotificationRequest( identifier: identifier, - threadIdentifier: SwiftWorkmanagerPlugin.identifier, + threadIdentifier: WorkmanagerPlugin.identifier, title: title, body: body, icon: icon @@ -95,7 +103,7 @@ class DebugNotificationHelper { } private static var logPrefix: String { - return "\(String(describing: SwiftWorkmanagerPlugin.self)) - \(DebugNotificationHelper.self)" + return "\(String(describing: WorkmanagerPlugin.self)) - \(DebugNotificationHelper.self)" } } diff --git a/workmanager_apple/ios/Classes/NetworkType.swift b/workmanager_apple/ios/Classes/NetworkType.swift deleted file mode 100644 index f2f7d552..00000000 --- a/workmanager_apple/ios/Classes/NetworkType.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// NetworkType.swift -// workmanager -// -// Created by Sebastian Roth on 10/06/2021. -// - -import Foundation - -/// An enumeration of various network types that can be used as Constraints for work. -enum NetworkType: String { - /// Any working network connection is required for this work. - case connected - - /// A metered network connection is required for this work. - case metered - - /// Default value. A network is not required for this work. - case notRequired - - /// A non-roaming network connection is required for this work. - case notRoaming - - /// An unmetered network connection is required for this work. - case unmetered - - /// A temporarily unmetered Network. This capability will be set for - /// networks that are generally metered, but are currently unmetered. - /// - /// Only applies to Android. - case temporarilyUnmetered - - /// Convenience constructor to build a [NetworkType] from a Dart enum. - init?(fromDart: String) { - self.init(rawValue: fromDart.camelCased(with: "_")) - } -} - -private extension String { - func camelCased(with separator: Character) -> String { - return self.lowercased() - .split(separator: separator) - .enumerated() - .map { $0.offset > 0 ? $0.element.capitalized : $0.element.lowercased() } - .joined() - } -} diff --git a/workmanager_apple/ios/Classes/SwiftWorkmanagerPlugin.swift b/workmanager_apple/ios/Classes/SwiftWorkmanagerPlugin.swift deleted file mode 100644 index f9fb6b61..00000000 --- a/workmanager_apple/ios/Classes/SwiftWorkmanagerPlugin.swift +++ /dev/null @@ -1,494 +0,0 @@ -import BackgroundTasks -import Flutter -import UIKit -import os - -extension String { - var lowercasingFirst: String { - return prefix(1).lowercased() + dropFirst() - } -} - -public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { - static let identifier = "dev.fluttercommunity.workmanager" - - private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - - private struct ForegroundMethodChannel { - static let channelName = "\(SwiftWorkmanagerPlugin.identifier)/foreground_channel_work_manager" - - struct Methods { - struct Initialize { - static let name = "\(Initialize.self)".lowercasingFirst - enum Arguments: String { - case isInDebugMode - case callbackHandle - } - } - - struct RegisterOneOffTask { - static let name = "\(RegisterOneOffTask.self)".lowercasingFirst - enum Arguments: String { - case taskName - case uniqueName - case initialDelaySeconds - case inputData - } - } - - struct RegisterProcessingTask { - static let name = "\(RegisterProcessingTask.self)".lowercasingFirst - enum Arguments: String { - case taskName - case uniqueName - case initialDelaySeconds - case networkType - case requiresCharging - } - } - - struct RegisterPeriodicTask { - static let name = "\(RegisterPeriodicTask.self)".lowercasingFirst - enum Arguments: String { - case taskName - case uniqueName - case initialDelaySeconds - case inputData - } - } - - struct CancelAllTasks { - static let name = "\(CancelAllTasks.self)".lowercasingFirst - enum Arguments: String { - case none - } - } - - struct CancelTaskByUniqueName { - static let name = "\(CancelTaskByUniqueName.self)".lowercasingFirst - enum Arguments: String { - case uniqueName - } - } - - struct PrintScheduledTasks { - static let name = "\(PrintScheduledTasks.self)".lowercasingFirst - enum Arguments: String { - case none - } - } - } - } - - @available(iOS 13.0, *) - private static func handleBGProcessingTask(identifier: String, task: BGProcessingTask) { - let operationQueue = OperationQueue() - - // Create an operation that performs the main part of the background task - let operation = BackgroundTaskOperation( - task.identifier, - inputData: nil, - flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundProcessingTask(identifier: identifier) - ) - - // Provide an expiration handler for the background task - // that cancels the operation - task.expirationHandler = { - operation.cancel() - } - - // Inform the system that the background task is complete - // when the operation completes - operation.completionBlock = { - task.setTaskCompleted(success: !operation.isCancelled) - } - - // Start the operation - operationQueue.addOperation(operation) - } - - @available(iOS 13.0, *) - public static func handlePeriodicTask(identifier: String, task: BGAppRefreshTask, earliestBeginInSeconds: Double?) { - guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), - let _ = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) - else { - logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") - return - } - - // If frequency is not provided it will default to 15 minutes - schedulePeriodicTask(taskIdentifier: task.identifier, earliestBeginInSeconds: earliestBeginInSeconds ?? (15 * 60)) - - let operationQueue = OperationQueue() - // Create an operation that performs the main part of the background task - let operation = BackgroundTaskOperation( - task.identifier, - inputData: nil, - flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundPeriodicTask(identifier: identifier) - ) - - // Provide an expiration handler for the background task that cancels the operation - task.expirationHandler = { - operation.cancel() - } - - // Inform the system that the background task is complete when the operation completes - operation.completionBlock = { - task.setTaskCompleted(success: !operation.isCancelled) - } - - // Start the operation - operationQueue.addOperation(operation) - } - - /// Immediately starts a one off task - @available(iOS 13.0, *) - public static func startOneOffTask(identifier: String, taskIdentifier: UIBackgroundTaskIdentifier, inputData: [String: Any]?, delaySeconds: Int64) { - let operationQueue = OperationQueue() - // Create an operation that performs the main part of the background task - let operation = BackgroundTaskOperation( - identifier, - inputData: inputData, - flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundOneOffTask(identifier: identifier) - ) - - // Inform the system that the task is complete when the operation completes - operation.completionBlock = { - UIApplication.shared.endBackgroundTask(taskIdentifier) - } - - // Start the operation - operationQueue.addOperation(operation) - } - - /// Registers [BGAppRefresh] task name for the given identifier. - /// You must register task names before app finishes launching in AppDelegate. - @objc - public static func registerPeriodicTask(withIdentifier identifier: String, frequency: NSNumber?) { - if #available(iOS 13.0, *) { - var frequencyInSeconds: Double? - if let frequencyValue = frequency { - frequencyInSeconds = frequencyValue.doubleValue - } - - BGTaskScheduler.shared.register( - forTaskWithIdentifier: identifier, - using: nil - ) { task in - if let task = task as? BGAppRefreshTask { - handlePeriodicTask(identifier: identifier, task: task, earliestBeginInSeconds: frequencyInSeconds) - } - } - } - } - - @objc - @available(iOS 13.0, *) - private static func schedulePeriodicTask(taskIdentifier identifier: String, earliestBeginInSeconds begin: Double) { - if #available(iOS 13.0, *) { - let request = BGAppRefreshTaskRequest(identifier: identifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: begin) - do { - try BGTaskScheduler.shared.submit(request) - logInfo("BGAppRefreshTask submitted \(identifier) earliestBeginInSeconds:\(begin)") - } catch { - logInfo("Could not schedule BGAppRefreshTask \(error.localizedDescription)") - return - } - } - } - - /// Registers [BGProcessingTask] task name for the given identifier. - /// Task names must be registered before app finishes launching in AppDelegate. - @objc - public static func registerBGProcessingTask(withIdentifier identifier: String) { - if #available(iOS 13.0, *) { - BGTaskScheduler.shared.register( - forTaskWithIdentifier: identifier, - using: nil - ) { task in - if let task = task as? BGProcessingTask { - handleBGProcessingTask(identifier: identifier, task: task) - } - } - } - } - - /// Schedules a long running BackgroundProcessingTask - @objc - @available(iOS 13.0, *) - private static func scheduleBackgroundProcessingTask( - withIdentifier uniqueTaskIdentifier: String, - earliestBeginInSeconds begin: Double, - requiresNetworkConnectivity: Bool, - requiresExternalPower: Bool - ) { - let request = BGProcessingTaskRequest(identifier: uniqueTaskIdentifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: begin) - request.requiresNetworkConnectivity = requiresNetworkConnectivity - request.requiresExternalPower = requiresExternalPower - do { - try BGTaskScheduler.shared.submit(request) - logInfo("BGProcessingTask submitted \(uniqueTaskIdentifier) earliestBeginInSeconds:\(begin)") - } catch { - logInfo("Could not schedule BGProcessingTask identifier:\(uniqueTaskIdentifier) error:\(error.localizedDescription)") - logInfo("Possible issues can be: running on a simulator instead of a real device, or the task name is not registered") - } - } - - static func callback(_: UIBackgroundFetchResult) { - } -} - -// MARK: - FlutterPlugin conformance - -extension SwiftWorkmanagerPlugin: FlutterPlugin { - - @objc - public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { - flutterPluginRegistrantCallback = callback - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let foregroundMethodChannel = FlutterMethodChannel( - name: ForegroundMethodChannel.channelName, - binaryMessenger: registrar.messenger() - ) - let instance = SwiftWorkmanagerPlugin() - registrar.addMethodCallDelegate(instance, channel: foregroundMethodChannel) - registrar.addApplicationDelegate(instance) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - - switch (call.method, call.arguments as? [AnyHashable: Any]) { - case (ForegroundMethodChannel.Methods.Initialize.name, let .some(arguments)): - initialize(arguments: arguments, result: result) - return - case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): - registerOneOffTask(arguments: arguments, result: result) - return - case (ForegroundMethodChannel.Methods.RegisterPeriodicTask.name, let .some(arguments)): - registerPeriodicTask(arguments: arguments, result: result) - return - case (ForegroundMethodChannel.Methods.RegisterProcessingTask.name, let .some(arguments)): - registerProcessingTask(arguments: arguments, result: result) - return - case (ForegroundMethodChannel.Methods.CancelAllTasks.name, .none): - cancelAllTasks(result: result) - return - case (ForegroundMethodChannel.Methods.CancelTaskByUniqueName.name, let .some(arguments)): - cancelTaskByUniqueName(arguments: arguments, result: result) - return - case (ForegroundMethodChannel.Methods.PrintScheduledTasks.name, .none): - printScheduledTasks(result: result) - return - default: - result(WMPError.unhandledMethod(call.method).asFlutterError) - return - } - } - - private func initialize(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - let method = ForegroundMethodChannel.Methods.Initialize.self - guard let isInDebug = arguments[method.Arguments.isInDebugMode.rawValue] as? Bool, - let handle = arguments[method.Arguments.callbackHandle.rawValue] as? Int64 else { - result(WMPError.invalidParameters.asFlutterError) - return - } - UserDefaultsHelper.storeCallbackHandle(handle) - UserDefaultsHelper.storeIsDebug(isInDebug) - result(true) - } - - private func registerOneOffTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - if !validateCallbackHandle(result: result) { - return - } - - if #available(iOS 13.0, *) { - let method = ForegroundMethodChannel.Methods.RegisterOneOffTask.self - let delaySeconds = arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 ?? 0 - guard let uniqueTaskIdentifier = - arguments[method.Arguments.uniqueName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } - - var taskIdentifier: UIBackgroundTaskIdentifier = .invalid - // Extract inputData as native Map - let inputDataMap = arguments[method.Arguments.inputData.rawValue] as? [String: Any] - - taskIdentifier = UIApplication.shared.beginBackgroundTask(withName: uniqueTaskIdentifier, expirationHandler: { - // Mark the task as ended if time is expired, otherwise iOS might terminate and will throttle future executions - UIApplication.shared.endBackgroundTask(taskIdentifier) - }) - SwiftWorkmanagerPlugin.startOneOffTask(identifier: uniqueTaskIdentifier, - taskIdentifier: taskIdentifier, - inputData: inputDataMap, - delaySeconds: delaySeconds) - result(true) - return - } else { - result(FlutterError(code: "99", - message: "OneOffTask could not be registered", - details: "BGTaskScheduler tasks are only supported on iOS 13+")) - } - } - - private func registerPeriodicTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - if !validateCallbackHandle(result: result) { - return - } - - if #available(iOS 13.0, *) { - let method = ForegroundMethodChannel.Methods.RegisterPeriodicTask.self - guard let uniqueTaskIdentifier = - arguments[method.Arguments.uniqueName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } - let initialDelaySeconds = - arguments[method.Arguments.initialDelaySeconds.rawValue] as? Double ?? 0.0 - - SwiftWorkmanagerPlugin.schedulePeriodicTask( - taskIdentifier: uniqueTaskIdentifier, - earliestBeginInSeconds: initialDelaySeconds) - result(true) - return - } else { - result(FlutterError(code: "99", - message: "PeriodicTask could not be registered", - details: "BGAppRefreshTasks are only supported on iOS 13+. Instead you should use Background Fetch")) - } - } - - private func registerProcessingTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - if !validateCallbackHandle(result: result) { - return - } - - if #available(iOS 13.0, *) { - let method = ForegroundMethodChannel.Methods.RegisterProcessingTask.self - guard let uniqueTaskIdentifier = - arguments[method.Arguments.uniqueName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } - let delaySeconds = - arguments[method.Arguments.initialDelaySeconds.rawValue] as? Double ?? 0.0 - - let requiresCharging = arguments[method.Arguments.requiresCharging.rawValue] as? Bool ?? false - var requiresNetwork = false - if let networkTypeInput = arguments[method.Arguments.networkType.rawValue] as? String, - let networkType = NetworkType(fromDart: networkTypeInput), - networkType == .connected || networkType == .metered { - requiresNetwork = true - } - - SwiftWorkmanagerPlugin.scheduleBackgroundProcessingTask( - withIdentifier: uniqueTaskIdentifier, - earliestBeginInSeconds: delaySeconds, - requiresNetworkConnectivity: requiresNetwork, - requiresExternalPower: requiresCharging) - - result(true) - return - } else { - result(FlutterError(code: "99", - message: "BackgroundProcessingTask could not be registered", - details: "BGProcessingTasks are only supported on iOS 13+")) - } - } - - private func cancelAllTasks(result: @escaping FlutterResult) { - if #available(iOS 13.0, *) { - BGTaskScheduler.shared.cancelAllTaskRequests() - } - result(true) - } - - private func cancelTaskByUniqueName(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - if #available(iOS 13.0, *) { - let method = ForegroundMethodChannel.Methods.CancelTaskByUniqueName.self - guard let identifier = arguments[method.Arguments.uniqueName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier) - } - result(true) - } - - /// Checks whether getStoredCallbackHandle is set. - /// Returns true when initialized, if false, result contains error message. - private func validateCallbackHandle(result: @escaping FlutterResult) -> Bool { - if UserDefaultsHelper.getStoredCallbackHandle() == nil { - result( - FlutterError( - code: "1", - message: "You have not properly initialized the Flutter WorkManager Package. " + - "You should ensure you have called the 'initialize' function first! " + - "Example: \n" + - "\n" + - "`Workmanager().initialize(\n" + - " callbackDispatcher,\n" + - " )`" + - "\n" + - "\n" + - "The `callbackDispatcher` is a top level function. See example in repository.", - details: nil - ) - ) - return false - } - return true - } - - /// Prints details of un-executed scheduled tasks. To be used during development/debugging - private func printScheduledTasks(result: @escaping FlutterResult) { - if #available(iOS 13.0, *) { - BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in - if taskRequests.isEmpty { - let message = "[BGTaskScheduler] There are no scheduled tasks" - os_log("%{public}@", log: OSLog.default, type: .debug, message) - result(message) - return - } - var message = "[BGTaskScheduler] Scheduled Tasks:" - for taskRequest in taskRequests { - message += "\n[BGTaskScheduler] Task Identifier: \(taskRequest.identifier) earliestBeginDate: \(taskRequest.earliestBeginDate?.formatted() ?? "")" - } - os_log("%{public}@", log: OSLog.default, type: .debug, message) - result(message) - } - } else { - result(FlutterError(code: "99", - message: "printScheduledTasks is only supported on iOS 13+", - details: "BGTaskScheduler.getPendingTaskRequests is only supported on iOS 13+")) - } - } -} - -// MARK: - AppDelegate conformance - -extension SwiftWorkmanagerPlugin { - - override public func application( - _ application: UIApplication, - performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) -> Bool { - // Old background fetch API for iOS 12 and lower, in theory it should work for iOS 13+ as well - let worker = BackgroundWorker( - mode: .backgroundFetch, - inputData: nil, - flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback - ) - - return worker.performBackgroundRequest(completionHandler) - } - -} diff --git a/workmanager_apple/ios/Classes/ThumbnailGenerator.swift b/workmanager_apple/ios/Classes/ThumbnailGenerator.swift index a6c64a40..28874791 100644 --- a/workmanager_apple/ios/Classes/ThumbnailGenerator.swift +++ b/workmanager_apple/ios/Classes/ThumbnailGenerator.swift @@ -43,7 +43,7 @@ struct ThumbnailGenerator { let thumbnailImage = try thumbnail.renderAsImage() let localURL = try thumbnailImage.persist(fileName: name) return try UNNotificationAttachment( - identifier: "\(SwiftWorkmanagerPlugin.identifier).\(name)", + identifier: "\(WorkmanagerPlugin.identifier).\(name)", url: localURL, options: nil ) @@ -55,7 +55,7 @@ struct ThumbnailGenerator { } private static var logPrefix: String { - return "\(String(describing: SwiftWorkmanagerPlugin.self)) - \(ThumbnailGenerator.self)" + return "\(String(describing: WorkmanagerPlugin.self)) - \(ThumbnailGenerator.self)" } } @@ -85,7 +85,7 @@ private extension UIView { private extension UIImage { func persist(fileName: String, in directory: URL = URL(fileURLWithPath: NSTemporaryDirectory())) throws -> URL { - let directoryURL = directory.appendingPathComponent(SwiftWorkmanagerPlugin.identifier, isDirectory: true) + let directoryURL = directory.appendingPathComponent(WorkmanagerPlugin.identifier, isDirectory: true) let fileURL = directoryURL.appendingPathComponent("\(fileName).png") try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) guard let imageData = self.pngData() else { diff --git a/workmanager_apple/ios/Classes/UserDefaultsHelper.swift b/workmanager_apple/ios/Classes/UserDefaultsHelper.swift index 562b0930..6437ae14 100644 --- a/workmanager_apple/ios/Classes/UserDefaultsHelper.swift +++ b/workmanager_apple/ios/Classes/UserDefaultsHelper.swift @@ -11,14 +11,14 @@ struct UserDefaultsHelper { // MARK: Properties - private static let userDefaults = UserDefaults(suiteName: "\(SwiftWorkmanagerPlugin.identifier).userDefaults")! + private static let userDefaults = UserDefaults(suiteName: "\(WorkmanagerPlugin.identifier).userDefaults")! enum Key { case callbackHandle case isDebug var stringValue: String { - return "\(SwiftWorkmanagerPlugin.identifier).\(self)" + return "\(WorkmanagerPlugin.identifier).\(self)" } } diff --git a/workmanager_apple/ios/Classes/WMPError.swift b/workmanager_apple/ios/Classes/WMPError.swift index 7bc2c926..1be0c1da 100644 --- a/workmanager_apple/ios/Classes/WMPError.swift +++ b/workmanager_apple/ios/Classes/WMPError.swift @@ -7,6 +7,14 @@ import Foundation +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + enum WMPError: Error { case invalidParameters case methodChannelNotSet diff --git a/workmanager_apple/ios/Classes/WorkmanagerPlugin.h b/workmanager_apple/ios/Classes/WorkmanagerPlugin.h deleted file mode 100644 index 5c8fd0a3..00000000 --- a/workmanager_apple/ios/Classes/WorkmanagerPlugin.h +++ /dev/null @@ -1,31 +0,0 @@ -#import - -@interface WorkmanagerPlugin : NSObject - -/** - * Register a custom task identifier to be scheduled/executed later on. - * @author Tuyen Vu - * - * @param taskIdentifier The identifier of the custom task. - */ -+ (void)registerTaskWithIdentifier:(NSString *) taskIdentifier; - -/** - * Register a custom task identifier as iOS BGAppRefresh Task executed randomly in future. - * @author Lars Huth - * - * @param taskIdentifier The identifier of the custom task. Must be set in info.plist - * @param frequency The repeat frequency in seconds - */ -+ (void)registerPeriodicTaskWithIdentifier:(NSString *) taskIdentifier frequency:(NSNumber *) frequency; - -/** - * Register a custom task identifier as iOS BackgroundProcessingTask executed randomly in future. - * @author Lars Huth - * - * @param taskIdentifier The identifier of the custom task. Must be set in info.plist - */ -+ (void)registerBGProcessingTaskWithIdentifier:(NSString *) taskIdentifier; - - -@end diff --git a/workmanager_apple/ios/Classes/WorkmanagerPlugin.m b/workmanager_apple/ios/Classes/WorkmanagerPlugin.m deleted file mode 100644 index cee8376b..00000000 --- a/workmanager_apple/ios/Classes/WorkmanagerPlugin.m +++ /dev/null @@ -1,38 +0,0 @@ -#import "WorkmanagerPlugin.h" - -#if __has_include() -#import -#else -#import "workmanager_apple-Swift.h" -#endif - -@implementation WorkmanagerPlugin - -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftWorkmanagerPlugin registerWithRegistrar:registrar]; -} - -+ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { - [SwiftWorkmanagerPlugin setPluginRegistrantCallback:callback]; -} - -// TODO this might not be needed anymore -+ (void)registerTaskWithIdentifier:(NSString *) taskIdentifier { - if (@available(iOS 13, *)) { - [SwiftWorkmanagerPlugin registerBGProcessingTaskWithIdentifier:taskIdentifier]; - } -} - -+ (void)registerPeriodicTaskWithIdentifier:(NSString *)taskIdentifier frequency:(NSNumber *) frequency { - if (@available(iOS 13, *)) { - [SwiftWorkmanagerPlugin registerPeriodicTaskWithIdentifier:taskIdentifier frequency:frequency]; - } -} - -+ (void)registerBGProcessingTaskWithIdentifier:(NSString *) taskIdentifier{ - if (@available(iOS 13, *)) { - [SwiftWorkmanagerPlugin registerBGProcessingTaskWithIdentifier:taskIdentifier]; - } -} - -@end diff --git a/workmanager_apple/ios/Classes/WorkmanagerPlugin.swift b/workmanager_apple/ios/Classes/WorkmanagerPlugin.swift new file mode 100644 index 00000000..2dafea12 --- /dev/null +++ b/workmanager_apple/ios/Classes/WorkmanagerPlugin.swift @@ -0,0 +1,372 @@ +import BackgroundTasks +import Flutter +import UIKit +import os + +/** + * Pigeon-based implementation of WorkmanagerHostApi for iOS. + * Replaces the manual method channel and data extraction approach. + * + * Note: Pigeon guarantees that host API handlers are not called when the plugin + * is detached, so properties can be safely used without null checks in API methods. + */ +public class WorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate, FlutterPlugin, WorkmanagerHostApi { + static let identifier = "dev.fluttercommunity.workmanager" + + private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? + private var isInDebugMode: Bool = false + + // MARK: - Static Background Task Handlers + + @available(iOS 13.0, *) + private static func handleBGProcessingTask(identifier: String, task: BGProcessingTask) { + let operationQueue = OperationQueue() + let operation = createBackgroundOperation( + identifier: task.identifier, + inputData: nil, + backgroundMode: .backgroundProcessingTask(identifier: identifier) + ) + + task.expirationHandler = { operation.cancel() } + operation.completionBlock = { task.setTaskCompleted(success: !operation.isCancelled) } + + operationQueue.addOperation(operation) + } + + @available(iOS 13.0, *) + public static func handlePeriodicTask(identifier: String, task: BGAppRefreshTask, earliestBeginInSeconds: Double?) { + guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), + let _ = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) + else { + logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") + return + } + + // If frequency is not provided it will default to 15 minutes + schedulePeriodicTask(taskIdentifier: task.identifier, earliestBeginInSeconds: earliestBeginInSeconds ?? (15 * 60)) + + let operationQueue = OperationQueue() + let operation = createBackgroundOperation( + identifier: task.identifier, + inputData: nil, + backgroundMode: .backgroundPeriodicTask(identifier: identifier) + ) + + task.expirationHandler = { operation.cancel() } + operation.completionBlock = { task.setTaskCompleted(success: !operation.isCancelled) } + + operationQueue.addOperation(operation) + } + + @available(iOS 13.0, *) + public static func startOneOffTask(identifier: String, taskIdentifier: UIBackgroundTaskIdentifier, inputData: [String: Any]?, delaySeconds: Int64) { + let operationQueue = OperationQueue() + let operation = createBackgroundOperation( + identifier: identifier, + inputData: inputData, + backgroundMode: .backgroundOneOffTask(identifier: identifier) + ) + + operation.completionBlock = { UIApplication.shared.endBackgroundTask(taskIdentifier) } + operationQueue.addOperation(operation) + } + + @objc + public static func registerPeriodicTask(withIdentifier identifier: String, frequency: NSNumber?) { + if #available(iOS 13.0, *) { + var frequencyInSeconds: Double? + if let frequencyValue = frequency { + frequencyInSeconds = frequencyValue.doubleValue + } + + BGTaskScheduler.shared.register( + forTaskWithIdentifier: identifier, + using: nil + ) { task in + if let task = task as? BGAppRefreshTask { + handlePeriodicTask(identifier: identifier, task: task, earliestBeginInSeconds: frequencyInSeconds) + } + } + } + } + + @objc + @available(iOS 13.0, *) + private static func schedulePeriodicTask(taskIdentifier identifier: String, earliestBeginInSeconds begin: Double) { + let request = BGAppRefreshTaskRequest(identifier: identifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: begin) + do { + try BGTaskScheduler.shared.submit(request) + logInfo("BGAppRefreshTask submitted \(identifier) earliestBeginInSeconds:\(begin)") + } catch { + logInfo("Could not schedule BGAppRefreshTask \(error.localizedDescription)") + } + } + + @objc + public static func registerBGProcessingTask(withIdentifier identifier: String) { + if #available(iOS 13.0, *) { + BGTaskScheduler.shared.register( + forTaskWithIdentifier: identifier, + using: nil + ) { task in + if let task = task as? BGProcessingTask { + handleBGProcessingTask(identifier: identifier, task: task) + } + } + } + } + + @objc + @available(iOS 13.0, *) + private static func scheduleBackgroundProcessingTask( + withIdentifier uniqueTaskIdentifier: String, + earliestBeginInSeconds begin: Double, + requiresNetworkConnectivity: Bool, + requiresExternalPower: Bool + ) { + let request = BGProcessingTaskRequest(identifier: uniqueTaskIdentifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: begin) + request.requiresNetworkConnectivity = requiresNetworkConnectivity + request.requiresExternalPower = requiresExternalPower + do { + try BGTaskScheduler.shared.submit(request) + logInfo("BGProcessingTask submitted \(uniqueTaskIdentifier) earliestBeginInSeconds:\(begin)") + } catch { + logInfo("Could not schedule BGProcessingTask identifier:\(uniqueTaskIdentifier) error:\(error.localizedDescription)") + logInfo("Possible issues can be: running on a simulator instead of a real device, or the task name is not registered") + } + } + + // MARK: - FlutterPlugin conformance + + @objc + public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { + flutterPluginRegistrantCallback = callback + } + + // MARK: - WorkmanagerHostApi implementation + + func initialize(request: InitializeRequest, completion: @escaping (Result) -> Void) { + UserDefaultsHelper.storeCallbackHandle(request.callbackHandle) + UserDefaultsHelper.storeIsDebug(request.isInDebugMode) + isInDebugMode = request.isInDebugMode + completion(.success(())) + } + + func registerOneOffTask(request: OneOffTaskRequest, completion: @escaping (Result) -> Void) { + guard validateCallbackHandle() else { + completion(.failure(createInitializationError())) + return + } + + executeIfSupportedVoid(completion: completion, feature: "OneOffTask") { + var taskIdentifier: UIBackgroundTaskIdentifier = .invalid + let delaySeconds = request.initialDelaySeconds ?? 0 + + taskIdentifier = UIApplication.shared.beginBackgroundTask(withName: request.uniqueName, expirationHandler: { + UIApplication.shared.endBackgroundTask(taskIdentifier) + }) + + WorkmanagerPlugin.startOneOffTask( + identifier: request.uniqueName, + taskIdentifier: taskIdentifier, + inputData: request.inputData as? [String: Any], + delaySeconds: delaySeconds + ) + } + } + + func registerPeriodicTask(request: PeriodicTaskRequest, completion: @escaping (Result) -> Void) { + guard validateCallbackHandle() else { + completion(.failure(createInitializationError())) + return + } + + executeIfSupportedVoid(completion: completion, feature: "PeriodicTask") { + let initialDelaySeconds = Double(request.initialDelaySeconds ?? 0) + WorkmanagerPlugin.schedulePeriodicTask( + taskIdentifier: request.uniqueName, + earliestBeginInSeconds: initialDelaySeconds + ) + } + } + + func registerProcessingTask(request: ProcessingTaskRequest, completion: @escaping (Result) -> Void) { + guard validateCallbackHandle() else { + completion(.failure(createInitializationError())) + return + } + + executeIfSupportedVoid(completion: completion, feature: "BackgroundProcessingTask") { + let delaySeconds = Double(request.initialDelaySeconds ?? 0) + let requiresCharging = request.requiresCharging ?? false + let requiresNetwork = request.networkType == .connected || request.networkType == .metered + + WorkmanagerPlugin.scheduleBackgroundProcessingTask( + withIdentifier: request.uniqueName, + earliestBeginInSeconds: delaySeconds, + requiresNetworkConnectivity: requiresNetwork, + requiresExternalPower: requiresCharging + ) + } + } + + func cancelByUniqueName(uniqueName: String, completion: @escaping (Result) -> Void) { + executeIfSupportedVoid(completion: completion, feature: "cancelByUniqueName") { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: uniqueName) + } + } + + func cancelByTag(tag: String, completion: @escaping (Result) -> Void) { + // iOS doesn't support canceling by tag - this is an Android-specific feature + completion(.success(())) + } + + func cancelAll(completion: @escaping (Result) -> Void) { + executeIfSupportedVoid(completion: completion, feature: "cancelAll") { + BGTaskScheduler.shared.cancelAllTaskRequests() + } + } + + func isScheduledByUniqueName(uniqueName: String, completion: @escaping (Result) -> Void) { + if #available(iOS 13.0, *) { + BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in + let isScheduled = taskRequests.contains { $0.identifier == uniqueName } + completion(.success(isScheduled)) + } + } else { + completion(.success(false)) + } + } + + func printScheduledTasks(completion: @escaping (Result) -> Void) { + if #available(iOS 13.0, *) { + BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in + if taskRequests.isEmpty { + let message = "[BGTaskScheduler] There are no scheduled tasks" + log(message) + completion(.success(message)) + return + } + + var message = "[BGTaskScheduler] Scheduled Tasks:" + for taskRequest in taskRequests { + message += "\n[BGTaskScheduler] Task Identifier: \(taskRequest.identifier) earliestBeginDate: \(taskRequest.earliestBeginDate?.formatted() ?? "")" + } + log("\(message)") + completion(.success(message)) + } + } else { + completion(.failure(PigeonError( + code: "99", + message: "printScheduledTasks is only supported on iOS 13+", + details: "BGTaskScheduler.getPendingTaskRequests is only supported on iOS 13+" + ))) + } + } + + // MARK: - Helper methods + + private func validateCallbackHandle() -> Bool { + return UserDefaultsHelper.getStoredCallbackHandle() != nil + } + + private func createInitializationError() -> PigeonError { + return PigeonError( + code: "1", + message: "You have not properly initialized the Flutter WorkManager Package. " + + "You should ensure you have called the 'initialize' function first! " + + "Example: \n" + + "\n" + + "`Workmanager().initialize(\n" + + " callbackDispatcher,\n" + + " )`" + + "\n" + + "\n" + + "The `callbackDispatcher` is a top level function. See example in repository.", + details: nil + ) + } + + private func createUnsupportedVersionError(feature: String) -> PigeonError { + return PigeonError( + code: "99", + message: "\(feature) could not be registered", + details: "BGTaskScheduler tasks are only supported on iOS 13+" + ) + } + + private func executeIfSupported( + completion: @escaping (Result) -> Void, + defaultValue: T? = nil, + feature: String, + action: @escaping () -> T + ) { + if #available(iOS 13.0, *) { + let result = action() + completion(.success(result)) + } else { + if let defaultValue = defaultValue { + completion(.success(defaultValue)) + } else { + completion(.failure(createUnsupportedVersionError(feature: feature))) + } + } + } + + private func executeIfSupportedVoid( + completion: @escaping (Result) -> Void, + feature: String, + action: @escaping () -> Void + ) { + if #available(iOS 13.0, *) { + action() + completion(.success(())) + } else { + completion(.failure(createUnsupportedVersionError(feature: feature))) + } + } + + @available(iOS 13.0, *) + private static func createBackgroundOperation( + identifier: String, + inputData: [String: Any]?, + backgroundMode: BackgroundMode + ) -> BackgroundTaskOperation { + return BackgroundTaskOperation( + identifier, + inputData: inputData, + flutterPluginRegistrantCallback: flutterPluginRegistrantCallback, + backgroundMode: backgroundMode + ) + } +} + +// MARK: - FlutterPlugin conformance + +extension WorkmanagerPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = WorkmanagerPlugin() + WorkmanagerHostApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) + registrar.addApplicationDelegate(instance) + } +} + +// MARK: - AppDelegate conformance + +extension WorkmanagerPlugin { + override public func application( + _ application: UIApplication, + performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) -> Bool { + // Old background fetch API for iOS 12 and lower + let worker = BackgroundWorker( + mode: .backgroundFetch, + inputData: nil, + flutterPluginRegistrantCallback: WorkmanagerPlugin.flutterPluginRegistrantCallback + ) + + return worker.performBackgroundRequest(completionHandler) + } +} diff --git a/workmanager_apple/ios/Classes/pigeon/WorkmanagerApi.g.swift b/workmanager_apple/ios/Classes/pigeon/WorkmanagerApi.g.swift new file mode 100644 index 00000000..8c29c614 --- /dev/null +++ b/workmanager_apple/ios/Classes/pigeon/WorkmanagerApi.g.swift @@ -0,0 +1,688 @@ +// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved. +// // Use of this source code is governed by a MIT-style license that can be +// // found in the LICENSE file. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Any? + + init(code: String, message: String?, details: Any?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func createConnectionError(withChannelName channelName: String) -> PigeonError { + return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +/// An enumeration of various network types that can be used as Constraints for work. +/// +/// Fully supported on Android. +/// +/// On iOS, this enumeration is used to define whether a piece of work requires +/// internet connectivity, by checking for either [NetworkType.connected] or +/// [NetworkType.metered]. +enum NetworkType: Int { + /// Any working network connection is required for this work. + case connected = 0 + /// A metered network connection is required for this work. + case metered = 1 + /// Default value. A network is not required for this work. + case notRequired = 2 + /// A non-roaming network connection is required for this work. + case notRoaming = 3 + /// An unmetered network connection is required for this work. + case unmetered = 4 + /// A temporarily unmetered Network. This capability will be set for + /// networks that are generally metered, but are currently unmetered. + /// + /// Android API 30+ + case temporarilyUnmetered = 5 +} + +/// An enumeration of backoff policies when retrying work. +/// These policies are used when you have a return ListenableWorker.Result.retry() from a worker to determine the correct backoff time. +/// Backoff policies are set in WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit) or one of its variants. +enum BackoffPolicy: Int { + /// Used to indicate that WorkManager should increase the backoff time exponentially + case exponential = 0 + /// Used to indicate that WorkManager should increase the backoff time linearly + case linear = 1 +} + +/// An enumeration of the conflict resolution policies in case of a collision. +enum ExistingWorkPolicy: Int { + /// If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as a child of all the leaves of that work sequence. + case append = 0 + /// If there is existing pending (uncompleted) work with the same unique name, do nothing. + case keep = 1 + /// If there is existing pending (uncompleted) work with the same unique name, cancel and delete it. + case replace = 2 + /// If there is existing pending (uncompleted) work with the same unique name, it will be updated the new specification. + /// Note: This maps to appendOrReplace in the native implementation. + case update = 3 +} + +/// An enumeration of policies that help determine out of quota behavior for expedited jobs. +/// +/// Only supported on Android. +enum OutOfQuotaPolicy: Int { + /// When the app does not have any expedited job quota, the expedited work request will + /// fallback to a regular work request. + case runAsNonExpeditedWorkRequest = 0 + /// When the app does not have any expedited job quota, the expedited work request will + /// we dropped and no work requests are enqueued. + case dropWorkRequest = 1 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct Constraints { + var networkType: NetworkType? = nil + var requiresBatteryNotLow: Bool? = nil + var requiresCharging: Bool? = nil + var requiresDeviceIdle: Bool? = nil + var requiresStorageNotLow: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> Constraints? { + let networkType: NetworkType? = nilOrValue(pigeonVar_list[0]) + let requiresBatteryNotLow: Bool? = nilOrValue(pigeonVar_list[1]) + let requiresCharging: Bool? = nilOrValue(pigeonVar_list[2]) + let requiresDeviceIdle: Bool? = nilOrValue(pigeonVar_list[3]) + let requiresStorageNotLow: Bool? = nilOrValue(pigeonVar_list[4]) + + return Constraints( + networkType: networkType, + requiresBatteryNotLow: requiresBatteryNotLow, + requiresCharging: requiresCharging, + requiresDeviceIdle: requiresDeviceIdle, + requiresStorageNotLow: requiresStorageNotLow + ) + } + func toList() -> [Any?] { + return [ + networkType, + requiresBatteryNotLow, + requiresCharging, + requiresDeviceIdle, + requiresStorageNotLow, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct BackoffPolicyConfig { + var backoffPolicy: BackoffPolicy? = nil + var backoffDelayMillis: Int64? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> BackoffPolicyConfig? { + let backoffPolicy: BackoffPolicy? = nilOrValue(pigeonVar_list[0]) + let backoffDelayMillis: Int64? = nilOrValue(pigeonVar_list[1]) + + return BackoffPolicyConfig( + backoffPolicy: backoffPolicy, + backoffDelayMillis: backoffDelayMillis + ) + } + func toList() -> [Any?] { + return [ + backoffPolicy, + backoffDelayMillis, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct InitializeRequest { + var callbackHandle: Int64 + var isInDebugMode: Bool + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> InitializeRequest? { + let callbackHandle = pigeonVar_list[0] as! Int64 + let isInDebugMode = pigeonVar_list[1] as! Bool + + return InitializeRequest( + callbackHandle: callbackHandle, + isInDebugMode: isInDebugMode + ) + } + func toList() -> [Any?] { + return [ + callbackHandle, + isInDebugMode, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct OneOffTaskRequest { + var uniqueName: String + var taskName: String + var inputData: [String?: Any?]? = nil + var initialDelaySeconds: Int64? = nil + var constraints: Constraints? = nil + var backoffPolicy: BackoffPolicyConfig? = nil + var tag: String? = nil + var existingWorkPolicy: ExistingWorkPolicy? = nil + var outOfQuotaPolicy: OutOfQuotaPolicy? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> OneOffTaskRequest? { + let uniqueName = pigeonVar_list[0] as! String + let taskName = pigeonVar_list[1] as! String + let inputData: [String?: Any?]? = nilOrValue(pigeonVar_list[2]) + let initialDelaySeconds: Int64? = nilOrValue(pigeonVar_list[3]) + let constraints: Constraints? = nilOrValue(pigeonVar_list[4]) + let backoffPolicy: BackoffPolicyConfig? = nilOrValue(pigeonVar_list[5]) + let tag: String? = nilOrValue(pigeonVar_list[6]) + let existingWorkPolicy: ExistingWorkPolicy? = nilOrValue(pigeonVar_list[7]) + let outOfQuotaPolicy: OutOfQuotaPolicy? = nilOrValue(pigeonVar_list[8]) + + return OneOffTaskRequest( + uniqueName: uniqueName, + taskName: taskName, + inputData: inputData, + initialDelaySeconds: initialDelaySeconds, + constraints: constraints, + backoffPolicy: backoffPolicy, + tag: tag, + existingWorkPolicy: existingWorkPolicy, + outOfQuotaPolicy: outOfQuotaPolicy + ) + } + func toList() -> [Any?] { + return [ + uniqueName, + taskName, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + outOfQuotaPolicy, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PeriodicTaskRequest { + var uniqueName: String + var taskName: String + var frequencySeconds: Int64 + var flexIntervalSeconds: Int64? = nil + var inputData: [String?: Any?]? = nil + var initialDelaySeconds: Int64? = nil + var constraints: Constraints? = nil + var backoffPolicy: BackoffPolicyConfig? = nil + var tag: String? = nil + var existingWorkPolicy: ExistingWorkPolicy? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PeriodicTaskRequest? { + let uniqueName = pigeonVar_list[0] as! String + let taskName = pigeonVar_list[1] as! String + let frequencySeconds = pigeonVar_list[2] as! Int64 + let flexIntervalSeconds: Int64? = nilOrValue(pigeonVar_list[3]) + let inputData: [String?: Any?]? = nilOrValue(pigeonVar_list[4]) + let initialDelaySeconds: Int64? = nilOrValue(pigeonVar_list[5]) + let constraints: Constraints? = nilOrValue(pigeonVar_list[6]) + let backoffPolicy: BackoffPolicyConfig? = nilOrValue(pigeonVar_list[7]) + let tag: String? = nilOrValue(pigeonVar_list[8]) + let existingWorkPolicy: ExistingWorkPolicy? = nilOrValue(pigeonVar_list[9]) + + return PeriodicTaskRequest( + uniqueName: uniqueName, + taskName: taskName, + frequencySeconds: frequencySeconds, + flexIntervalSeconds: flexIntervalSeconds, + inputData: inputData, + initialDelaySeconds: initialDelaySeconds, + constraints: constraints, + backoffPolicy: backoffPolicy, + tag: tag, + existingWorkPolicy: existingWorkPolicy + ) + } + func toList() -> [Any?] { + return [ + uniqueName, + taskName, + frequencySeconds, + flexIntervalSeconds, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct ProcessingTaskRequest { + var uniqueName: String + var taskName: String + var inputData: [String?: Any?]? = nil + var initialDelaySeconds: Int64? = nil + var networkType: NetworkType? = nil + var requiresCharging: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ProcessingTaskRequest? { + let uniqueName = pigeonVar_list[0] as! String + let taskName = pigeonVar_list[1] as! String + let inputData: [String?: Any?]? = nilOrValue(pigeonVar_list[2]) + let initialDelaySeconds: Int64? = nilOrValue(pigeonVar_list[3]) + let networkType: NetworkType? = nilOrValue(pigeonVar_list[4]) + let requiresCharging: Bool? = nilOrValue(pigeonVar_list[5]) + + return ProcessingTaskRequest( + uniqueName: uniqueName, + taskName: taskName, + inputData: inputData, + initialDelaySeconds: initialDelaySeconds, + networkType: networkType, + requiresCharging: requiresCharging + ) + } + func toList() -> [Any?] { + return [ + uniqueName, + taskName, + inputData, + initialDelaySeconds, + networkType, + requiresCharging, + ] + } +} + +private class WorkmanagerApiPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return NetworkType(rawValue: enumResultAsInt) + } + return nil + case 130: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return BackoffPolicy(rawValue: enumResultAsInt) + } + return nil + case 131: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return ExistingWorkPolicy(rawValue: enumResultAsInt) + } + return nil + case 132: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return OutOfQuotaPolicy(rawValue: enumResultAsInt) + } + return nil + case 133: + return Constraints.fromList(self.readValue() as! [Any?]) + case 134: + return BackoffPolicyConfig.fromList(self.readValue() as! [Any?]) + case 135: + return InitializeRequest.fromList(self.readValue() as! [Any?]) + case 136: + return OneOffTaskRequest.fromList(self.readValue() as! [Any?]) + case 137: + return PeriodicTaskRequest.fromList(self.readValue() as! [Any?]) + case 138: + return ProcessingTaskRequest.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class WorkmanagerApiPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? NetworkType { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? BackoffPolicy { + super.writeByte(130) + super.writeValue(value.rawValue) + } else if let value = value as? ExistingWorkPolicy { + super.writeByte(131) + super.writeValue(value.rawValue) + } else if let value = value as? OutOfQuotaPolicy { + super.writeByte(132) + super.writeValue(value.rawValue) + } else if let value = value as? Constraints { + super.writeByte(133) + super.writeValue(value.toList()) + } else if let value = value as? BackoffPolicyConfig { + super.writeByte(134) + super.writeValue(value.toList()) + } else if let value = value as? InitializeRequest { + super.writeByte(135) + super.writeValue(value.toList()) + } else if let value = value as? OneOffTaskRequest { + super.writeByte(136) + super.writeValue(value.toList()) + } else if let value = value as? PeriodicTaskRequest { + super.writeByte(137) + super.writeValue(value.toList()) + } else if let value = value as? ProcessingTaskRequest { + super.writeByte(138) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class WorkmanagerApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return WorkmanagerApiPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return WorkmanagerApiPigeonCodecWriter(data: data) + } +} + +class WorkmanagerApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = WorkmanagerApiPigeonCodec(readerWriter: WorkmanagerApiPigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol WorkmanagerHostApi { + func initialize(request: InitializeRequest, completion: @escaping (Result) -> Void) + func registerOneOffTask(request: OneOffTaskRequest, completion: @escaping (Result) -> Void) + func registerPeriodicTask(request: PeriodicTaskRequest, completion: @escaping (Result) -> Void) + func registerProcessingTask(request: ProcessingTaskRequest, completion: @escaping (Result) -> Void) + func cancelByUniqueName(uniqueName: String, completion: @escaping (Result) -> Void) + func cancelByTag(tag: String, completion: @escaping (Result) -> Void) + func cancelAll(completion: @escaping (Result) -> Void) + func isScheduledByUniqueName(uniqueName: String, completion: @escaping (Result) -> Void) + func printScheduledTasks(completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class WorkmanagerHostApiSetup { + static var codec: FlutterStandardMessageCodec { WorkmanagerApiPigeonCodec.shared } + /// Sets up an instance of `WorkmanagerHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: WorkmanagerHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let initializeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.initialize\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + initializeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! InitializeRequest + api.initialize(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + initializeChannel.setMessageHandler(nil) + } + let registerOneOffTaskChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerOneOffTask\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + registerOneOffTaskChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! OneOffTaskRequest + api.registerOneOffTask(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + registerOneOffTaskChannel.setMessageHandler(nil) + } + let registerPeriodicTaskChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerPeriodicTask\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + registerPeriodicTaskChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! PeriodicTaskRequest + api.registerPeriodicTask(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + registerPeriodicTaskChannel.setMessageHandler(nil) + } + let registerProcessingTaskChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerProcessingTask\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + registerProcessingTaskChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! ProcessingTaskRequest + api.registerProcessingTask(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + registerProcessingTaskChannel.setMessageHandler(nil) + } + let cancelByUniqueNameChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByUniqueName\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + cancelByUniqueNameChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let uniqueNameArg = args[0] as! String + api.cancelByUniqueName(uniqueName: uniqueNameArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + cancelByUniqueNameChannel.setMessageHandler(nil) + } + let cancelByTagChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByTag\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + cancelByTagChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let tagArg = args[0] as! String + api.cancelByTag(tag: tagArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + cancelByTagChannel.setMessageHandler(nil) + } + let cancelAllChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelAll\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + cancelAllChannel.setMessageHandler { _, reply in + api.cancelAll { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + cancelAllChannel.setMessageHandler(nil) + } + let isScheduledByUniqueNameChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.isScheduledByUniqueName\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isScheduledByUniqueNameChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let uniqueNameArg = args[0] as! String + api.isScheduledByUniqueName(uniqueName: uniqueNameArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isScheduledByUniqueNameChannel.setMessageHandler(nil) + } + let printScheduledTasksChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.printScheduledTasks\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + printScheduledTasksChannel.setMessageHandler { _, reply in + api.printScheduledTasks { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + printScheduledTasksChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol WorkmanagerFlutterApiProtocol { + func backgroundChannelInitialized(completion: @escaping (Result) -> Void) + func executeTask(taskName taskNameArg: String, inputData inputDataArg: [String?: Any?]?, completion: @escaping (Result) -> Void) +} +class WorkmanagerFlutterApi: WorkmanagerFlutterApiProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + var codec: WorkmanagerApiPigeonCodec { + return WorkmanagerApiPigeonCodec.shared + } + func backgroundChannelInitialized(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.backgroundChannelInitialized\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func executeTask(taskName taskNameArg: String, inputData inputDataArg: [String?: Any?]?, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.executeTask\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([taskNameArg, inputDataArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else if listResponse[0] == nil { + completion(.failure(PigeonError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: ""))) + } else { + let result = listResponse[0] as! Bool + completion(.success(result)) + } + } + } +} diff --git a/workmanager_apple/ios/workmanager_apple.podspec b/workmanager_apple/ios/workmanager_apple.podspec index 670791b5..48cde2b9 100644 --- a/workmanager_apple/ios/workmanager_apple.podspec +++ b/workmanager_apple/ios/workmanager_apple.podspec @@ -13,7 +13,6 @@ Flutter Android Workmanager s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.ios.deployment_target = '13.0' diff --git a/workmanager_apple/lib/workmanager_apple.dart b/workmanager_apple/lib/workmanager_apple.dart index 009c7b48..9c70eade 100644 --- a/workmanager_apple/lib/workmanager_apple.dart +++ b/workmanager_apple/lib/workmanager_apple.dart @@ -85,7 +85,7 @@ class WorkmanagerApple extends WorkmanagerPlatform { 'taskName': taskName, 'inputData': inputData, 'initialDelaySeconds': initialDelay?.inSeconds, - 'networkType': constraints?.networkType.name, + 'networkType': constraints?.networkType?.name, 'requiresCharging': constraints?.requiresCharging, }); } diff --git a/workmanager_platform_interface/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt b/workmanager_platform_interface/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt new file mode 100644 index 00000000..8460e109 --- /dev/null +++ b/workmanager_platform_interface/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt @@ -0,0 +1,686 @@ +// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved. +// // Use of this source code is governed by a MIT-style license that can be +// // found in the LICENSE file. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package dev.fluttercommunity.workmanager.pigeon + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private fun wrapResult(result: Any?): List { + return listOf(result) +} + +private fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } +} + +private fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "")} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** + * An enumeration of various network types that can be used as Constraints for work. + * + * Fully supported on Android. + * + * On iOS, this enumeration is used to define whether a piece of work requires + * internet connectivity, by checking for either [NetworkType.connected] or + * [NetworkType.metered]. + */ +enum class NetworkType(val raw: Int) { + /** Any working network connection is required for this work. */ + CONNECTED(0), + /** A metered network connection is required for this work. */ + METERED(1), + /** Default value. A network is not required for this work. */ + NOT_REQUIRED(2), + /** A non-roaming network connection is required for this work. */ + NOT_ROAMING(3), + /** An unmetered network connection is required for this work. */ + UNMETERED(4), + /** + * A temporarily unmetered Network. This capability will be set for + * networks that are generally metered, but are currently unmetered. + * + * Android API 30+ + */ + TEMPORARILY_UNMETERED(5); + + companion object { + fun ofRaw(raw: Int): NetworkType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * An enumeration of backoff policies when retrying work. + * These policies are used when you have a return ListenableWorker.Result.retry() from a worker to determine the correct backoff time. + * Backoff policies are set in WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit) or one of its variants. + */ +enum class BackoffPolicy(val raw: Int) { + /** Used to indicate that WorkManager should increase the backoff time exponentially */ + EXPONENTIAL(0), + /** Used to indicate that WorkManager should increase the backoff time linearly */ + LINEAR(1); + + companion object { + fun ofRaw(raw: Int): BackoffPolicy? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** An enumeration of the conflict resolution policies in case of a collision. */ +enum class ExistingWorkPolicy(val raw: Int) { + /** If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as a child of all the leaves of that work sequence. */ + APPEND(0), + /** If there is existing pending (uncompleted) work with the same unique name, do nothing. */ + KEEP(1), + /** If there is existing pending (uncompleted) work with the same unique name, cancel and delete it. */ + REPLACE(2), + /** + * If there is existing pending (uncompleted) work with the same unique name, it will be updated the new specification. + * Note: This maps to appendOrReplace in the native implementation. + */ + UPDATE(3); + + companion object { + fun ofRaw(raw: Int): ExistingWorkPolicy? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * An enumeration of policies that help determine out of quota behavior for expedited jobs. + * + * Only supported on Android. + */ +enum class OutOfQuotaPolicy(val raw: Int) { + /** + * When the app does not have any expedited job quota, the expedited work request will + * fallback to a regular work request. + */ + RUN_AS_NON_EXPEDITED_WORK_REQUEST(0), + /** + * When the app does not have any expedited job quota, the expedited work request will + * we dropped and no work requests are enqueued. + */ + DROP_WORK_REQUEST(1); + + companion object { + fun ofRaw(raw: Int): OutOfQuotaPolicy? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class Constraints ( + val networkType: NetworkType? = null, + val requiresBatteryNotLow: Boolean? = null, + val requiresCharging: Boolean? = null, + val requiresDeviceIdle: Boolean? = null, + val requiresStorageNotLow: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): Constraints { + val networkType = pigeonVar_list[0] as NetworkType? + val requiresBatteryNotLow = pigeonVar_list[1] as Boolean? + val requiresCharging = pigeonVar_list[2] as Boolean? + val requiresDeviceIdle = pigeonVar_list[3] as Boolean? + val requiresStorageNotLow = pigeonVar_list[4] as Boolean? + return Constraints(networkType, requiresBatteryNotLow, requiresCharging, requiresDeviceIdle, requiresStorageNotLow) + } + } + fun toList(): List { + return listOf( + networkType, + requiresBatteryNotLow, + requiresCharging, + requiresDeviceIdle, + requiresStorageNotLow, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class BackoffPolicyConfig ( + val backoffPolicy: BackoffPolicy? = null, + val backoffDelayMillis: Long? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): BackoffPolicyConfig { + val backoffPolicy = pigeonVar_list[0] as BackoffPolicy? + val backoffDelayMillis = pigeonVar_list[1] as Long? + return BackoffPolicyConfig(backoffPolicy, backoffDelayMillis) + } + } + fun toList(): List { + return listOf( + backoffPolicy, + backoffDelayMillis, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class InitializeRequest ( + val callbackHandle: Long, + val isInDebugMode: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): InitializeRequest { + val callbackHandle = pigeonVar_list[0] as Long + val isInDebugMode = pigeonVar_list[1] as Boolean + return InitializeRequest(callbackHandle, isInDebugMode) + } + } + fun toList(): List { + return listOf( + callbackHandle, + isInDebugMode, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class OneOffTaskRequest ( + val uniqueName: String, + val taskName: String, + val inputData: Map? = null, + val initialDelaySeconds: Long? = null, + val constraints: Constraints? = null, + val backoffPolicy: BackoffPolicyConfig? = null, + val tag: String? = null, + val existingWorkPolicy: ExistingWorkPolicy? = null, + val outOfQuotaPolicy: OutOfQuotaPolicy? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): OneOffTaskRequest { + val uniqueName = pigeonVar_list[0] as String + val taskName = pigeonVar_list[1] as String + val inputData = pigeonVar_list[2] as Map? + val initialDelaySeconds = pigeonVar_list[3] as Long? + val constraints = pigeonVar_list[4] as Constraints? + val backoffPolicy = pigeonVar_list[5] as BackoffPolicyConfig? + val tag = pigeonVar_list[6] as String? + val existingWorkPolicy = pigeonVar_list[7] as ExistingWorkPolicy? + val outOfQuotaPolicy = pigeonVar_list[8] as OutOfQuotaPolicy? + return OneOffTaskRequest(uniqueName, taskName, inputData, initialDelaySeconds, constraints, backoffPolicy, tag, existingWorkPolicy, outOfQuotaPolicy) + } + } + fun toList(): List { + return listOf( + uniqueName, + taskName, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + outOfQuotaPolicy, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PeriodicTaskRequest ( + val uniqueName: String, + val taskName: String, + val frequencySeconds: Long, + val flexIntervalSeconds: Long? = null, + val inputData: Map? = null, + val initialDelaySeconds: Long? = null, + val constraints: Constraints? = null, + val backoffPolicy: BackoffPolicyConfig? = null, + val tag: String? = null, + val existingWorkPolicy: ExistingWorkPolicy? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): PeriodicTaskRequest { + val uniqueName = pigeonVar_list[0] as String + val taskName = pigeonVar_list[1] as String + val frequencySeconds = pigeonVar_list[2] as Long + val flexIntervalSeconds = pigeonVar_list[3] as Long? + val inputData = pigeonVar_list[4] as Map? + val initialDelaySeconds = pigeonVar_list[5] as Long? + val constraints = pigeonVar_list[6] as Constraints? + val backoffPolicy = pigeonVar_list[7] as BackoffPolicyConfig? + val tag = pigeonVar_list[8] as String? + val existingWorkPolicy = pigeonVar_list[9] as ExistingWorkPolicy? + return PeriodicTaskRequest(uniqueName, taskName, frequencySeconds, flexIntervalSeconds, inputData, initialDelaySeconds, constraints, backoffPolicy, tag, existingWorkPolicy) + } + } + fun toList(): List { + return listOf( + uniqueName, + taskName, + frequencySeconds, + flexIntervalSeconds, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class ProcessingTaskRequest ( + val uniqueName: String, + val taskName: String, + val inputData: Map? = null, + val initialDelaySeconds: Long? = null, + val networkType: NetworkType? = null, + val requiresCharging: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): ProcessingTaskRequest { + val uniqueName = pigeonVar_list[0] as String + val taskName = pigeonVar_list[1] as String + val inputData = pigeonVar_list[2] as Map? + val initialDelaySeconds = pigeonVar_list[3] as Long? + val networkType = pigeonVar_list[4] as NetworkType? + val requiresCharging = pigeonVar_list[5] as Boolean? + return ProcessingTaskRequest(uniqueName, taskName, inputData, initialDelaySeconds, networkType, requiresCharging) + } + } + fun toList(): List { + return listOf( + uniqueName, + taskName, + inputData, + initialDelaySeconds, + networkType, + requiresCharging, + ) + } +} +private open class WorkmanagerApiPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + NetworkType.ofRaw(it.toInt()) + } + } + 130.toByte() -> { + return (readValue(buffer) as Long?)?.let { + BackoffPolicy.ofRaw(it.toInt()) + } + } + 131.toByte() -> { + return (readValue(buffer) as Long?)?.let { + ExistingWorkPolicy.ofRaw(it.toInt()) + } + } + 132.toByte() -> { + return (readValue(buffer) as Long?)?.let { + OutOfQuotaPolicy.ofRaw(it.toInt()) + } + } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { + Constraints.fromList(it) + } + } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + BackoffPolicyConfig.fromList(it) + } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { + InitializeRequest.fromList(it) + } + } + 136.toByte() -> { + return (readValue(buffer) as? List)?.let { + OneOffTaskRequest.fromList(it) + } + } + 137.toByte() -> { + return (readValue(buffer) as? List)?.let { + PeriodicTaskRequest.fromList(it) + } + } + 138.toByte() -> { + return (readValue(buffer) as? List)?.let { + ProcessingTaskRequest.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is NetworkType -> { + stream.write(129) + writeValue(stream, value.raw) + } + is BackoffPolicy -> { + stream.write(130) + writeValue(stream, value.raw) + } + is ExistingWorkPolicy -> { + stream.write(131) + writeValue(stream, value.raw) + } + is OutOfQuotaPolicy -> { + stream.write(132) + writeValue(stream, value.raw) + } + is Constraints -> { + stream.write(133) + writeValue(stream, value.toList()) + } + is BackoffPolicyConfig -> { + stream.write(134) + writeValue(stream, value.toList()) + } + is InitializeRequest -> { + stream.write(135) + writeValue(stream, value.toList()) + } + is OneOffTaskRequest -> { + stream.write(136) + writeValue(stream, value.toList()) + } + is PeriodicTaskRequest -> { + stream.write(137) + writeValue(stream, value.toList()) + } + is ProcessingTaskRequest -> { + stream.write(138) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface WorkmanagerHostApi { + fun initialize(request: InitializeRequest, callback: (Result) -> Unit) + fun registerOneOffTask(request: OneOffTaskRequest, callback: (Result) -> Unit) + fun registerPeriodicTask(request: PeriodicTaskRequest, callback: (Result) -> Unit) + fun registerProcessingTask(request: ProcessingTaskRequest, callback: (Result) -> Unit) + fun cancelByUniqueName(uniqueName: String, callback: (Result) -> Unit) + fun cancelByTag(tag: String, callback: (Result) -> Unit) + fun cancelAll(callback: (Result) -> Unit) + fun isScheduledByUniqueName(uniqueName: String, callback: (Result) -> Unit) + fun printScheduledTasks(callback: (Result) -> Unit) + + companion object { + /** The codec used by WorkmanagerHostApi. */ + val codec: MessageCodec by lazy { + WorkmanagerApiPigeonCodec() + } + /** Sets up an instance of `WorkmanagerHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: WorkmanagerHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.initialize$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as InitializeRequest + api.initialize(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerOneOffTask$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as OneOffTaskRequest + api.registerOneOffTask(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerPeriodicTask$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as PeriodicTaskRequest + api.registerPeriodicTask(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerProcessingTask$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as ProcessingTaskRequest + api.registerProcessingTask(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByUniqueName$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val uniqueNameArg = args[0] as String + api.cancelByUniqueName(uniqueNameArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByTag$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tagArg = args[0] as String + api.cancelByTag(tagArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelAll$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.cancelAll{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.isScheduledByUniqueName$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val uniqueNameArg = args[0] as String + api.isScheduledByUniqueName(uniqueNameArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.printScheduledTasks$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.printScheduledTasks{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class WorkmanagerFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by WorkmanagerFlutterApi. */ + val codec: MessageCodec by lazy { + WorkmanagerApiPigeonCodec() + } + } + fun backgroundChannelInitialized(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.backgroundChannelInitialized$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun executeTask(taskNameArg: String, inputDataArg: Map?, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.executeTask$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(taskNameArg, inputDataArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else if (it[0] == null) { + callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", ""))) + } else { + val output = it[0] as Boolean + callback(Result.success(output)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } +} diff --git a/workmanager_platform_interface/ios/Classes/pigeon/WorkmanagerApi.g.swift b/workmanager_platform_interface/ios/Classes/pigeon/WorkmanagerApi.g.swift new file mode 100644 index 00000000..8c29c614 --- /dev/null +++ b/workmanager_platform_interface/ios/Classes/pigeon/WorkmanagerApi.g.swift @@ -0,0 +1,688 @@ +// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved. +// // Use of this source code is governed by a MIT-style license that can be +// // found in the LICENSE file. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Any? + + init(code: String, message: String?, details: Any?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func createConnectionError(withChannelName channelName: String) -> PigeonError { + return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +/// An enumeration of various network types that can be used as Constraints for work. +/// +/// Fully supported on Android. +/// +/// On iOS, this enumeration is used to define whether a piece of work requires +/// internet connectivity, by checking for either [NetworkType.connected] or +/// [NetworkType.metered]. +enum NetworkType: Int { + /// Any working network connection is required for this work. + case connected = 0 + /// A metered network connection is required for this work. + case metered = 1 + /// Default value. A network is not required for this work. + case notRequired = 2 + /// A non-roaming network connection is required for this work. + case notRoaming = 3 + /// An unmetered network connection is required for this work. + case unmetered = 4 + /// A temporarily unmetered Network. This capability will be set for + /// networks that are generally metered, but are currently unmetered. + /// + /// Android API 30+ + case temporarilyUnmetered = 5 +} + +/// An enumeration of backoff policies when retrying work. +/// These policies are used when you have a return ListenableWorker.Result.retry() from a worker to determine the correct backoff time. +/// Backoff policies are set in WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit) or one of its variants. +enum BackoffPolicy: Int { + /// Used to indicate that WorkManager should increase the backoff time exponentially + case exponential = 0 + /// Used to indicate that WorkManager should increase the backoff time linearly + case linear = 1 +} + +/// An enumeration of the conflict resolution policies in case of a collision. +enum ExistingWorkPolicy: Int { + /// If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as a child of all the leaves of that work sequence. + case append = 0 + /// If there is existing pending (uncompleted) work with the same unique name, do nothing. + case keep = 1 + /// If there is existing pending (uncompleted) work with the same unique name, cancel and delete it. + case replace = 2 + /// If there is existing pending (uncompleted) work with the same unique name, it will be updated the new specification. + /// Note: This maps to appendOrReplace in the native implementation. + case update = 3 +} + +/// An enumeration of policies that help determine out of quota behavior for expedited jobs. +/// +/// Only supported on Android. +enum OutOfQuotaPolicy: Int { + /// When the app does not have any expedited job quota, the expedited work request will + /// fallback to a regular work request. + case runAsNonExpeditedWorkRequest = 0 + /// When the app does not have any expedited job quota, the expedited work request will + /// we dropped and no work requests are enqueued. + case dropWorkRequest = 1 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct Constraints { + var networkType: NetworkType? = nil + var requiresBatteryNotLow: Bool? = nil + var requiresCharging: Bool? = nil + var requiresDeviceIdle: Bool? = nil + var requiresStorageNotLow: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> Constraints? { + let networkType: NetworkType? = nilOrValue(pigeonVar_list[0]) + let requiresBatteryNotLow: Bool? = nilOrValue(pigeonVar_list[1]) + let requiresCharging: Bool? = nilOrValue(pigeonVar_list[2]) + let requiresDeviceIdle: Bool? = nilOrValue(pigeonVar_list[3]) + let requiresStorageNotLow: Bool? = nilOrValue(pigeonVar_list[4]) + + return Constraints( + networkType: networkType, + requiresBatteryNotLow: requiresBatteryNotLow, + requiresCharging: requiresCharging, + requiresDeviceIdle: requiresDeviceIdle, + requiresStorageNotLow: requiresStorageNotLow + ) + } + func toList() -> [Any?] { + return [ + networkType, + requiresBatteryNotLow, + requiresCharging, + requiresDeviceIdle, + requiresStorageNotLow, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct BackoffPolicyConfig { + var backoffPolicy: BackoffPolicy? = nil + var backoffDelayMillis: Int64? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> BackoffPolicyConfig? { + let backoffPolicy: BackoffPolicy? = nilOrValue(pigeonVar_list[0]) + let backoffDelayMillis: Int64? = nilOrValue(pigeonVar_list[1]) + + return BackoffPolicyConfig( + backoffPolicy: backoffPolicy, + backoffDelayMillis: backoffDelayMillis + ) + } + func toList() -> [Any?] { + return [ + backoffPolicy, + backoffDelayMillis, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct InitializeRequest { + var callbackHandle: Int64 + var isInDebugMode: Bool + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> InitializeRequest? { + let callbackHandle = pigeonVar_list[0] as! Int64 + let isInDebugMode = pigeonVar_list[1] as! Bool + + return InitializeRequest( + callbackHandle: callbackHandle, + isInDebugMode: isInDebugMode + ) + } + func toList() -> [Any?] { + return [ + callbackHandle, + isInDebugMode, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct OneOffTaskRequest { + var uniqueName: String + var taskName: String + var inputData: [String?: Any?]? = nil + var initialDelaySeconds: Int64? = nil + var constraints: Constraints? = nil + var backoffPolicy: BackoffPolicyConfig? = nil + var tag: String? = nil + var existingWorkPolicy: ExistingWorkPolicy? = nil + var outOfQuotaPolicy: OutOfQuotaPolicy? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> OneOffTaskRequest? { + let uniqueName = pigeonVar_list[0] as! String + let taskName = pigeonVar_list[1] as! String + let inputData: [String?: Any?]? = nilOrValue(pigeonVar_list[2]) + let initialDelaySeconds: Int64? = nilOrValue(pigeonVar_list[3]) + let constraints: Constraints? = nilOrValue(pigeonVar_list[4]) + let backoffPolicy: BackoffPolicyConfig? = nilOrValue(pigeonVar_list[5]) + let tag: String? = nilOrValue(pigeonVar_list[6]) + let existingWorkPolicy: ExistingWorkPolicy? = nilOrValue(pigeonVar_list[7]) + let outOfQuotaPolicy: OutOfQuotaPolicy? = nilOrValue(pigeonVar_list[8]) + + return OneOffTaskRequest( + uniqueName: uniqueName, + taskName: taskName, + inputData: inputData, + initialDelaySeconds: initialDelaySeconds, + constraints: constraints, + backoffPolicy: backoffPolicy, + tag: tag, + existingWorkPolicy: existingWorkPolicy, + outOfQuotaPolicy: outOfQuotaPolicy + ) + } + func toList() -> [Any?] { + return [ + uniqueName, + taskName, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + outOfQuotaPolicy, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PeriodicTaskRequest { + var uniqueName: String + var taskName: String + var frequencySeconds: Int64 + var flexIntervalSeconds: Int64? = nil + var inputData: [String?: Any?]? = nil + var initialDelaySeconds: Int64? = nil + var constraints: Constraints? = nil + var backoffPolicy: BackoffPolicyConfig? = nil + var tag: String? = nil + var existingWorkPolicy: ExistingWorkPolicy? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PeriodicTaskRequest? { + let uniqueName = pigeonVar_list[0] as! String + let taskName = pigeonVar_list[1] as! String + let frequencySeconds = pigeonVar_list[2] as! Int64 + let flexIntervalSeconds: Int64? = nilOrValue(pigeonVar_list[3]) + let inputData: [String?: Any?]? = nilOrValue(pigeonVar_list[4]) + let initialDelaySeconds: Int64? = nilOrValue(pigeonVar_list[5]) + let constraints: Constraints? = nilOrValue(pigeonVar_list[6]) + let backoffPolicy: BackoffPolicyConfig? = nilOrValue(pigeonVar_list[7]) + let tag: String? = nilOrValue(pigeonVar_list[8]) + let existingWorkPolicy: ExistingWorkPolicy? = nilOrValue(pigeonVar_list[9]) + + return PeriodicTaskRequest( + uniqueName: uniqueName, + taskName: taskName, + frequencySeconds: frequencySeconds, + flexIntervalSeconds: flexIntervalSeconds, + inputData: inputData, + initialDelaySeconds: initialDelaySeconds, + constraints: constraints, + backoffPolicy: backoffPolicy, + tag: tag, + existingWorkPolicy: existingWorkPolicy + ) + } + func toList() -> [Any?] { + return [ + uniqueName, + taskName, + frequencySeconds, + flexIntervalSeconds, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct ProcessingTaskRequest { + var uniqueName: String + var taskName: String + var inputData: [String?: Any?]? = nil + var initialDelaySeconds: Int64? = nil + var networkType: NetworkType? = nil + var requiresCharging: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ProcessingTaskRequest? { + let uniqueName = pigeonVar_list[0] as! String + let taskName = pigeonVar_list[1] as! String + let inputData: [String?: Any?]? = nilOrValue(pigeonVar_list[2]) + let initialDelaySeconds: Int64? = nilOrValue(pigeonVar_list[3]) + let networkType: NetworkType? = nilOrValue(pigeonVar_list[4]) + let requiresCharging: Bool? = nilOrValue(pigeonVar_list[5]) + + return ProcessingTaskRequest( + uniqueName: uniqueName, + taskName: taskName, + inputData: inputData, + initialDelaySeconds: initialDelaySeconds, + networkType: networkType, + requiresCharging: requiresCharging + ) + } + func toList() -> [Any?] { + return [ + uniqueName, + taskName, + inputData, + initialDelaySeconds, + networkType, + requiresCharging, + ] + } +} + +private class WorkmanagerApiPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return NetworkType(rawValue: enumResultAsInt) + } + return nil + case 130: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return BackoffPolicy(rawValue: enumResultAsInt) + } + return nil + case 131: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return ExistingWorkPolicy(rawValue: enumResultAsInt) + } + return nil + case 132: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return OutOfQuotaPolicy(rawValue: enumResultAsInt) + } + return nil + case 133: + return Constraints.fromList(self.readValue() as! [Any?]) + case 134: + return BackoffPolicyConfig.fromList(self.readValue() as! [Any?]) + case 135: + return InitializeRequest.fromList(self.readValue() as! [Any?]) + case 136: + return OneOffTaskRequest.fromList(self.readValue() as! [Any?]) + case 137: + return PeriodicTaskRequest.fromList(self.readValue() as! [Any?]) + case 138: + return ProcessingTaskRequest.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class WorkmanagerApiPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? NetworkType { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? BackoffPolicy { + super.writeByte(130) + super.writeValue(value.rawValue) + } else if let value = value as? ExistingWorkPolicy { + super.writeByte(131) + super.writeValue(value.rawValue) + } else if let value = value as? OutOfQuotaPolicy { + super.writeByte(132) + super.writeValue(value.rawValue) + } else if let value = value as? Constraints { + super.writeByte(133) + super.writeValue(value.toList()) + } else if let value = value as? BackoffPolicyConfig { + super.writeByte(134) + super.writeValue(value.toList()) + } else if let value = value as? InitializeRequest { + super.writeByte(135) + super.writeValue(value.toList()) + } else if let value = value as? OneOffTaskRequest { + super.writeByte(136) + super.writeValue(value.toList()) + } else if let value = value as? PeriodicTaskRequest { + super.writeByte(137) + super.writeValue(value.toList()) + } else if let value = value as? ProcessingTaskRequest { + super.writeByte(138) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class WorkmanagerApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return WorkmanagerApiPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return WorkmanagerApiPigeonCodecWriter(data: data) + } +} + +class WorkmanagerApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = WorkmanagerApiPigeonCodec(readerWriter: WorkmanagerApiPigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol WorkmanagerHostApi { + func initialize(request: InitializeRequest, completion: @escaping (Result) -> Void) + func registerOneOffTask(request: OneOffTaskRequest, completion: @escaping (Result) -> Void) + func registerPeriodicTask(request: PeriodicTaskRequest, completion: @escaping (Result) -> Void) + func registerProcessingTask(request: ProcessingTaskRequest, completion: @escaping (Result) -> Void) + func cancelByUniqueName(uniqueName: String, completion: @escaping (Result) -> Void) + func cancelByTag(tag: String, completion: @escaping (Result) -> Void) + func cancelAll(completion: @escaping (Result) -> Void) + func isScheduledByUniqueName(uniqueName: String, completion: @escaping (Result) -> Void) + func printScheduledTasks(completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class WorkmanagerHostApiSetup { + static var codec: FlutterStandardMessageCodec { WorkmanagerApiPigeonCodec.shared } + /// Sets up an instance of `WorkmanagerHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: WorkmanagerHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let initializeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.initialize\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + initializeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! InitializeRequest + api.initialize(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + initializeChannel.setMessageHandler(nil) + } + let registerOneOffTaskChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerOneOffTask\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + registerOneOffTaskChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! OneOffTaskRequest + api.registerOneOffTask(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + registerOneOffTaskChannel.setMessageHandler(nil) + } + let registerPeriodicTaskChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerPeriodicTask\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + registerPeriodicTaskChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! PeriodicTaskRequest + api.registerPeriodicTask(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + registerPeriodicTaskChannel.setMessageHandler(nil) + } + let registerProcessingTaskChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerProcessingTask\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + registerProcessingTaskChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestArg = args[0] as! ProcessingTaskRequest + api.registerProcessingTask(request: requestArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + registerProcessingTaskChannel.setMessageHandler(nil) + } + let cancelByUniqueNameChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByUniqueName\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + cancelByUniqueNameChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let uniqueNameArg = args[0] as! String + api.cancelByUniqueName(uniqueName: uniqueNameArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + cancelByUniqueNameChannel.setMessageHandler(nil) + } + let cancelByTagChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByTag\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + cancelByTagChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let tagArg = args[0] as! String + api.cancelByTag(tag: tagArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + cancelByTagChannel.setMessageHandler(nil) + } + let cancelAllChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelAll\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + cancelAllChannel.setMessageHandler { _, reply in + api.cancelAll { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + cancelAllChannel.setMessageHandler(nil) + } + let isScheduledByUniqueNameChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.isScheduledByUniqueName\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isScheduledByUniqueNameChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let uniqueNameArg = args[0] as! String + api.isScheduledByUniqueName(uniqueName: uniqueNameArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isScheduledByUniqueNameChannel.setMessageHandler(nil) + } + let printScheduledTasksChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.printScheduledTasks\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + printScheduledTasksChannel.setMessageHandler { _, reply in + api.printScheduledTasks { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + printScheduledTasksChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol WorkmanagerFlutterApiProtocol { + func backgroundChannelInitialized(completion: @escaping (Result) -> Void) + func executeTask(taskName taskNameArg: String, inputData inputDataArg: [String?: Any?]?, completion: @escaping (Result) -> Void) +} +class WorkmanagerFlutterApi: WorkmanagerFlutterApiProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + var codec: WorkmanagerApiPigeonCodec { + return WorkmanagerApiPigeonCodec.shared + } + func backgroundChannelInitialized(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.backgroundChannelInitialized\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func executeTask(taskName taskNameArg: String, inputData inputDataArg: [String?: Any?]?, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.executeTask\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([taskNameArg, inputDataArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else if listResponse[0] == nil { + completion(.failure(PigeonError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: ""))) + } else { + let result = listResponse[0] as! Bool + completion(.success(result)) + } + } + } +} diff --git a/workmanager_platform_interface/lib/src/enums.dart b/workmanager_platform_interface/lib/src/enums.dart deleted file mode 100644 index 0254126b..00000000 --- a/workmanager_platform_interface/lib/src/enums.dart +++ /dev/null @@ -1,26 +0,0 @@ -/// Enum for specifying the frequency at which periodic work repeats. -enum Frequency { - /// When no frequency is given. - never, - - /// Work repeats with a minimal interval of 15 minutes. - min15minutes, - - /// Work repeats with an interval of 30 minutes. - min30minutes, - - /// Work repeats with an interval of 1 hour. - hourly, - - /// Work repeats with an interval of 6 hours. - sixHourly, - - /// Work repeats with an interval of 12 hours. - twelveHourly, - - /// Work repeats with an interval of 1 day. - daily, - - /// Work repeats with an interval of 1 week. - weekly, -} diff --git a/workmanager_platform_interface/lib/src/options.dart b/workmanager_platform_interface/lib/src/options.dart deleted file mode 100644 index b23ec4cd..00000000 --- a/workmanager_platform_interface/lib/src/options.dart +++ /dev/null @@ -1,103 +0,0 @@ -/// An enumeration of the conflict resolution policies in case of a collision. -enum ExistingWorkPolicy { - /// If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as a child of all the leaves of that work sequence. - append, - - /// If there is existing pending (uncompleted) work with the same unique name, it will be updated the new specification. - update, - - /// If there is existing pending (uncompleted) work with the same unique name, do nothing. - keep, - - /// If there is existing pending (uncompleted) work with the same unique name, cancel and delete it. - replace -} - -/// An enumeration of various network types that can be used as Constraints for work. -/// -/// Fully supported on Android. -/// -/// On iOS, this enumeration is used to define whether a piece of work requires -/// internet connectivity, by checking for either [NetworkType.connected] or -/// [NetworkType.metered]. -enum NetworkType { - /// Any working network connection is required for this work. - connected, - - /// A metered network connection is required for this work. - metered, - - /// Default value. A network is not required for this work. - notRequired, - - /// A non-roaming network connection is required for this work. - notRoaming, - - /// An unmetered network connection is required for this work. - unmetered, - - /// A temporarily unmetered Network. This capability will be set for - /// networks that are generally metered, but are currently unmetered. - /// - /// Android API 30+ - temporarilyUnmetered, -} - -/// An enumeration of policies that help determine out of quota behavior for expedited jobs. -/// -/// Only supported on Android. -enum OutOfQuotaPolicy { - /// When the app does not have any expedited job quota, the expedited work request will - /// fallback to a regular work request. - runAsNonExpeditedWorkRequest, - - /// When the app does not have any expedited job quota, the expedited work request will - /// we dropped and no work requests are enqueued. - dropWorkRequest, -} - -/// An enumeration of backoff policies when retrying work. -/// These policies are used when you have a return ListenableWorker.Result.retry() from a worker to determine the correct backoff time. -/// Backoff policies are set in WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit) or one of its variants. -enum BackoffPolicy { - /// Used to indicate that WorkManager should increase the backoff time exponentially - exponential, - - /// Used to indicate that WorkManager should increase the backoff time linearly - linear -} - -/// A specification of the requirements that need to be met before a WorkRequest can run. -/// By default, WorkRequests do not have any requirements and can run immediately. -/// By adding requirements, you can make sure that work only runs in certain situations - -/// for example, when you have an unmetered network and are charging. -class Constraints { - /// An enumeration of various network types that can be used as Constraints for work. - final NetworkType networkType; - - /// `true` if the work should only execute when the battery isn't low. - /// - /// Only supported on Android. - final bool? requiresBatteryNotLow; - - /// `true` if the work should only execute while the device is charging. - final bool? requiresCharging; - - /// `true` if the work should only execute while the device is idle. - /// - /// Only supported on Android. - final bool? requiresDeviceIdle; - - /// `true` if the work should only execute when the storage isn't low. - /// - /// Only supported on Android. - final bool? requiresStorageNotLow; - - Constraints({ - required this.networkType, - this.requiresBatteryNotLow, - this.requiresCharging, - this.requiresDeviceIdle, - this.requiresStorageNotLow, - }); -} diff --git a/workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart b/workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart new file mode 100644 index 00000000..2ecbfe18 --- /dev/null +++ b/workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart @@ -0,0 +1,710 @@ +// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved. +// // Use of this source code is governed by a MIT-style license that can be +// // found in the LICENSE file. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +/// An enumeration of various network types that can be used as Constraints for work. +/// +/// Fully supported on Android. +/// +/// On iOS, this enumeration is used to define whether a piece of work requires +/// internet connectivity, by checking for either [NetworkType.connected] or +/// [NetworkType.metered]. +enum NetworkType { + /// Any working network connection is required for this work. + connected, + /// A metered network connection is required for this work. + metered, + /// Default value. A network is not required for this work. + notRequired, + /// A non-roaming network connection is required for this work. + notRoaming, + /// An unmetered network connection is required for this work. + unmetered, + /// A temporarily unmetered Network. This capability will be set for + /// networks that are generally metered, but are currently unmetered. + /// + /// Android API 30+ + temporarilyUnmetered, +} + +/// An enumeration of backoff policies when retrying work. +/// These policies are used when you have a return ListenableWorker.Result.retry() from a worker to determine the correct backoff time. +/// Backoff policies are set in WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit) or one of its variants. +enum BackoffPolicy { + /// Used to indicate that WorkManager should increase the backoff time exponentially + exponential, + /// Used to indicate that WorkManager should increase the backoff time linearly + linear, +} + +/// An enumeration of the conflict resolution policies in case of a collision. +enum ExistingWorkPolicy { + /// If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as a child of all the leaves of that work sequence. + append, + /// If there is existing pending (uncompleted) work with the same unique name, do nothing. + keep, + /// If there is existing pending (uncompleted) work with the same unique name, cancel and delete it. + replace, + /// If there is existing pending (uncompleted) work with the same unique name, it will be updated the new specification. + /// Note: This maps to appendOrReplace in the native implementation. + update, +} + +/// An enumeration of policies that help determine out of quota behavior for expedited jobs. +/// +/// Only supported on Android. +enum OutOfQuotaPolicy { + /// When the app does not have any expedited job quota, the expedited work request will + /// fallback to a regular work request. + runAsNonExpeditedWorkRequest, + /// When the app does not have any expedited job quota, the expedited work request will + /// we dropped and no work requests are enqueued. + dropWorkRequest, +} + +class Constraints { + Constraints({ + this.networkType, + this.requiresBatteryNotLow, + this.requiresCharging, + this.requiresDeviceIdle, + this.requiresStorageNotLow, + }); + + NetworkType? networkType; + + bool? requiresBatteryNotLow; + + bool? requiresCharging; + + bool? requiresDeviceIdle; + + bool? requiresStorageNotLow; + + Object encode() { + return [ + networkType, + requiresBatteryNotLow, + requiresCharging, + requiresDeviceIdle, + requiresStorageNotLow, + ]; + } + + static Constraints decode(Object result) { + result as List; + return Constraints( + networkType: result[0] as NetworkType?, + requiresBatteryNotLow: result[1] as bool?, + requiresCharging: result[2] as bool?, + requiresDeviceIdle: result[3] as bool?, + requiresStorageNotLow: result[4] as bool?, + ); + } +} + +class BackoffPolicyConfig { + BackoffPolicyConfig({ + this.backoffPolicy, + this.backoffDelayMillis, + }); + + BackoffPolicy? backoffPolicy; + + int? backoffDelayMillis; + + Object encode() { + return [ + backoffPolicy, + backoffDelayMillis, + ]; + } + + static BackoffPolicyConfig decode(Object result) { + result as List; + return BackoffPolicyConfig( + backoffPolicy: result[0] as BackoffPolicy?, + backoffDelayMillis: result[1] as int?, + ); + } +} + +class InitializeRequest { + InitializeRequest({ + required this.callbackHandle, + required this.isInDebugMode, + }); + + int callbackHandle; + + bool isInDebugMode; + + Object encode() { + return [ + callbackHandle, + isInDebugMode, + ]; + } + + static InitializeRequest decode(Object result) { + result as List; + return InitializeRequest( + callbackHandle: result[0]! as int, + isInDebugMode: result[1]! as bool, + ); + } +} + +class OneOffTaskRequest { + OneOffTaskRequest({ + required this.uniqueName, + required this.taskName, + this.inputData, + this.initialDelaySeconds, + this.constraints, + this.backoffPolicy, + this.tag, + this.existingWorkPolicy, + this.outOfQuotaPolicy, + }); + + String uniqueName; + + String taskName; + + Map? inputData; + + int? initialDelaySeconds; + + Constraints? constraints; + + BackoffPolicyConfig? backoffPolicy; + + String? tag; + + ExistingWorkPolicy? existingWorkPolicy; + + OutOfQuotaPolicy? outOfQuotaPolicy; + + Object encode() { + return [ + uniqueName, + taskName, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + outOfQuotaPolicy, + ]; + } + + static OneOffTaskRequest decode(Object result) { + result as List; + return OneOffTaskRequest( + uniqueName: result[0]! as String, + taskName: result[1]! as String, + inputData: (result[2] as Map?)?.cast(), + initialDelaySeconds: result[3] as int?, + constraints: result[4] as Constraints?, + backoffPolicy: result[5] as BackoffPolicyConfig?, + tag: result[6] as String?, + existingWorkPolicy: result[7] as ExistingWorkPolicy?, + outOfQuotaPolicy: result[8] as OutOfQuotaPolicy?, + ); + } +} + +class PeriodicTaskRequest { + PeriodicTaskRequest({ + required this.uniqueName, + required this.taskName, + required this.frequencySeconds, + this.flexIntervalSeconds, + this.inputData, + this.initialDelaySeconds, + this.constraints, + this.backoffPolicy, + this.tag, + this.existingWorkPolicy, + }); + + String uniqueName; + + String taskName; + + int frequencySeconds; + + int? flexIntervalSeconds; + + Map? inputData; + + int? initialDelaySeconds; + + Constraints? constraints; + + BackoffPolicyConfig? backoffPolicy; + + String? tag; + + ExistingWorkPolicy? existingWorkPolicy; + + Object encode() { + return [ + uniqueName, + taskName, + frequencySeconds, + flexIntervalSeconds, + inputData, + initialDelaySeconds, + constraints, + backoffPolicy, + tag, + existingWorkPolicy, + ]; + } + + static PeriodicTaskRequest decode(Object result) { + result as List; + return PeriodicTaskRequest( + uniqueName: result[0]! as String, + taskName: result[1]! as String, + frequencySeconds: result[2]! as int, + flexIntervalSeconds: result[3] as int?, + inputData: (result[4] as Map?)?.cast(), + initialDelaySeconds: result[5] as int?, + constraints: result[6] as Constraints?, + backoffPolicy: result[7] as BackoffPolicyConfig?, + tag: result[8] as String?, + existingWorkPolicy: result[9] as ExistingWorkPolicy?, + ); + } +} + +class ProcessingTaskRequest { + ProcessingTaskRequest({ + required this.uniqueName, + required this.taskName, + this.inputData, + this.initialDelaySeconds, + this.networkType, + this.requiresCharging, + }); + + String uniqueName; + + String taskName; + + Map? inputData; + + int? initialDelaySeconds; + + NetworkType? networkType; + + bool? requiresCharging; + + Object encode() { + return [ + uniqueName, + taskName, + inputData, + initialDelaySeconds, + networkType, + requiresCharging, + ]; + } + + static ProcessingTaskRequest decode(Object result) { + result as List; + return ProcessingTaskRequest( + uniqueName: result[0]! as String, + taskName: result[1]! as String, + inputData: (result[2] as Map?)?.cast(), + initialDelaySeconds: result[3] as int?, + networkType: result[4] as NetworkType?, + requiresCharging: result[5] as bool?, + ); + } +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is NetworkType) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is BackoffPolicy) { + buffer.putUint8(130); + writeValue(buffer, value.index); + } else if (value is ExistingWorkPolicy) { + buffer.putUint8(131); + writeValue(buffer, value.index); + } else if (value is OutOfQuotaPolicy) { + buffer.putUint8(132); + writeValue(buffer, value.index); + } else if (value is Constraints) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is BackoffPolicyConfig) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is InitializeRequest) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is OneOffTaskRequest) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is PeriodicTaskRequest) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is ProcessingTaskRequest) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : NetworkType.values[value]; + case 130: + final int? value = readValue(buffer) as int?; + return value == null ? null : BackoffPolicy.values[value]; + case 131: + final int? value = readValue(buffer) as int?; + return value == null ? null : ExistingWorkPolicy.values[value]; + case 132: + final int? value = readValue(buffer) as int?; + return value == null ? null : OutOfQuotaPolicy.values[value]; + case 133: + return Constraints.decode(readValue(buffer)!); + case 134: + return BackoffPolicyConfig.decode(readValue(buffer)!); + case 135: + return InitializeRequest.decode(readValue(buffer)!); + case 136: + return OneOffTaskRequest.decode(readValue(buffer)!); + case 137: + return PeriodicTaskRequest.decode(readValue(buffer)!); + case 138: + return ProcessingTaskRequest.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WorkmanagerHostApi { + /// Constructor for [WorkmanagerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WorkmanagerHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future initialize(InitializeRequest request) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.initialize$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([request]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future registerOneOffTask(OneOffTaskRequest request) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerOneOffTask$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([request]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future registerPeriodicTask(PeriodicTaskRequest request) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerPeriodicTask$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([request]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future registerProcessingTask(ProcessingTaskRequest request) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerProcessingTask$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([request]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future cancelByUniqueName(String uniqueName) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByUniqueName$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([uniqueName]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future cancelByTag(String tag) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByTag$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([tag]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future cancelAll() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelAll$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future isScheduledByUniqueName(String uniqueName) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.isScheduledByUniqueName$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([uniqueName]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future printScheduledTasks() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.printScheduledTasks$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as String?)!; + } + } +} + +abstract class WorkmanagerFlutterApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + Future backgroundChannelInitialized(); + + Future executeTask(String taskName, Map? inputData); + + static void setUp(WorkmanagerFlutterApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.backgroundChannelInitialized$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + await api.backgroundChannelInitialized(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.executeTask$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.executeTask was null.'); + final List args = (message as List?)!; + final String? arg_taskName = (args[0] as String?); + assert(arg_taskName != null, + 'Argument for dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi.executeTask was null, expected non-null String.'); + final Map? arg_inputData = (args[1] as Map?)?.cast(); + try { + final bool output = await api.executeTask(arg_taskName!, arg_inputData); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/workmanager_platform_interface/lib/src/workmanager_platform_interface.dart b/workmanager_platform_interface/lib/src/workmanager_platform_interface.dart index 0551beb0..c76bf463 100644 --- a/workmanager_platform_interface/lib/src/workmanager_platform_interface.dart +++ b/workmanager_platform_interface/lib/src/workmanager_platform_interface.dart @@ -1,6 +1,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'options.dart'; +import 'pigeon/workmanager_api.g.dart'; /// The interface that implementations of workmanager must implement. /// diff --git a/workmanager_platform_interface/lib/workmanager_platform_interface.dart b/workmanager_platform_interface/lib/workmanager_platform_interface.dart index 5efa30ac..8bae37ce 100644 --- a/workmanager_platform_interface/lib/workmanager_platform_interface.dart +++ b/workmanager_platform_interface/lib/workmanager_platform_interface.dart @@ -1,5 +1,4 @@ library workmanager_platform_interface; export 'src/workmanager_platform_interface.dart'; -export 'src/options.dart'; -export 'src/enums.dart'; +export 'src/pigeon/workmanager_api.g.dart'; diff --git a/workmanager_platform_interface/pigeons/copyright.txt b/workmanager_platform_interface/pigeons/copyright.txt new file mode 100644 index 00000000..177dbcb9 --- /dev/null +++ b/workmanager_platform_interface/pigeons/copyright.txt @@ -0,0 +1,3 @@ +// Copyright 2024 The Flutter Workmanager Authors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. \ No newline at end of file diff --git a/workmanager_platform_interface/pigeons/workmanager_api.dart b/workmanager_platform_interface/pigeons/workmanager_api.dart new file mode 100644 index 00000000..249443ab --- /dev/null +++ b/workmanager_platform_interface/pigeons/workmanager_api.dart @@ -0,0 +1,230 @@ +import 'package:pigeon/pigeon.dart'; + +// Pigeon configuration +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/pigeon/workmanager_api.g.dart', + dartOptions: DartOptions(), + kotlinOut: '../workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt', + kotlinOptions: KotlinOptions( + package: 'dev.fluttercommunity.workmanager.pigeon', + ), + swiftOut: '../workmanager_apple/ios/Classes/pigeon/WorkmanagerApi.g.swift', + copyrightHeader: 'pigeons/copyright.txt', + dartPackageName: 'workmanager_platform_interface', +)) + +// Enums - Moved from platform interface for Pigeon compatibility + +/// An enumeration of various network types that can be used as Constraints for work. +/// +/// Fully supported on Android. +/// +/// On iOS, this enumeration is used to define whether a piece of work requires +/// internet connectivity, by checking for either [NetworkType.connected] or +/// [NetworkType.metered]. +enum NetworkType { + /// Any working network connection is required for this work. + connected, + + /// A metered network connection is required for this work. + metered, + + /// Default value. A network is not required for this work. + notRequired, + + /// A non-roaming network connection is required for this work. + notRoaming, + + /// An unmetered network connection is required for this work. + unmetered, + + /// A temporarily unmetered Network. This capability will be set for + /// networks that are generally metered, but are currently unmetered. + /// + /// Android API 30+ + temporarilyUnmetered, +} + +/// An enumeration of backoff policies when retrying work. +/// These policies are used when you have a return ListenableWorker.Result.retry() from a worker to determine the correct backoff time. +/// Backoff policies are set in WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit) or one of its variants. +enum BackoffPolicy { + /// Used to indicate that WorkManager should increase the backoff time exponentially + exponential, + + /// Used to indicate that WorkManager should increase the backoff time linearly + linear, +} + +/// An enumeration of the conflict resolution policies in case of a collision. +enum ExistingWorkPolicy { + /// If there is existing pending (uncompleted) work with the same unique name, append the newly-specified work as a child of all the leaves of that work sequence. + append, + + /// If there is existing pending (uncompleted) work with the same unique name, do nothing. + keep, + + /// If there is existing pending (uncompleted) work with the same unique name, cancel and delete it. + replace, + + /// If there is existing pending (uncompleted) work with the same unique name, it will be updated the new specification. + /// Note: This maps to appendOrReplace in the native implementation. + update, +} + +/// An enumeration of policies that help determine out of quota behavior for expedited jobs. +/// +/// Only supported on Android. +enum OutOfQuotaPolicy { + /// When the app does not have any expedited job quota, the expedited work request will + /// fallback to a regular work request. + runAsNonExpeditedWorkRequest, + + /// When the app does not have any expedited job quota, the expedited work request will + /// we dropped and no work requests are enqueued. + dropWorkRequest, +} + +// Data classes +class Constraints { + Constraints({ + this.networkType, + this.requiresBatteryNotLow, + this.requiresCharging, + this.requiresDeviceIdle, + this.requiresStorageNotLow, + }); + + NetworkType? networkType; + bool? requiresBatteryNotLow; + bool? requiresCharging; + bool? requiresDeviceIdle; + bool? requiresStorageNotLow; +} + +class BackoffPolicyConfig { + BackoffPolicyConfig({ + this.backoffPolicy, + this.backoffDelayMillis, + }); + + BackoffPolicy? backoffPolicy; + int? backoffDelayMillis; +} + +class InitializeRequest { + InitializeRequest({required this.callbackHandle, required this.isInDebugMode}); + + int callbackHandle; + bool isInDebugMode; +} + +class OneOffTaskRequest { + OneOffTaskRequest({ + required this.uniqueName, + required this.taskName, + this.inputData, + this.initialDelaySeconds, + this.constraints, + this.backoffPolicy, + this.tag, + this.existingWorkPolicy, + this.outOfQuotaPolicy, + }); + + String uniqueName; + String taskName; + Map? inputData; + int? initialDelaySeconds; + Constraints? constraints; + BackoffPolicyConfig? backoffPolicy; + String? tag; + ExistingWorkPolicy? existingWorkPolicy; + OutOfQuotaPolicy? outOfQuotaPolicy; +} + +class PeriodicTaskRequest { + PeriodicTaskRequest({ + required this.uniqueName, + required this.taskName, + required this.frequencySeconds, + this.flexIntervalSeconds, + this.inputData, + this.initialDelaySeconds, + this.constraints, + this.backoffPolicy, + this.tag, + this.existingWorkPolicy, + }); + + String uniqueName; + String taskName; + int frequencySeconds; + int? flexIntervalSeconds; + Map? inputData; + int? initialDelaySeconds; + Constraints? constraints; + BackoffPolicyConfig? backoffPolicy; + String? tag; + ExistingWorkPolicy? existingWorkPolicy; +} + +// iOS specific request +class ProcessingTaskRequest { + ProcessingTaskRequest({ + required this.uniqueName, + required this.taskName, + this.inputData, + this.initialDelaySeconds, + this.networkType, + this.requiresCharging, + }); + + String uniqueName; + String taskName; + Map? inputData; + int? initialDelaySeconds; + NetworkType? networkType; + bool? requiresCharging; +} + +// Host API (Flutter calls native) +@HostApi() +abstract class WorkmanagerHostApi { + @async + void initialize(InitializeRequest request); + + @async + void registerOneOffTask(OneOffTaskRequest request); + + @async + void registerPeriodicTask(PeriodicTaskRequest request); + + @async + void registerProcessingTask(ProcessingTaskRequest request); + + @async + void cancelByUniqueName(String uniqueName); + + @async + void cancelByTag(String tag); + + @async + void cancelAll(); + + @async + bool isScheduledByUniqueName(String uniqueName); + + @async + String printScheduledTasks(); +} + +// Flutter API (Native calls Flutter) +@FlutterApi() +abstract class WorkmanagerFlutterApi { + @async + void backgroundChannelInitialized(); + + @async + bool executeTask(String taskName, Map? inputData); +} \ No newline at end of file diff --git a/workmanager_platform_interface/pubspec.yaml b/workmanager_platform_interface/pubspec.yaml index 81237076..5a0ce94b 100644 --- a/workmanager_platform_interface/pubspec.yaml +++ b/workmanager_platform_interface/pubspec.yaml @@ -20,4 +20,5 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 + pigeon: ^22.6.0