From 60ba067982e247d11f523a352697ddd93087f692 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 1 Jul 2019 12:09:46 -0700 Subject: [PATCH] [AltStore] Skip running apps when background refreshing --- AltStore/AppDelegate.swift | 185 +++++++++++++++++++++++-------------- 1 file changed, 116 insertions(+), 69 deletions(-) diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index f4d660a9..d3a2c95c 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -12,10 +12,36 @@ import UserNotifications import AltSign import Roxas +private extension CFNotificationName +{ + static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString) + static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString) + + static func requestAppState(for appID: String) -> CFNotificationName + { + let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID + return CFNotificationName(name as CFString) + } + + static func appIsRunning(for appID: String) -> CFNotificationName + { + let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID + return CFNotificationName(name as CFString) + } +} + +private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = +{ (center, observer, name, object, userInfo) in + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let name = name else { return } + appDelegate.receivedApplicationState(notification: name) +} + @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? + + private var runningApplications: Set? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -75,7 +101,23 @@ extension AppDelegate let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: DatabaseManager.shared.viewContext) guard !installedApps.isEmpty else { return completionHandler(.noData) } - print("Apps to refresh:", installedApps.map { $0.app.identifier }) + self.runningApplications = [] + + let identifiers = installedApps.compactMap { $0.app?.identifier } + print("Apps to refresh:", identifiers) + + DispatchQueue.global().async { + let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() + + for identifier in identifiers + { + let appIsRunningNotification = CFNotificationName.appIsRunning(for: identifier) + CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately) + + let requestAppStateNotification = CFNotificationName.requestAppState(for: identifier) + CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true) + } + } ServerManager.shared.startDiscovering() @@ -91,81 +133,78 @@ extension AppDelegate } } + // Sleep for three seconds to: + // a) give us time to discover AltServers + // b) give other processes a chance to respond to requestAppState notification + // NOTE: We need to sleep, *not* DispatchQueue.asyncAfter, or else background audio might fail to start. + Thread.sleep(forTimeInterval: 3) + BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in + + func finish(_ result: Result<[String: Result], Error>) + { + ServerManager.shared.stopDiscovering() + + let content = UNMutableNotificationContent() + var shouldPresentAlert = true + + do + { + let results = try result.get() + shouldPresentAlert = !results.isEmpty + + for (_, result) in results + { + guard case let .failure(error) = result else { continue } + throw error + } + + content.title = NSLocalizedString("Refreshed all apps!", comment: "") + } + catch + { + print("Failed to refresh apps in background.", error) + + content.title = NSLocalizedString("Failed to Refresh Apps", comment: "") + content.body = error.localizedDescription + + shouldPresentAlert = true + } + + if shouldPresentAlert + { + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false) + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + UNUserNotificationCenter.current().add(request) { (error) in + if let error = error { + print(error) + } + } + } + + switch result + { + case .failure(ConnectionError.serverNotFound): completionHandler(.newData) + case .failure: completionHandler(.failed) + case .success: completionHandler(.newData) + } + + taskCompletionHandler() + } + if let error = taskResult.error { print("Error starting extended background task. Aborting.", error) - - let content = UNMutableNotificationContent() - content.title = NSLocalizedString("Failed to refresh apps.", comment: "") - content.body = taskResult.error?.localizedDescription ?? "" - - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false) - - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request) { (error) in - if let error = error { - print(error) - } - } + finish(.failure(error)) + return } - // Wait a few seconds so we have a chance to discover nearby AltServers. - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + DispatchQueue.main.async { + let filteredApps = installedApps.filter { !(self.runningApplications?.contains($0.app.identifier) ?? false) } + print("Filtered Apps to Refresh:", filteredApps.map { $0.app.identifier }) - func finish(_ result: Result<[String: Result], Error>) - { - ServerManager.shared.stopDiscovering() - - let content = UNMutableNotificationContent() - var shouldPresentAlert = true - - do - { - let results = try result.get() - shouldPresentAlert = !results.isEmpty - - for (_, result) in results - { - guard case let .failure(error) = result else { continue } - throw error - } - - content.title = NSLocalizedString("Refreshed all apps!", comment: "") - } - catch - { - print("Failed to refresh apps in background.", error) - - content.title = NSLocalizedString("Failed to Refresh Apps", comment: "") - content.body = error.localizedDescription - - shouldPresentAlert = true - } - - if shouldPresentAlert - { - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false) - - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request) { (error) in - if let error = error { - print(error) - } - } - } - - switch result - { - case .failure(ConnectionError.serverNotFound): completionHandler(.newData) - case .failure: completionHandler(.failed) - case .success: completionHandler(.newData) - } - - taskCompletionHandler() - } - - let group = AppManager.shared.refresh(installedApps, presentingViewController: nil) + let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil) group.beginInstallationHandler = { (installedApp) in guard installedApp.app.identifier == App.altstoreAppID else { return } @@ -191,4 +230,12 @@ extension AppDelegate } } } + + func receivedApplicationState(notification: CFNotificationName) + { + let baseName = String(CFNotificationName.appIsRunning.rawValue) + + let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "") + self.runningApplications?.insert(appID) + } }