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]