From 590ce5c92823bd45891d16108d3fcb4e42370b88 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 23 Mar 2020 12:12:49 -0700 Subject: [PATCH] =?UTF-8?q?Deletes=20cached=20apps=20after=20they=E2=80=99?= =?UTF-8?q?ve=20been=20uninstalled=20from=20device?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AltStore/Managing Apps/AppManager.swift | 110 +++++++++++++++--------- AltStore/Patreon/PatreonAPI.swift | 2 +- AltStore/Protocols/Fetchable.swift | 22 +++-- 3 files changed, 85 insertions(+), 49 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 1268cffb..8f2c3dda 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -48,55 +48,79 @@ extension AppManager { func update() { - #if targetEnvironment(simulator) - // Apps aren't ever actually installed to simulator, so just do nothing rather than delete them from database. - return - #else - - let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext() - - let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest - fetchRequest.returnsObjectsAsFaults = false - - do - { - let installedApps = try context.fetch(fetchRequest) - - if UserDefaults.standard.legacySideloadedApps == nil + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + #if targetEnvironment(simulator) + // Apps aren't ever actually installed to simulator, so just do nothing rather than delete them from database. + #else + do { - // First time updating apps since updating AltStore to use custom UTIs, - // so cache all existing apps temporarily to prevent us from accidentally - // deleting them due to their custom UTI not existing (yet). - let apps = installedApps.map { $0.bundleIdentifier } - UserDefaults.standard.legacySideloadedApps = apps - } - - let legacySideloadedApps = Set(UserDefaults.standard.legacySideloadedApps ?? []) - - for app in installedApps - { - guard app.bundleIdentifier != StoreApp.altstoreAppID else { - self.scheduleExpirationWarningLocalNotification(for: app) - continue - } + let installedApps = InstalledApp.all(in: context) - let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary? - if uti == nil && !legacySideloadedApps.contains(app.bundleIdentifier) + if UserDefaults.standard.legacySideloadedApps == nil { - // This UTI is not declared by any apps, which means this app has been deleted by the user. - // This app is also not a legacy sideloaded app, so we can assume it's fine to delete it. - context.delete(app) + // First time updating apps since updating AltStore to use custom UTIs, + // so cache all existing apps temporarily to prevent us from accidentally + // deleting them due to their custom UTI not existing (yet). + let apps = installedApps.map { $0.bundleIdentifier } + UserDefaults.standard.legacySideloadedApps = apps + } + + let legacySideloadedApps = Set(UserDefaults.standard.legacySideloadedApps ?? []) + + for app in installedApps + { + guard app.bundleIdentifier != StoreApp.altstoreAppID else { + self.scheduleExpirationWarningLocalNotification(for: app) + continue + } + + let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary? + if uti == nil && !legacySideloadedApps.contains(app.bundleIdentifier) + { + // This UTI is not declared by any apps, which means this app has been deleted by the user. + // This app is also not a legacy sideloaded app, so we can assume it's fine to delete it. + context.delete(app) + } + } + + try context.save() + } + catch + { + print("Error while fetching installed apps.", error) + } + #endif + + do + { + let installedAppBundleIDs = InstalledApp.all(in: context).map { $0.bundleIdentifier } + + let cachedAppDirectories = try FileManager.default.contentsOfDirectory(at: InstalledApp.appsDirectoryURL, + includingPropertiesForKeys: [.isDirectoryKey, .nameKey], + options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles]) + for appDirectory in cachedAppDirectories + { + do + { + let resourceValues = try appDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey]) + guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue } + + if isDirectory && !installedAppBundleIDs.contains(bundleID) && !self.installationProgress.keys.contains(bundleID) + { + try FileManager.default.removeItem(at: appDirectory) + } + } + catch + { + print("Failed to remove cached app directory.", error) + } } } - - try context.save() + catch + { + print("Failed to remove cached apps.", error) + } } - catch - { - print("Error while fetching installed apps") - } - - #endif } @discardableResult diff --git a/AltStore/Patreon/PatreonAPI.swift b/AltStore/Patreon/PatreonAPI.swift index fec81443..a37d592e 100644 --- a/AltStore/Patreon/PatreonAPI.swift +++ b/AltStore/Patreon/PatreonAPI.swift @@ -235,7 +235,7 @@ extension PatreonAPI func signOut(completion: @escaping (Result) -> Void) { DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in - let accounts = PatreonAccount.all(in: context) + let accounts = PatreonAccount.all(in: context, requestProperties: [\FetchRequest.returnsObjectsAsFaults: true]) accounts.forEach(context.delete(_:)) do diff --git a/AltStore/Protocols/Fetchable.swift b/AltStore/Protocols/Fetchable.swift index a202474f..ce0d9308 100644 --- a/AltStore/Protocols/Fetchable.swift +++ b/AltStore/Protocols/Fetchable.swift @@ -8,21 +8,25 @@ import CoreData +typealias FetchRequest = NSFetchRequest + protocol Fetchable: NSManagedObject { } extension Fetchable { - static func first(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext) -> Self? + static func first(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, + requestProperties: [PartialKeyPath: Any?] = [:]) -> Self? { - let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, returnFirstResult: true) + let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, requestProperties: requestProperties, returnFirstResult: true) return managedObjects.first } - static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext) -> [Self] + static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, + requestProperties: [PartialKeyPath: Any?] = [:]) -> [Self] { - let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, returnFirstResult: false) + let managedObjects = Self.all(satisfying: predicate, sortedBy: sortDescriptors, in: context, requestProperties: requestProperties, returnFirstResult: false) return managedObjects } @@ -40,7 +44,7 @@ extension Fetchable } } - private static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, returnFirstResult: Bool) -> [Self] + private static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, requestProperties: [PartialKeyPath: Any?], returnFirstResult: Bool) -> [Self] { let registeredObjects = context.registeredObjects.lazy.compactMap({ $0 as? Self }).filter({ predicate?.evaluate(with: $0) != false }) @@ -52,6 +56,14 @@ extension Fetchable let fetchRequest = self.fetchRequest() as! NSFetchRequest fetchRequest.predicate = predicate fetchRequest.sortDescriptors = sortDescriptors + fetchRequest.returnsObjectsAsFaults = false + + for (keyPath, value) in requestProperties + { + // Still no easy way to cast PartialKeyPath back to usable WritableKeyPath :( + guard let objcKeyString = keyPath._kvcKeyPathString else { continue } + fetchRequest.setValue(value, forKey: objcKeyString) + } let fetchedObjects = self.fetch(fetchRequest, in: context)