diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 318ef4da8..e553de111 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -170,6 +170,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg observeAppLockChanges() registerBackgroundAppRefresh() + registerSearchBackfill() appSettings.analyticsConsentStatePublisher .dropFirst() // Sentry is configured during init; only reconfigure when consent state actually changes @@ -1189,6 +1190,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg scheduleDelayedPauseServices() scheduleBackgroundAppRefresh() + scheduleSearchBackfill() } @objc @@ -1325,3 +1327,63 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } } } + +// MARK: Search backfill + +private extension AppCoordinator { + func registerSearchBackfill() { + let result = BGTaskScheduler.shared.register(forTaskWithIdentifier: appSettings.searchBackfillTaskIdentifier, using: .main) { [weak self] task in + guard let task = task as? BGProcessingTask else { + MXLog.error("Invalid search backfill configuration") + return + } + + Task { + await self?.handleSearchBackfill(task) + } + } + + MXLog.info("Register search backfill with result: \(result)") + } + + func scheduleSearchBackfill() { + guard let userSession, userSession.userSettings.globalSearchEnabled, #available(iOS 26.0, *) else { + return + } + + let request = BGProcessingTaskRequest(identifier: appSettings.searchBackfillTaskIdentifier) + request.requiresExternalPower = true + request.requiresNetworkConnectivity = true + + do { + try BGTaskScheduler.shared.submit(request) + MXLog.info("Successfully scheduled search backfill task") + } catch { + MXLog.error("Failed scheduling search backfill with error: \(error)") + } + } + + func handleSearchBackfill(_ task: BGProcessingTask) async { + MXLog.info("Started search backfill task") + + guard let clientProxy = userSession?.clientProxy else { + task.setTaskCompleted(success: false) + return + } + + task.expirationHandler = { @Sendable [weak self] in + MXLog.info("Search backfill task is about to expire.") + Task { @MainActor in self?.userSession?.clientProxy.stopSearchBackfill() } + } + + clientProxy.startSearchBackfill(strategy: .background) + + // TaskHandle can't be awaited, so poll until the sweep finishes or is stopped on expiry. + while clientProxy.isSearchBackfillRunning { + try? await Task.sleep(for: .seconds(1)) + } + + MXLog.info("Marking search backfill task as complete.") + task.setTaskCompleted(success: true) + } +} diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index bc5ba52bf..2de8f94cd 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -151,6 +151,8 @@ final nonisolated class AppSettings: @unchecked Sendable { /// The task identifier used for background app refresh. Also used in main target's the Info.plist let backgroundAppRefreshTaskIdentifier = "io.element.elementx.background.refresh" + /// The task identifier used for the search index backfill. Also used in main target's the Info.plist + let searchBackfillTaskIdentifier = "io.element.elementx.background.search-backfill" /// A URL where users can go read more about the app. private(set) var websiteURL: URL = "https://element.io" diff --git a/ElementX/SupportingFiles/Info.plist b/ElementX/SupportingFiles/Info.plist index 0c956f384..531148c16 100644 --- a/ElementX/SupportingFiles/Info.plist +++ b/ElementX/SupportingFiles/Info.plist @@ -5,6 +5,7 @@ BGTaskSchedulerPermittedIdentifiers io.element.elementx.background.refresh + io.element.elementx.background.search-backfill CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 9c17e77b1..8fccc124d 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -100,7 +100,8 @@ targets: location ] BGTaskSchedulerPermittedIdentifiers: [ - io.element.elementx.background.refresh + io.element.elementx.background.refresh, + io.element.elementx.background.search-backfill ] UTExportedTypeDeclarations: - UTTypeConformsTo: [public.text]