Automatically purges LoggedErrors older than one month

Occurs whenever app enters background.
This commit is contained in:
Riley Testut
2022-09-09 17:47:49 -05:00
committed by Joseph Mattello
parent d2b419c42e
commit f472b227bb
2 changed files with 93 additions and 69 deletions

View File

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

View File

@@ -53,6 +53,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate
guard UIApplication.shared.applicationState == .background else { return } 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)
}
}
} }