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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
.idea/
.packages
.pub/
.build/
.swiftpm/

/pubspec.lock
build/
2 changes: 1 addition & 1 deletion ios/flutter_foreground_task.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A new Flutter plugin.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '8.0'
s.platform = :ios, '12.0'

# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
Expand Down
20 changes: 20 additions & 0 deletions ios/flutter_foreground_task/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "flutter_foreground_task",
platforms: [
.iOS("12.0")
],
products: [
.library(name: "flutter-foreground-task", targets: ["flutter_foreground_task"])
],
dependencies: [],
targets: [
.target(
name: "flutter_foreground_task",
dependencies: [],
path: "Sources/flutter_foreground_task"
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// FlutterForegroundTaskLifecycleListener.swift
// flutter_foreground_task
//
// Created by Woo Jin Hwang on 7/15/24.
//

import Foundation
import Flutter

/** A listener that can listen to the task lifecycle events. */
public protocol FlutterForegroundTaskLifecycleListener : AnyObject {
/**
* Each time a task starts, a new FlutterEngine is created.
*
* This is called before onTaskStart, Initialize the service you want to use in the task. (like PlatformChannel initialization)
*/
func onEngineCreate(flutterEngine: FlutterEngine?)

/** Called when the task is started. */
func onTaskStart(starter: FlutterForegroundTaskStarter)

/** Called based on the eventAction set in ForegroundTaskOptions. */
func onTaskRepeatEvent()

/** Called when the task is destroyed. */
func onTaskDestroy()

/**
* If one task is finished or replaced by another, the FlutterEngine is destroyed.
*
* This is called after onTaskDestroy, where dispose the service that was initialized in onEngineCreate.
*/
func onEngineWillDestroy()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// FlutterForegroundTaskStarter.swift
// flutter_foreground_task
//
// Created by Woo Jin Hwang on 9/23/24.
//

import Foundation

public enum FlutterForegroundTaskStarter : Int {
case DEVELOPER = 0
case SYSTEM = 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// BackgroundServicePrefsKey.swift
// flutter_foreground_task
//
// Created by WOO JIN HWANG on 2021/08/11.
//

// service status
let BACKGROUND_SERVICE_ACTION = "backgroundServiceAction"

// notification options
let SHOW_NOTIFICATION = "showNotification"
let PLAY_SOUND = "playSound"

// notification content
let NOTIFICATION_CONTENT_TITLE = "notificationContentTitle"
let NOTIFICATION_CONTENT_TEXT = "notificationContentText"
let NOTIFICATION_CONTENT_BUTTONS = "buttons"

// task options
let TASK_EVENT_ACTION = "taskEventAction" // new
let INTERVAL = "interval" // deprecated
let IS_ONCE_EVENT = "isOnceEvent" // deprecated

// task data
let CALLBACK_HANDLE = "callbackHandle"
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import Flutter
import UIKit
import BackgroundTasks

public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin {
// ====================== Plugin ======================
static private(set) var registerPlugins: FlutterPluginRegistrantCallback? = nil

private var notificationPermissionManager: NotificationPermissionManager? = nil
private var backgroundServiceManager: BackgroundServiceManager? = nil

private var foregroundChannel: FlutterMethodChannel? = nil

public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftFlutterForegroundTaskPlugin()
instance.initServices()
instance.initChannels(registrar.messenger())
registrar.addApplicationDelegate(instance)
}

public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) {
registerPlugins = callback
}

public static func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) {
BackgroundService.sharedInstance.addTaskLifecycleListener(listener)
}

public static func removeTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) {
BackgroundService.sharedInstance.removeTaskLifecycleListener(listener)
}

private func initServices() {
notificationPermissionManager = NotificationPermissionManager()
backgroundServiceManager = BackgroundServiceManager()
}

private func initChannels(_ messenger: FlutterBinaryMessenger) {
foregroundChannel = FlutterMethodChannel(name: "flutter_foreground_task/methods", binaryMessenger: messenger)
foregroundChannel?.setMethodCallHandler(onMethodCall)
}

private func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments
do {
switch call.method {
case "checkNotificationPermission":
notificationPermissionManager!.checkPermission { permission in
result(permission.rawValue)
}
case "requestNotificationPermission":
notificationPermissionManager!.requestPermission { permission in
result(permission.rawValue)
}
case "startService":
try backgroundServiceManager!.start(arguments: args)
result(true)
case "restartService":
try backgroundServiceManager!.restart(arguments: args)
result(true)
case "updateService":
try backgroundServiceManager!.update(arguments: args)
result(true)
case "stopService":
try backgroundServiceManager!.stop()
result(true)
case "sendData":
backgroundServiceManager!.sendData(data: args)
case "isRunningService":
result(backgroundServiceManager!.isRunningService())
case "minimizeApp":
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
case "isAppOnForeground":
result(UIApplication.shared.applicationState == .active)
default:
result(FlutterMethodNotImplemented)
}
} catch {
let code = String(describing: error.self)
let message = error.localizedDescription
let flutterError = FlutterError(code: code, message: message, details: nil)
result(flutterError)
}
}

// ================== App Lifecycle ===================
public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable : Any] = [:]) -> Bool {
UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
if #available(iOS 13.0, *) {
SwiftFlutterForegroundTaskPlugin.registerAppRefresh()
}
return true
}

public func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool {
completionHandler(.newData)
return true
}

public func applicationDidEnterBackground(_ application: UIApplication) {
if #available(iOS 13.0, *) {
SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh()
}
}

public func applicationWillTerminate(_ application: UIApplication) {
if !BackgroundService.sharedInstance.isRunningService {
return
}

BackgroundServiceStatus.setData(action: BackgroundServiceAction.APP_TERMINATE)
BackgroundService.sharedInstance.run()

// Chance to handle onDestroy before app terminates
sleep(5)
}

// ================= Service Delegate =================
@available(iOS 10.0, *)
public func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
BackgroundService.sharedInstance.userNotificationCenter(center, response, completionHandler)
}

@available(iOS 10.0, *)
public func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
BackgroundService.sharedInstance.userNotificationCenter(center, notification, completionHandler)
}

// ============== Background App Refresh ==============
public static var refreshIdentifier: String = "com.pravera.flutter_foreground_task.refresh"

@available(iOS 13.0, *)
private static func registerAppRefresh() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshIdentifier, using: nil) { task in
handleAppRefresh(task: task as! BGAppRefreshTask)
}
}

@available(iOS 13.0, *)
private static func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: refreshIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)

do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}

@available(iOS 13.0, *)
private static func cancelAppRefresh() {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshIdentifier)
}

@available(iOS 13.0, *)
private static func handleAppRefresh(task: BGAppRefreshTask) {
let queue = OperationQueue()
let operation = AppRefreshOperation()

task.expirationHandler = {
operation.cancel()
}

operation.completionBlock = {
// Schedule a new refresh task
scheduleAppRefresh()

task.setTaskCompleted(success: true)
}

queue.addOperation(operation)
}
}

class AppRefreshOperation: Operation {
override func main() {
let semaphore = DispatchSemaphore(value: 0)

// avoid non-platform thread
DispatchQueue.main.asyncAfter(deadline: .now() + 25) {
semaphore.signal()
}

semaphore.wait()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// ServiceError.swift
// flutter_foreground_task
//
// Created by Woo Jin Hwang on 7/17/24.
//

import Foundation

enum ServiceError {
case ServiceArgumentNullException
case ServiceAlreadyStartedException
case ServiceNotStartedException
case ServiceNotSupportedException
}

extension ServiceError : LocalizedError {
public var errorDescription: String? {
switch self {
case .ServiceArgumentNullException:
return NSLocalizedString("The required argument was not passed to the service.", comment: "ServiceArgumentNullException")
case .ServiceAlreadyStartedException:
return NSLocalizedString("The service has already started.", comment: "ServiceAlreadyStartedException")
case .ServiceNotStartedException:
return NSLocalizedString("The service is not started.", comment: "ServiceNotStartedException")
case .ServiceNotSupportedException:
return NSLocalizedString("The current iOS version does not support the service.", comment: "ServiceNotSupportedException")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// BackgroundServiceAction.swift
// flutter_foreground_task
//
// Created by WOO JIN HWANG on 2021/08/11.
//

import Foundation

enum BackgroundServiceAction: String {
case API_START
case API_RESTART
case API_UPDATE
case API_STOP

case APP_TERMINATE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// BackgroundServiceStatus.swift
// flutter_foreground_task
//
// Created by Woo Jin Hwang on 9/13/24.
//

import Foundation

struct BackgroundServiceStatus {
let action: BackgroundServiceAction

static func getData() -> BackgroundServiceStatus {
let prefs = UserDefaults.standard
let actionValue = prefs.string(forKey: BACKGROUND_SERVICE_ACTION) ?? BackgroundServiceAction.API_STOP.rawValue
let action = BackgroundServiceAction(rawValue: actionValue)!

return BackgroundServiceStatus(action: action)
}

static func setData(action: BackgroundServiceAction) {
let prefs = UserDefaults.standard
prefs.set(action.rawValue, forKey: BACKGROUND_SERVICE_ACTION)
}
}
Loading