From 49084f2cd155cf79e2792357f0c5ac56ae0ddc12 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Wed, 13 Nov 2024 08:49:16 +0100 Subject: [PATCH] fix: background runner strategy (#258) * fix: background runner strategy --- .../org/ooni/probe/SetupDependencies.kt | 4 +- .../ooni/probe/background/BackgroundRunner.kt | 5 ++ .../probe/background/OperationsManager.kt | 30 +++----- .../org/ooni/probe/background/RunOperation.kt | 26 ------- iosApp/iosApp.xcodeproj/project.pbxproj | 11 +++ .../background/IosBackgroundRunner.swift | 70 +++++++++++++++++++ iosApp/iosApp/iOSApp.swift | 3 +- 7 files changed, 100 insertions(+), 49 deletions(-) create mode 100644 composeApp/src/iosMain/kotlin/org/ooni/probe/background/BackgroundRunner.kt delete mode 100644 composeApp/src/iosMain/kotlin/org/ooni/probe/background/RunOperation.kt create mode 100644 iosApp/iosApp/background/IosBackgroundRunner.swift diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt index 4a8dc8b9..38398c6a 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/SetupDependencies.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.ooni.engine.NetworkTypeFinder import org.ooni.engine.OonimkallBridge +import org.ooni.probe.background.BackgroundRunner import org.ooni.probe.background.OperationsManager import org.ooni.probe.config.BatteryOptimization import org.ooni.probe.config.FlavorConfig @@ -60,6 +61,7 @@ import platform.darwin.NSObjectMeta class SetupDependencies( bridge: OonimkallBridge, networkTypeFinder: NetworkTypeFinder, + val backgroundRunner: BackgroundRunner, ) { /** * See link for `baseFileDir` https://github.com/ooni/probe-ios/blob/2145bbd5eda6e696be216e3bce97e8d5fb33dcea/ooniprobe/Engine/Engine.m#L54 @@ -87,7 +89,7 @@ class SetupDependencies( flavorConfig = FlavorConfig(), ) - private val operationsManager = OperationsManager(dependencies) + private val operationsManager = OperationsManager(dependencies, backgroundRunner) private fun localeDirection(): LayoutDirection { return if (NSLocale.characterDirectionForLanguage(Locale.current.language) == NSLocaleLanguageDirectionRightToLeft) { diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/background/BackgroundRunner.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/background/BackgroundRunner.kt new file mode 100644 index 00000000..b1c9e257 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/background/BackgroundRunner.kt @@ -0,0 +1,5 @@ +package org.ooni.probe.background + +fun interface BackgroundRunner { + operator fun invoke(background: (() -> Unit)) +} diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/background/OperationsManager.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/background/OperationsManager.kt index 4c9127d3..154b7440 100644 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/background/OperationsManager.kt +++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/background/OperationsManager.kt @@ -1,6 +1,8 @@ package org.ooni.probe.background import co.touchlab.kermit.Logger +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.runBlocking import org.ooni.probe.data.models.InstalledTestDescriptorModel import org.ooni.probe.data.models.RunSpecification import org.ooni.probe.di.Dependencies @@ -8,32 +10,18 @@ import platform.BackgroundTasks.BGProcessingTask import platform.Foundation.NSOperationQueue import platform.UIKit.UIApplication -class OperationsManager(private val dependencies: Dependencies) { +class OperationsManager(private val dependencies: Dependencies, private val backgroundRunner: BackgroundRunner) { fun startSingleRun(spec: RunSpecification) { - val operationQueue = NSOperationQueue() - val operation = RunOperation( - spec = spec, - runBackgroundTask = dependencies.runBackgroundTask::invoke, - ) - val identifier = UIApplication.sharedApplication.beginBackgroundTaskWithExpirationHandler { - operation.cancel() - } - operation.completionBlock = { - UIApplication.sharedApplication.endBackgroundTask(identifier) - } - operationQueue.addOperation(operation) + backgroundRunner(background = { + runBlocking { dependencies.runBackgroundTask(spec).collect() } + }) } fun handleAutorunTask(task: BGProcessingTask) { Logger.d { "Handling autorun task" } - val operationQueue = NSOperationQueue() - val operation = RunOperation( - spec = null, - runBackgroundTask = dependencies.runBackgroundTask::invoke, - ) - task.expirationHandler = { operation.cancel() } - operation.completionBlock = { task.setTaskCompletedWithSuccess(!operation.isCancelled()) } - operationQueue.addOperation(operation) + backgroundRunner(background = { + runBlocking { dependencies.runBackgroundTask(null).collect() } + }) } fun handleUpdateDescriptorTask(task: BGProcessingTask) { diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/background/RunOperation.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/background/RunOperation.kt deleted file mode 100644 index a9c62fd7..00000000 --- a/composeApp/src/iosMain/kotlin/org/ooni/probe/background/RunOperation.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.ooni.probe.background - -import co.touchlab.kermit.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import org.ooni.probe.data.models.RunSpecification -import platform.Foundation.NSOperation - -class RunOperation( - private val runBackgroundTask: (RunSpecification?) -> Flow, - private val spec: RunSpecification? = null, -) : NSOperation() { - override fun main() { - Logger.d { "Running operation" } - val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - coroutineScope.launch { - runBackgroundTask(spec).collect() - }.invokeOnCompletion { - Logger.d { "Operation completed" } - } - } -} diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 7e9abdd3..b04874d6 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -81,6 +81,10 @@ DD59ED7E2702C84FF0455021 /* Pods_OONIProbe.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OONIProbe.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 79167AE52CE2A92800C070C5 /* background */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = background; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 795E37722CD5053900086360 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -167,6 +171,7 @@ 7555FF7D242A565900829871 /* iosApp */ = { isa = PBXGroup; children = ( + 79167AE52CE2A92800C070C5 /* background */, 058557BA273AAA24004C7B11 /* Assets.xcassets */, 7555FF82242A565900829871 /* ContentView.swift */, 7555FF8C242A565B00829871 /* Info.plist */, @@ -260,6 +265,9 @@ dependencies = ( 795E377E2CD5053900086360 /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 79167AE52CE2A92800C070C5 /* background */, + ); name = OONIProbe; productName = iosApp; productReference = 7555FF7B242A565900829871 /* OONI Probe.app */; @@ -299,6 +307,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 79167AE52CE2A92800C070C5 /* background */, + ); name = NewsMediaScan; productName = iosApp; productReference = 79FBD01A2C5A70AF004E041C /* News Media Scan.app */; diff --git a/iosApp/iosApp/background/IosBackgroundRunner.swift b/iosApp/iosApp/background/IosBackgroundRunner.swift new file mode 100644 index 00000000..fd2a4d6a --- /dev/null +++ b/iosApp/iosApp/background/IosBackgroundRunner.swift @@ -0,0 +1,70 @@ +import composeApp +import AVFAudio +import UIKit + +class IosBackgroundRunner : BackgroundRunner { + private var backgroundTask: UIBackgroundTaskIdentifier = .invalid + private var audioPlayer: AVAudioPlayer? + + /// Invoke the given task in the background. + func invoke(background: @escaping () -> Void) { + let serialQueue = DispatchQueue(label: Bundle.main.bundleIdentifier ?? OrganizationConfig().autorunTaskId, qos: .userInitiated, attributes: [.concurrent]) + serialQueue.async(execute: DispatchWorkItem { + self.setupBackgroundTask() + background() + self.stopBackgroundTask() + } ) + } + + /// Set up a background task and audio player to keep the app running in the background. + /// This method should be called at the beginning of the background task. + func setupBackgroundTask(){ + // Create a minimal WAV file as NSData + let bytes: [UInt8] = [0x52, 0x49, 0x46, 0x46, 0x26, 0x0, 0x0, 0x0, 0x57, 0x41, 0x56, 0x45, + 0x66, 0x6d, 0x74, 0x20, 0x10, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, + 0x44, 0xac, 0x0, 0x0, 0x88, 0x58, 0x1, 0x0, 0x2, 0x0, 0x10, 0x0, + 0x64, 0x61, 0x74, 0x61, 0x2, 0x0, 0x0, 0x0, 0xfc, 0xff] + let data = Data(bytes) + + // Get the document directory path + guard let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + return + } + let filePath = docsDir.appendingPathComponent("background.wav") + + // Write data to file + try? data.write(to: filePath) + + // Configure the audio session for background playback + let audioSession = AVAudioSession.sharedInstance() + do { + try audioSession.setCategory(.playback, options: [.mixWithOthers]) + try audioSession.setActive(true) + + // Initialize the audio player with the sound file URL + self.audioPlayer = try AVAudioPlayer(contentsOf: filePath) + self.audioPlayer?.numberOfLoops = -1 // Loop indefinitely + self.audioPlayer?.play() + } catch { + print("Failed to set up audio session or audio player: \(error)") + } + + self.backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: { + // Clean up when background time expires + UIApplication.shared.endBackgroundTask(self.backgroundTask) + self.backgroundTask = .invalid + }) + } + + /// Stop the background task and audio player. + /// This method should be called when the background task is complete. + func stopBackgroundTask() { + audioPlayer?.stop() + audioPlayer = nil + if backgroundTask != .invalid { + UIApplication.shared.endBackgroundTask(backgroundTask) + backgroundTask = .invalid + } + } + +} diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index c21e95a7..4b0428dd 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -17,7 +17,8 @@ struct iOSApp: App { let appDependencies = SetupDependencies( bridge: IosOonimkallBridge(), - networkTypeFinder: IosNetworkTypeFinder() + networkTypeFinder: IosNetworkTypeFinder(), + backgroundRunner: IosBackgroundRunner() ) let deepLinkFlow: Kotlinx_coroutines_coreMutableSharedFlow