From 05dc365dffa5a5bc4337051a7129938293b27ec9 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 17 May 2020 23:44:36 -0700 Subject: [PATCH] =?UTF-8?q?Adds=20altstore://install=3Furl=3D[link]=20deep?= =?UTF-8?q?=20link=20to=20install=20remote=20.ipa=E2=80=99s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AltStore/AppDelegate.swift | 10 + AltStore/My Apps/MyAppsViewController.swift | 281 +++++++++++++++----- 2 files changed, 221 insertions(+), 70 deletions(-) diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index f31273b7..cf3cae3a 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -172,6 +172,16 @@ private extension AppDelegate return true + case "install": + 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 } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL]) + } + + return true + default: return false } } diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index a74afe56..4022bf90 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -32,6 +32,7 @@ extension MyAppsViewController class MyAppsViewController: UICollectionViewController { private let coordinator = NSFileCoordinator() + private let operationQueue = OperationQueue() private lazy var dataSource = self.makeDataSource() private lazy var noUpdatesDataSource = self.makeNoUpdatesDataSource() @@ -694,14 +695,157 @@ private extension MyAppsViewController self.present(documentPickerViewController, animated: true, completion: nil) } - func sideloadApp(at fileURL: URL, completion: @escaping (Result) -> Void) + func sideloadApp(at url: URL, completion: @escaping (Result) -> Void) { - let temporaryDirectory = FileManager.default.uniqueTemporaryURL() + let progress = Progress.discreteProgress(totalUnitCount: 100) self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true - func finish(_ result: Result) + class Context { + var fileURL: URL? + var application: ALTApplication? + var installedApp: InstalledApp? { + didSet { + self.installedAppContext = self.installedApp?.managedObjectContext + } + } + private var installedAppContext: NSManagedObjectContext? + + var error: Error? + } + + let temporaryDirectory = FileManager.default.uniqueTemporaryURL() + let unzippedAppDirectory = temporaryDirectory.appendingPathComponent("App") + + let context = Context() + + let downloadOperation: RSTAsyncBlockOperation? + + if url.isFileURL + { + downloadOperation = nil + context.fileURL = url + progress.totalUnitCount -= 20 + } + else + { + let downloadProgress = Progress.discreteProgress(totalUnitCount: 100) + downloadOperation = RSTAsyncBlockOperation { (operation) in + let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in + do + { + let (fileURL, _) = try Result((fileURL, response), error).get() + + try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) + + let destinationURL = temporaryDirectory.appendingPathComponent("App.ipa") + try FileManager.default.moveItem(at: fileURL, to: destinationURL) + + context.fileURL = destinationURL + } + catch + { + context.error = error + } + operation.finish() + } + downloadProgress.addChild(downloadTask.progress, withPendingUnitCount: 100) + downloadTask.resume() + } + progress.addChild(downloadProgress, withPendingUnitCount: 20) + } + + let unzipProgress = Progress.discreteProgress(totalUnitCount: 1) + let unzipAppOperation = BlockOperation { + do + { + if let error = context.error + { + throw error + } + + guard let fileURL = context.fileURL else { throw OperationError.invalidParameters } + + try FileManager.default.createDirectory(at: unzippedAppDirectory, withIntermediateDirectories: true, attributes: nil) + let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: unzippedAppDirectory) + + guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp } + context.application = application + + unzipProgress.completedUnitCount = 1 + } + catch + { + context.error = error + } + } + progress.addChild(unzipProgress, withPendingUnitCount: 10) + + if let downloadOperation = downloadOperation + { + unzipAppOperation.addDependency(downloadOperation) + } + + let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1) + let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in + do + { + if let error = context.error + { + throw error + } + + guard let application = context.application else { throw OperationError.invalidParameters } + + DispatchQueue.main.async { + self?.removeAppExtensions(from: application) { (result) in + switch result + { + case .success: removeAppExtensionsProgress.completedUnitCount = 1 + case .failure(let error): context.error = error + } + operation.finish() + } + } + } + catch + { + context.error = error + operation.finish() + } + } + removeAppExtensionsOperation.addDependency(unzipAppOperation) + progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5) + + let installProgress = Progress.discreteProgress(totalUnitCount: 100) + let installAppOperation = RSTAsyncBlockOperation { (operation) in + do + { + if let error = context.error + { + throw error + } + + guard let application = context.application else { throw OperationError.invalidParameters } + + let progress = AppManager.shared.install(application, presentingViewController: self) { (result) in + switch result + { + case .success(let installedApp): context.installedApp = installedApp + case .failure(let error): context.error = error + } + operation.finish() + } + installProgress.addChild(progress, withPendingUnitCount: 100) + } + catch + { + context.error = error + operation.finish() + } + } + installAppOperation.completionBlock = { try? FileManager.default.removeItem(at: temporaryDirectory) DispatchQueue.main.async { @@ -709,13 +853,17 @@ private extension MyAppsViewController self.sideloadingProgressView.observedProgress = nil self.sideloadingProgressView.setHidden(true, animated: true) - switch result + switch Result(context.installedApp, context.error) { case .success(let app): - print("Successfully installed app:", app.bundleIdentifier) completion(.success(())) - case .failure(OperationError.cancelled): break + app.managedObjectContext?.perform { + print("Successfully installed app:", app.bundleIdentifier) + } + + case .failure(OperationError.cancelled): + completion(.failure((OperationError.cancelled))) case .failure(let error): let toastView = ToastView(error: error) @@ -725,68 +873,16 @@ private extension MyAppsViewController } } } + progress.addChild(installProgress, withPendingUnitCount: 65) + installAppOperation.addDependency(removeAppExtensionsOperation) - DispatchQueue.global().async { - do - { - try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) - - let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory) - - guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp } - - func install() - { - self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in - finish(result.map { _ in application }) - } - - DispatchQueue.main.async { - self.sideloadingProgressView.progress = 0 - self.sideloadingProgressView.isHidden = false - self.sideloadingProgressView.observedProgress = self.sideloadingProgress - } - } - - if !application.appExtensions.isEmpty - { - DispatchQueue.main.async { - let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions. Would you like to remove this app's app extensions so they don't count towards your limit?", comment: ""), preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in - finish(.failure(OperationError.cancelled)) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in - install() - }) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in - do - { - for appExtension in application.appExtensions - { - try FileManager.default.removeItem(at: appExtension.fileURL) - } - - install() - } - catch - { - finish(.failure(error)) - } - }) - - self.present(alertController, animated: true, completion: nil) - } - } - else - { - install() - } - } - catch - { - finish(.failure(error)) - } - } + self.sideloadingProgress = progress + self.sideloadingProgressView.progress = 0 + self.sideloadingProgressView.isHidden = false + self.sideloadingProgressView.observedProgress = self.sideloadingProgress + + let operations = [downloadOperation, unzipAppOperation, removeAppExtensionsOperation, installAppOperation].compactMap { $0 } + self.operationQueue.addOperations(operations, waitUntilFinished: false) } @IBAction func activateApp(_ sender: UIButton) @@ -834,6 +930,49 @@ private extension MyAppsViewController cell.bannerView.iconImageView.isIndicatingActivity = false } + + func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result) -> Void) + { + guard !application.appExtensions.isEmpty else { return completion(.success(())) } + + let firstSentence: String + + if UserDefaults.standard.activeAppLimitIncludesExtensions + { + firstSentence = NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions.", comment: "") + } + else + { + firstSentence = NSLocalizedString("Free developer accounts 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?", 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 + { + try FileManager.default.removeItem(at: appExtension.fileURL) + } + + completion(.success(())) + } + catch + { + completion(.failure(error)) + } + }) + + self.present(alertController, animated: true, completion: nil) + } } private extension MyAppsViewController @@ -1098,12 +1237,14 @@ private extension MyAppsViewController // Make sure left UIBarButtonItem has been set. self.loadViewIfNeeded() - guard let fileURL = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return } + guard let url = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return } - self.sideloadApp(at: fileURL) { (result) in + self.sideloadApp(at: url) { (result) in + guard url.isFileURL else { return } + do { - try FileManager.default.removeItem(at: fileURL) + try FileManager.default.removeItem(at: url) } catch {