From e8798499d398afe5e7a5a4b329cff9eaef72887a Mon Sep 17 00:00:00 2001 From: Magesh K <47920326+mahee96@users.noreply.github.com> Date: Mon, 13 Jan 2025 07:20:44 +0530 Subject: [PATCH] [removeExtensions]: Bug-Fix: 1. existing App is ALTApplication not InstalledApp - corrected this 2. process as background mode if prompt can't be made or else signal error if operation context changed in-flight --- .../Operations/Errors/OperationError.swift | 11 +- .../RemoveAppExtensionsOperation.swift | 131 ++++++++++++------ 2 files changed, 97 insertions(+), 45 deletions(-) diff --git a/AltStore/Operations/Errors/OperationError.swift b/AltStore/Operations/Errors/OperationError.swift index 775ba34a..52486a1b 100644 --- a/AltStore/Operations/Errors/OperationError.swift +++ b/AltStore/Operations/Errors/OperationError.swift @@ -58,6 +58,8 @@ extension OperationError case anisetteV3Error//(message: String) case cacheClearError//(errors: [String]) case noWiFi + + case invalidOperationContext } static var cancelled: CancellationError { CancellationError() } @@ -130,6 +132,10 @@ extension OperationError OperationError(code: .invalidParameters, failureReason: message) } + static func invalidOperationContext(_ message: String? = nil) -> OperationError { + OperationError(code: .invalidOperationContext, failureReason: message) + } + static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError { OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line) } @@ -232,7 +238,10 @@ struct OperationError: ALTLocalizedError { case .invalidParameters: let message = self._failureReason.map { ": \n\($0)" } ?? "." - return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message) + return String(format: NSLocalizedString("Invalid parameters\n%@", comment: ""), message) + case .invalidOperationContext: + let message = self._failureReason.map { ": \n\($0)" } ?? "." + return String(format: NSLocalizedString("Invalid Operation Context\n%@", comment: ""), message) case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "") case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "") case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "") diff --git a/AltStore/Operations/RemoveAppExtensionsOperation.swift b/AltStore/Operations/RemoveAppExtensionsOperation.swift index bdb20c57..1b07e123 100644 --- a/AltStore/Operations/RemoveAppExtensionsOperation.swift +++ b/AltStore/Operations/RemoveAppExtensionsOperation.swift @@ -41,8 +41,9 @@ final class RemoveAppExtensionsOperation: ResultOperation )) } + self.removeAppExtensions(from: targetAppBundle, - appInDatabase: appInDatabase as? InstalledApp, + appInDatabase: appInDatabase as? ALTApplication, extensions: targetAppBundle.appExtensions, context.authenticatedContext.presentingViewController) @@ -59,9 +60,9 @@ final class RemoveAppExtensionsOperation: ResultOperation private func removeAppExtensions(from targetAppBundle: ALTApplication, - appInDatabase: InstalledApp?, - extensions: Set, - _ presentingViewController: UIViewController?) + appInDatabase: ALTApplication?, + extensions: Set, + _ presentingViewController: UIViewController?) { // target App Bundle doesn't contain extensions so don't bother @@ -69,47 +70,39 @@ final class RemoveAppExtensionsOperation: ResultOperation 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)) + // process extensionsInfo + let excessExtensions = processExtensionsInfo(from: targetAppBundle, appInDatabase: appInDatabase) + + DispatchQueue.main.async { + guard let presentingViewController: UIViewController = presentingViewController, + presentingViewController.viewIfLoaded?.window != nil else { + // background mode: remove only the excess extensions automatically for re-installs + // keep all extensions for fresh install (appInDatabase = nil) + return self.backgroundModeExtensionsCleanup(excessExtensions: excessExtensions) + } + + // present prompt to the user if we have a view context + let alertController = self.createAlertDialog(from: targetAppBundle, extensions: extensions, presentingViewController) + presentingViewController.present(alertController, animated: true){ + + // if for any reason the view wasn't presented, then just signal that as error + if presentingViewController.presentedViewController == nil { + let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." + + "\nDid you move to different screen or background after starting the operation?" + self.finish(.failure( + OperationError.invalidOperationContext(errMsg) + )) + } } } - - - + } + + private func createAlertDialog(from targetAppBundle: ALTApplication, + extensions: Set, + _ presentingViewController: UIViewController) -> UIAlertController + { + /// Foreground prompt: let firstSentence: String if UserDefaults.standard.activeAppLimitIncludesExtensions @@ -175,8 +168,58 @@ final class RemoveAppExtensionsOperation: ResultOperation } }) - DispatchQueue.main.async { - presentingViewController.present(alertController, animated: true) + return alertController + } + + struct ExtensionsInfo{ + let excessInTarget: Set + let necessaryInExisting: Set + } + + private func processExtensionsInfo(from targetAppBundle: ALTApplication, + appInDatabase: ALTApplication?) -> Set + { + //App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match + let targetAppEx: Set = targetAppBundle.appExtensions + let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier} + + guard let extensionsInExistingApp = appInDatabase?.appExtensions else { + let diagnosticsMsg = "RemoveAppExtensionsOperation: ExistingApp is nil, Hence keeping all app extensions from targetAppBundle" + + "RemoveAppExtensionsOperation: ExistingAppEx: nil; targetAppBundleEx: \(targetAppExNames)" + print(diagnosticsMsg) + return Set() // nothing is excess since we are keeping all, so returning empty + } + + let existingAppEx: Set = extensionsInExistingApp + + let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier} + + let excessExtensionsInTargetApp = targetAppEx.filter{ + !(existingAppExNames.contains($0.bundleIdentifier)) + } + + let excessExtensionsInExistingApp = existingAppEx.filter{ + !(targetAppExNames.contains($0.bundleIdentifier)) + } + + 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) + + return excessExtensionsInTargetApp + } + + private func backgroundModeExtensionsCleanup(excessExtensions: Set) { + // 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: \(excessExtensions.map{$0.bundleIdentifier})") + + do { + try Self.removeExtensions(from: excessExtensions) + return self.finish(.success(())) + } catch { + return self.finish(.failure(error)) } } }