- iOS: Add BackgroundSyncService, SyncScheduler, SyncWorker, BookmarkViewModel, FeedViewModel - iOS: Add BackgroundSyncService, SyncScheduler, SyncWorker services - Linux: Add settings-store.vala, State.vala signals, view widgets (FeedList, FeedDetail, AddFeed, Search, Settings, Bookmark) - Linux: Add bookmark-store.vala, bookmark vala model, search-service.vala - Android: Add NotificationService, NotificationManager, NotificationPreferencesStore - Android: Add BookmarkDao, BookmarkRepository, SettingsStore - Add unit tests for iOS, Android, Linux - Add integration tests - Add performance benchmarks - Update tasks and documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
235 lines
7.4 KiB
Swift
235 lines
7.4 KiB
Swift
import Foundation
|
|
import BackgroundTasks
|
|
|
|
/// Main background sync service coordinator
|
|
/// Orchestrates background feed synchronization using BGTaskScheduler
|
|
final class BackgroundSyncService {
|
|
// MARK: - Singleton
|
|
|
|
static let shared = BackgroundSyncService()
|
|
|
|
// MARK: - Properties
|
|
|
|
/// Identifier for the background refresh task
|
|
static let backgroundRefreshIdentifier = "com.rssuper.backgroundRefresh"
|
|
|
|
/// Identifier for the periodic sync task
|
|
static let periodicSyncIdentifier = "com.rssuper.periodicSync"
|
|
|
|
private let syncScheduler: SyncScheduler
|
|
private let syncWorker: SyncWorker
|
|
|
|
/// Current sync state
|
|
private var isSyncing: Bool = false
|
|
|
|
/// Last successful sync date
|
|
var lastSyncDate: Date?
|
|
|
|
/// Pending feeds count
|
|
var pendingFeedsCount: Int = 0
|
|
|
|
// MARK: - Initialization
|
|
|
|
private init() {
|
|
self.syncScheduler = SyncScheduler()
|
|
self.syncWorker = SyncWorker()
|
|
}
|
|
|
|
// MARK: - Public API
|
|
|
|
/// Register background tasks with the system
|
|
func registerBackgroundTasks() {
|
|
// Register app refresh task
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.backgroundRefreshIdentifier,
|
|
with: nil) { task in
|
|
self.handleBackgroundTask(task)
|
|
}
|
|
|
|
// Register periodic sync task (if available on device)
|
|
BGTaskScheduler.shared.register(forTaskIdentifier: Self.periodicSyncIdentifier,
|
|
with: nil) { task in
|
|
self.handlePeriodicSync(task)
|
|
}
|
|
|
|
print("✓ Background tasks registered")
|
|
}
|
|
|
|
/// Schedule a background refresh task
|
|
func scheduleBackgroundRefresh() -> Bool {
|
|
guard !isSyncing else {
|
|
print("⚠️ Sync already in progress")
|
|
return false
|
|
}
|
|
|
|
let taskRequest = BGAppRefreshTaskRequest(identifier: Self.backgroundRefreshIdentifier)
|
|
|
|
// Schedule between 15 minutes and 4 hours from now
|
|
taskRequest.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
|
|
|
|
// Set retry interval (minimum 15 minutes)
|
|
taskRequest.requiredReasons = [.networkAvailable]
|
|
|
|
do {
|
|
try BGTaskScheduler.shared.submit(taskRequest)
|
|
print("✓ Background refresh scheduled")
|
|
return true
|
|
} catch {
|
|
print("❌ Failed to schedule background refresh: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Schedule periodic sync (iOS 13+) with custom interval
|
|
func schedulePeriodicSync(interval: TimeInterval = 6 * 3600) -> Bool {
|
|
guard !isSyncing else {
|
|
print("⚠️ Sync already in progress")
|
|
return false
|
|
}
|
|
|
|
let taskRequest = BGProcessingTaskRequest(identifier: Self.periodicSyncIdentifier)
|
|
taskRequest.requiresNetworkConnectivity = true
|
|
taskRequest.requiresExternalPower = false // Allow on battery
|
|
taskRequest.minimumInterval = interval
|
|
|
|
do {
|
|
try BGTaskScheduler.shared.submit(taskRequest)
|
|
print("✓ Periodic sync scheduled (interval: \(interval/3600)h)")
|
|
return true
|
|
} catch {
|
|
print("❌ Failed to schedule periodic sync: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Cancel all pending background tasks
|
|
func cancelAllPendingTasks() {
|
|
BGTaskScheduler.shared.cancelAllTaskRequests()
|
|
print("✓ All pending background tasks cancelled")
|
|
}
|
|
|
|
/// Get pending task requests
|
|
func getPendingTaskRequests() async -> [BGTaskScheduler.PendingTaskRequest] {
|
|
do {
|
|
let requests = try await BGTaskScheduler.shared.pendingTaskRequests()
|
|
return requests
|
|
} catch {
|
|
print("❌ Failed to get pending tasks: \(error)")
|
|
return []
|
|
}
|
|
}
|
|
|
|
/// Force immediate sync (for testing or user-initiated)
|
|
func forceSync() async {
|
|
guard !isSyncing else {
|
|
print("⚠️ Sync already in progress")
|
|
return
|
|
}
|
|
|
|
isSyncing = true
|
|
|
|
do {
|
|
let result = try await syncWorker.performSync()
|
|
lastSyncDate = Date()
|
|
|
|
print("✓ Force sync completed: \(result.feedsSynced) feeds, \(result.articlesFetched) articles")
|
|
|
|
// Schedule next background refresh
|
|
scheduleBackgroundRefresh()
|
|
|
|
} catch {
|
|
print("❌ Force sync failed: \(error)")
|
|
}
|
|
|
|
isSyncing = false
|
|
}
|
|
|
|
/// Check if background tasks are enabled
|
|
func areBackgroundTasksEnabled() -> Bool {
|
|
// Check if Background Modes capability is enabled
|
|
// This is a basic check; more sophisticated checks can be added
|
|
return true
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
/// Handle background app refresh task
|
|
private func handleBackgroundTask(_ task: BGTask) {
|
|
guard let appRefreshTask = task as? BGAppRefreshTask else {
|
|
print("❌ Unexpected task type")
|
|
task.setTaskCompleted(success: false)
|
|
return
|
|
}
|
|
|
|
print("🔄 Background refresh task started (expiration: \(appRefreshTask.expirationDate))")
|
|
|
|
isSyncing = true
|
|
|
|
Task(priority: .userInitiated) {
|
|
do {
|
|
let result = try await syncWorker.performSync()
|
|
|
|
lastSyncDate = Date()
|
|
|
|
print("✓ Background refresh completed: \(result.feedsSynced) feeds, \(result.articlesFetched) articles")
|
|
|
|
// Re-schedule the task
|
|
scheduleBackgroundRefresh()
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
} catch {
|
|
print("❌ Background refresh failed: \(error)")
|
|
task.setTaskCompleted(success: false, retryAttempted: true)
|
|
}
|
|
|
|
isSyncing = false
|
|
}
|
|
}
|
|
|
|
/// Handle periodic sync task
|
|
private func handlePeriodicSync(_ task: BGTask) {
|
|
guard let processingTask = task as? BGProcessingTask else {
|
|
print("❌ Unexpected task type")
|
|
task.setTaskCompleted(success: false)
|
|
return
|
|
}
|
|
|
|
print("🔄 Periodic sync task started (expiration: \(processingTask.expirationDate))")
|
|
|
|
isSyncing = true
|
|
|
|
Task(priority: .utility) {
|
|
do {
|
|
let result = try await syncWorker.performSync()
|
|
|
|
lastSyncDate = Date()
|
|
|
|
print("✓ Periodic sync completed: \(result.feedsSynced) feeds, \(result.articlesFetched) articles")
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
} catch {
|
|
print("❌ Periodic sync failed: \(error)")
|
|
task.setTaskCompleted(success: false, retryAttempted: true)
|
|
}
|
|
|
|
isSyncing = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SyncResult
|
|
|
|
/// Result of a sync operation
|
|
struct SyncResult {
|
|
let feedsSynced: Int
|
|
let articlesFetched: Int
|
|
let errors: [Error]
|
|
|
|
init(feedsSynced: Int = 0, articlesFetched: Int = 0, errors: [Error] = []) {
|
|
self.feedsSynced = feedsSynced
|
|
self.articlesFetched = articlesFetched
|
|
self.errors = errors
|
|
}
|
|
}
|