diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 852c9266..f6def7c5 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -64,6 +64,7 @@ A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; }; A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; }; + A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; }; A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; }; A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; }; A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */; }; @@ -645,6 +646,7 @@ A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = ""; }; A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = ""; }; A868CFE32D319988002F1201 /* SingletonGenericMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonGenericMap.swift; sourceTree = ""; }; + A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppExtensionsOperation.swift; sourceTree = ""; }; A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsTimelineProvider.swift; sourceTree = ""; }; A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = ""; }; @@ -2039,6 +2041,7 @@ D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */, D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */, D561AF812B21669400BF59C6 /* VerifyAppPledgeOperation.swift */, + A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */, BF7B44062725A4B8005288A4 /* Patch App */, ); path = Operations; @@ -2988,6 +2991,7 @@ BFD2478F2284C8F900981D42 /* Button.swift in Sources */, D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */, + A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */, diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 65fcdcc9..a6524501 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -1234,123 +1234,6 @@ private extension AppManager return group } - func removeAppExtensions(from application: ALTApplication, existingApp: InstalledApp?, extensions: Set, _ presentingViewController: UIViewController?, completion: @escaping (Result) -> Void) - { - - //App-Extensions: Ensure existing app's extensions and currently installing app's extensions must match - let existingAppEx: Set = existingApp?.appExtensions ?? Set() - let currentAppEx: Set = application.appExtensions - - let currentAppExNames = currentAppEx.map{ appEx in appEx.bundleIdentifier} - let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier} - - let excessExtensions = currentAppEx.filter{ - !(existingAppExNames.contains($0.bundleIdentifier)) - } - - let isMatching = (currentAppEx.count == existingAppEx.count) && excessExtensions.isEmpty - let diagnosticsMsg = "AppManager.removeAppExtensions: App Extensions in existingApp and currentApp are matching: \(isMatching)\n" - + "AppManager.removeAppExtensions: existingAppEx: \(existingAppExNames); currentAppEx: \(String(describing: currentAppExNames))\n" - print(diagnosticsMsg) - - // if background mode, then remove only the excess extensions - guard let presentingViewController: UIViewController = presentingViewController else { - // perform silent extensions cleanup for those that aren't already present in existing app - print("\n Performing background mode Extensions removal \n") - print("AppManager.removeAppExtensions: Excess Extensions: \(excessExtensions)") - - do { - for appExtension in excessExtensions { - print("Deleting extension \(appExtension.bundleIdentifier)") - try FileManager.default.removeItem(at: appExtension.fileURL) - } - return completion(.success(())) - } catch { - return completion(.failure(error)) - } - } - - guard !application.appExtensions.isEmpty else { return completion(.success(())) } - - let firstSentence: String - - if UserDefaults.standard.activeAppLimitIncludesExtensions - { - firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "") - } - else - { - firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "") - } - - let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "") - - let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in - completion(.failure(OperationError.cancelled)) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in - completion(.success(())) - }) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in - do - { - for appExtension in application.appExtensions - { - print("Deleting extension \(appExtension.bundleIdentifier)") - try FileManager.default.removeItem(at: appExtension.fileURL) - } - - completion(.success(())) - } - catch - { - completion(.failure(error)) - } - }) - - alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in - let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in - do - { - for appExtension in selection - { - print("Deleting extension \(appExtension.bundleIdentifier)") - - try FileManager.default.removeItem(at: appExtension.fileURL) - } - completion(.success(())) - } - catch - { - completion(.failure(error)) - } - return nil - } - - let suiview = popoverContentController.view! - suiview.translatesAutoresizingMaskIntoConstraints = false - - popoverContentController.modalPresentationStyle = .popover - - if let popoverPresentationController = popoverContentController.popoverPresentationController { - popoverPresentationController.sourceView = presentingViewController.view - popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4) - popoverPresentationController.delegate = popoverContentController - - DispatchQueue.main.async { - presentingViewController.present(popoverContentController, animated: true) - } - } - - - }) - - DispatchQueue.main.async { - presentingViewController.present(alertController, animated: true) - } - } - private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, @@ -1460,52 +1343,18 @@ private extension AppManager verifyOperation.addDependency(downloadOperation) /* Remove App Extensions */ - - let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in - do - { - if let error = context.error - { - throw error - } -/* - guard case .install = appOperation else { - operation.finish() - return - } -*/ - guard let extensions = context.app?.appExtensions else { - throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app?.appExtensions is nil") - } - - guard let currentApp = context.app else { - throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app is nil") - } - - - self?.removeAppExtensions(from: currentApp, - existingApp: app as? InstalledApp, - extensions: extensions, - context.authenticatedContext.presentingViewController - ) { result in - switch result { - case .success(): break - case .failure(let error): context.error = error - } - operation.finish() - } - - } - catch + let removeAppExtensionsOperation = RemoveAppExtensionsOperation(context: context, appInDatabase: app) + removeAppExtensionsOperation.resultHandler = { (result) in + switch result { + case .failure(let error): context.error = error - operation.finish() + case .success: break } } - removeAppExtensionsOperation.addDependency(verifyOperation) - + /* Refresh Anisette Data */ let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context) refreshAnisetteDataOperation.resultHandler = { (result) in diff --git a/AltStore/Operations/RemoveAppExtensionsOperation.swift b/AltStore/Operations/RemoveAppExtensionsOperation.swift new file mode 100644 index 00000000..bdb20c57 --- /dev/null +++ b/AltStore/Operations/RemoveAppExtensionsOperation.swift @@ -0,0 +1,182 @@ +// +// RefreshAppOperation.swift +// AltStore +// +// Created by Riley Testut on 2/27/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltStoreCore +import Roxas +import AltSign + +@objc(RemoveAppExtensionsOperation) +final class RemoveAppExtensionsOperation: ResultOperation +{ + let context: AppOperationContext + let appInDatabase: AppProtocol + + init(context: AppOperationContext, appInDatabase: AppProtocol) + { + self.context = context + self.appInDatabase = appInDatabase + super.init() + } + + override func main() + { + super.main() + + if let error = self.context.error + { + self.finish(.failure(error)) + return + } + + guard let targetAppBundle = context.app else { + return self.finish(.failure( + OperationError.invalidParameters("RemoveAppExtensionsOperation: context.app is nil") + )) + } + + self.removeAppExtensions(from: targetAppBundle, + appInDatabase: appInDatabase as? InstalledApp, + extensions: targetAppBundle.appExtensions, + context.authenticatedContext.presentingViewController) + + } + + + private static func removeExtensions(from extensions: Set) throws { + for appExtension in extensions { + print("Deleting extension \(appExtension.bundleIdentifier)") + try FileManager.default.removeItem(at: appExtension.fileURL) + } + } + + + + private func removeAppExtensions(from targetAppBundle: ALTApplication, + appInDatabase: InstalledApp?, + extensions: Set, + _ presentingViewController: UIViewController?) + { + + // target App Bundle doesn't contain extensions so don't bother + guard !targetAppBundle.appExtensions.isEmpty else { + return self.finish(.success(())) + } + + //App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match + let existingAppEx: Set = appInDatabase?.appExtensions ?? Set() + let targetAppEx: Set = targetAppBundle.appExtensions + + let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier} + let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier} + + let excessExtensionsInTargetApp = targetAppEx.filter{ + !(existingAppExNames.contains($0.bundleIdentifier)) + } + + let necessaryExtensionsInExistingApp = existingAppEx.filter{ + targetAppExNames.contains($0.bundleIdentifier) + } + + // always cleanup existing app (app-in-db) based on incoming app that is targeted for install + appInDatabase?.appExtensions = necessaryExtensionsInExistingApp + + let isMatching = (targetAppEx.count == existingAppEx.count) && excessExtensionsInTargetApp.isEmpty + let diagnosticsMsg = "RemoveAppExtensionsOperation: App Extensions in existingApp and targetAppBundle are matching: \(isMatching)\n" + + "RemoveAppExtensionsOperation: existingAppEx: \(existingAppExNames); targetAppBundleEx: \(String(describing: targetAppExNames))\n" + print(diagnosticsMsg) + + // if background mode, then remove only the excess extensions + guard let presentingViewController: UIViewController = presentingViewController else { + // perform silent extensions cleanup for those that aren't already present in existing app + print("\n Performing background mode Extensions removal \n") + print("RemoveAppExtensionsOperation: Excess Extensions In TargetAppBundle: \(excessExtensionsInTargetApp)") + print("RemoveAppExtensionsOperation: Necessary Extensions In ExistingAppInDatabase: \(necessaryExtensionsInExistingApp)") + + do { + try Self.removeExtensions(from: excessExtensionsInTargetApp) + return self.finish(.success(())) + } catch { + return self.finish(.failure(error)) + } + } + + + + + let firstSentence: String + + if UserDefaults.standard.activeAppLimitIncludesExtensions + { + firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "") + } + else + { + firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "") + } + + let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "") + + + + let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in + self.finish(.failure(OperationError.cancelled)) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in + self.finish(.success(())) + }) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in + do { + try Self.removeExtensions(from: targetAppBundle.appExtensions) + return self.finish(.success(())) + } catch { + return self.finish(.failure(error)) + } + }) + + + + alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in + + + let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in + do { + try Self.removeExtensions(from: Set(selection)) + return self.finish(.success(())) + } catch { + return self.finish(.failure(error)) + } + } + + let suiview = popoverContentController.view! + suiview.translatesAutoresizingMaskIntoConstraints = false + + popoverContentController.modalPresentationStyle = .popover + + if let popoverPresentationController = popoverContentController.popoverPresentationController { + popoverPresentationController.sourceView = presentingViewController.view + popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4) + popoverPresentationController.delegate = popoverContentController + + DispatchQueue.main.async { + presentingViewController.present(popoverContentController, animated: true) + } + }else{ + self.finish(.failure( + OperationError.invalidParameters("RemoveAppExtensionsOperation: popoverContentController.popoverPresentationController is nil")) + ) + } + }) + + DispatchQueue.main.async { + presentingViewController.present(alertController, animated: true) + } + } +}