Files
SideStore/AltStore/Operations/RemoveAppExtensionsOperation.swift

236 lines
10 KiB
Swift
Raw Normal View History

//
// 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<Void>
{
let context: AppOperationContext
let localAppExtensions: Set<ALTApplication>?
init(context: AppOperationContext, localAppExtensions: Set<ALTApplication>?)
{
self.context = context
self.localAppExtensions = localAppExtensions
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,
localAppExtensions: localAppExtensions,
extensions: targetAppBundle.appExtensions,
context.authenticatedContext.presentingViewController)
}
private static func removeExtensions(from extensions: Set<ALTApplication>) throws {
for appExtension in extensions {
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
}
2025-02-08 04:45:22 +05:30
private func updateManifest() throws {
guard let app = context.app else {
return
}
let scInfoURL = app.fileURL.appendingPathComponent("SC_Info")
let manifestPlistURL = scInfoURL.appendingPathComponent("Manifest.plist")
if let manifestPlist = NSMutableDictionary(contentsOf: manifestPlistURL),
let sinfReplicationPaths = manifestPlist["SinfReplicationPaths"] as? [String]
{
let replacementPaths = sinfReplicationPaths.filter { !$0.starts(with: "PlugIns/") } // Filter out app extension paths.
manifestPlist["SinfReplicationPaths"] = replacementPaths
try manifestPlist.write(to: manifestPlistURL)
}
}
private func removeAppExtensions(from targetAppBundle: ALTApplication,
localAppExtensions: Set<ALTApplication>?,
extensions: Set<ALTApplication>,
_ presentingViewController: UIViewController?)
{
// target App Bundle doesn't contain extensions so don't bother
guard !targetAppBundle.appExtensions.isEmpty else {
return self.finish(.success(()))
}
// process extensionsInfo
let excessExtensions = processExtensionsInfo(from: targetAppBundle, localAppExtensions: localAppExtensions)
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 (localAppBundle = 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 && !alertController.isViewLoaded {
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<ALTApplication>,
_ presentingViewController: UIViewController) -> UIAlertController
{
/// Foreground prompt:
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)
2025-02-08 04:45:22 +05:30
try self.updateManifest()
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"))
)
}
})
return alertController
}
struct ExtensionsInfo{
let excessInTarget: Set<ALTApplication>
let necessaryInExisting: Set<ALTApplication>
}
private func processExtensionsInfo(from targetAppBundle: ALTApplication,
localAppExtensions: Set<ALTApplication>?) -> Set<ALTApplication>
{
//App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match
let targetAppEx: Set<ALTApplication> = targetAppBundle.appExtensions
let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier}
guard let extensionsInExistingApp = localAppExtensions 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<ALTApplication> = extensionsInExistingApp
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
let excessExtensionsInTargetApp = targetAppEx.filter{
!(existingAppExNames.contains($0.bundleIdentifier))
}
let isMatching = (targetAppEx.count == existingAppEx.count) && excessExtensionsInTargetApp.isEmpty
let diagnosticsMsg = "RemoveAppExtensionsOperation: App Extensions in localAppBundle and targetAppBundle are matching: \(isMatching)\n"
+ "RemoveAppExtensionsOperation: \nlocalAppBundleEx: \(existingAppExNames); \ntargetAppBundleEx: \(String(describing: targetAppExNames))\n"
print(diagnosticsMsg)
return excessExtensionsInTargetApp
}
private func backgroundModeExtensionsCleanup(excessExtensions: Set<ALTApplication>) {
// 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))
}
}
}