diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 37c640e8..d6589e6d 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -33,34 +33,34 @@ extension AppDelegate class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - + @available(iOS 14, *) private var intentHandler: IntentHandler { get { _intentHandler as! IntentHandler } set { _intentHandler = newValue } } - + @available(iOS 14, *) private var viewAppIntentHandler: ViewAppIntentHandler { get { _viewAppIntentHandler as! ViewAppIntentHandler } set { _viewAppIntentHandler = newValue } } - + private lazy var _intentHandler: Any = { guard #available(iOS 14, *) else { fatalError() } return IntentHandler() }() - + private lazy var _viewAppIntentHandler: Any = { guard #available(iOS 14, *) else { fatalError() } return ViewAppIntentHandler() }() - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Register default settings before doing anything else. UserDefaults.registerDefaults() - + DatabaseManager.shared.start { (error) in if let error = error { @@ -71,50 +71,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("Started DatabaseManager.") } } - + AnalyticsManager.shared.start() - + self.setTintColor() - - SecureValueTransformer.register() - + + SecureValueTransformer.register() + if UserDefaults.standard.firstLaunch == nil { Keychain.shared.reset() UserDefaults.standard.firstLaunch = Date() } - + UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String - + #if DEBUG || BETA UserDefaults.standard.isDebugModeEnabled = true #endif - + self.prepareForBackgroundFetch() - + return true } func applicationDidEnterBackground(_ application: UIApplication) - { + { + // Make sure to update SceneDelegate.sceneDidEnterBackground() as well. + + guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return } + + let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo) + DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in + switch result + { + case .success: break + case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error) + } + } - } + } func applicationWillEnterForeground(_ application: UIApplication) { AppManager.shared.update() start_em_proxy(bind_addr: Consts.Proxy.serverURL) } - + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool { return self.open(url) } - + func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? { guard #available(iOS 14, *) else { return nil } - + switch intent { case is RefreshAllIntent: return self.intentHandler @@ -133,7 +145,7 @@ extension AppDelegate // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { // Called when the user discards a scene session. @@ -148,36 +160,36 @@ private extension AppDelegate { self.window?.tintColor = .altPrimary } - + func open(_ url: URL) -> Bool { if url.isFileURL { guard url.pathExtension.lowercased() == "ipa" else { return false } - + DispatchQueue.main.async { NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url]) } - + return true } else { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } guard let host = components.host?.lowercased() else { return false } - + switch host { case "patreon": DispatchQueue.main.async { NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) } - + return true - + case "appbackupresponse": let result: Result - + switch url.path.lowercased() { case "/success": result = .success(()) @@ -188,37 +200,37 @@ private extension AppDelegate let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString), let errorDescription = queryItems["errorDescription"] else { return false } - + let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription]) result = .failure(error) - + default: return false } - + NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result]) - + return true - + case "install": let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:] guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false } - + DispatchQueue.main.async { NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL]) } - + return true - + case "source": let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:] guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false } - + DispatchQueue.main.async { NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL]) } - + return true - + default: return false } } @@ -231,47 +243,47 @@ extension AppDelegate { // "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery). UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60) - + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in } - + #if DEBUG UIApplication.shared.registerForRemoteNotifications() #endif } - + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let tokenParts = deviceToken.map { data -> String in return String(format: "%02.2hhx", data) } - + let token = tokenParts.joined() print("Push Token:", token) } - + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { self.application(application, performFetchWithCompletionHandler: completionHandler) } - + func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification { let threeHours: TimeInterval = 3 * 60 * 60 let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false) - + let content = UNMutableNotificationContent() content.title = NSLocalizedString("App Refresh Tip", comment: "") content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "") - + let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) - + UserDefaults.standard.presentedLaunchReminderNotification = true } - + BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in if let error = taskResult.error { @@ -280,7 +292,7 @@ extension AppDelegate taskCompletionHandler() return } - + if !DatabaseManager.shared.isStarted { DatabaseManager.shared.start() { (error) in @@ -309,7 +321,7 @@ extension AppDelegate } } } - + func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void, refreshAppsCompletionHandler: @escaping (Result<[String: Result], Error>) -> Void) { @@ -319,15 +331,15 @@ extension AppDelegate case .failure: backgroundFetchCompletionHandler(.failed) case .success: backgroundFetchCompletionHandler(.newData) } - + if !UserDefaults.standard.isBackgroundRefreshEnabled { refreshAppsCompletionHandler(.success([:])) } } - + guard UserDefaults.standard.isBackgroundRefreshEnabled else { return } - + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context) AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler) @@ -343,49 +355,49 @@ private extension AppDelegate do { let (sources, context) = try result.get() - + let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest previousUpdatesFetchRequest.includesPendingChanges = false previousUpdatesFetchRequest.resultType = .dictionaryResultType previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)] - + let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest previousNewsItemsFetchRequest.includesPendingChanges = false previousNewsItemsFetchRequest.resultType = .dictionaryResultType previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)] - + let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]] let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]] - + try context.save() - + let updatesFetchRequest = InstalledApp.updatesFetchRequest() let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest - + let updates = try context.fetch(updatesFetchRequest) let newsItems = try context.fetch(newsItemsFetchRequest) - + for update in updates { guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue } guard let storeApp = update.storeApp else { continue } - + let content = UNMutableNotificationContent() content.title = NSLocalizedString("New Update Available", comment: "") content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version) content.sound = .default - + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } - + for newsItem in newsItems { guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue } guard !newsItem.isSilent else { continue } - + let content = UNMutableNotificationContent() - + if let app = newsItem.storeApp { content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name) @@ -394,10 +406,10 @@ private extension AppDelegate { content.title = NSLocalizedString("SideStore News", comment: "") } - + content.body = newsItem.title content.sound = .default - + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } @@ -405,7 +417,7 @@ private extension AppDelegate DispatchQueue.main.async { UIApplication.shared.applicationIconBadgeNumber = updates.count } - + completionHandler(.success(sources)) } catch diff --git a/AltStore/SceneDelegate.swift b/AltStore/SceneDelegate.swift index 84be696e..938b895d 100644 --- a/AltStore/SceneDelegate.swift +++ b/AltStore/SceneDelegate.swift @@ -53,6 +53,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate guard UIApplication.shared.applicationState == .background else { return } + // Make sure to update AppDelegate.applicationDidEnterBackground() as well. + + guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return } + + let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo) + DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in + switch result + { + case .success: break + case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error) + } + } }