mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
If a jailbreak app contains the relevant Fugu14 entries in its Info.plist, AltStore will automatically guide the user through the Fugu14 untether process before installing the jailbreak.
500 lines
20 KiB
Swift
500 lines
20 KiB
Swift
//
|
|
// PatchViewController.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 10/20/21.
|
|
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Combine
|
|
|
|
import AltStoreCore
|
|
import AltSign
|
|
import Roxas
|
|
|
|
@available(iOS 14.0, *)
|
|
extension PatchViewController
|
|
{
|
|
enum Step
|
|
{
|
|
case confirm
|
|
case install
|
|
case openApp
|
|
case patchApp
|
|
case reboot
|
|
case refresh
|
|
case finish
|
|
}
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
class PatchViewController: UIViewController
|
|
{
|
|
var patchApp: AnyApp?
|
|
var installedApp: InstalledApp?
|
|
|
|
var completionHandler: ((Result<Void, Error>) -> Void)?
|
|
|
|
private let context = AuthenticatedOperationContext()
|
|
|
|
private var currentStep: Step = .confirm {
|
|
didSet {
|
|
DispatchQueue.main.async {
|
|
self.update()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var buttonHandler: (() -> Void)?
|
|
private var resignedApp: ALTApplication?
|
|
|
|
private lazy var temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
|
|
|
|
private var didEnterBackgroundObservation: NSObjectProtocol?
|
|
private weak var cancellableProgress: Progress?
|
|
|
|
@IBOutlet private var placeholderView: RSTPlaceholderView!
|
|
@IBOutlet private var taskDescriptionLabel: UILabel!
|
|
@IBOutlet private var pillButton: PillButton!
|
|
@IBOutlet private var cancelBarButtonItem: UIBarButtonItem!
|
|
@IBOutlet private var cancelButton: UIButton!
|
|
|
|
override func viewDidLoad()
|
|
{
|
|
super.viewDidLoad()
|
|
|
|
self.isModalInPresentation = true
|
|
|
|
self.placeholderView.stackView.spacing = 20
|
|
self.placeholderView.textLabel.textColor = .white
|
|
|
|
self.placeholderView.detailTextLabel.textAlignment = .left
|
|
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
|
|
|
|
self.buttonHandler = { [weak self] in
|
|
self?.startProcess()
|
|
}
|
|
|
|
do
|
|
{
|
|
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to create temporary directory:", error)
|
|
}
|
|
|
|
self.update()
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool)
|
|
{
|
|
super.viewWillAppear(animated)
|
|
|
|
if self.installedApp != nil
|
|
{
|
|
self.refreshApp()
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
private extension PatchViewController
|
|
{
|
|
func update()
|
|
{
|
|
self.cancelButton.alpha = 0.0
|
|
|
|
switch self.currentStep
|
|
{
|
|
case .confirm:
|
|
guard let app = self.patchApp else { break }
|
|
|
|
if UIDevice.current.isUntetheredJailbreakRequired
|
|
{
|
|
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Requires Untethering", comment: "")
|
|
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak is untethered, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
|
|
}
|
|
else
|
|
{
|
|
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Supports Untethering", comment: "")
|
|
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak has an untethered version, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
|
|
}
|
|
|
|
self.pillButton.setTitle(NSLocalizedString("Install Untethered Jailbreak", comment: ""), for: .normal)
|
|
|
|
self.cancelButton.alpha = 1.0
|
|
|
|
case .install:
|
|
guard let app = self.patchApp else { break }
|
|
|
|
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Installing %@ placeholder…", comment: ""), app.name)
|
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("A placeholder app needs to be installed in order to prepare your device for untethering.\n\nThis may take a few moments.", comment: "")
|
|
|
|
case .openApp:
|
|
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
|
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
|
|
|
|
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
|
|
|
|
case .patchApp:
|
|
guard let app = self.patchApp else { break }
|
|
|
|
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Patching %@ placeholder…", comment: ""), app.name)
|
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("This will take a few moments. Please do not turn off the screen or leave the app until patching is complete.", comment: "")
|
|
|
|
self.pillButton.setTitle(NSLocalizedString("Patch Placeholder", comment: ""), for: .normal)
|
|
|
|
case .reboot:
|
|
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
|
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
|
|
|
|
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
|
|
|
|
case .refresh:
|
|
guard let installedApp = self.installedApp else { break }
|
|
|
|
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish installing %@?", comment: ""), installedApp.name)
|
|
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("In order to finish jailbreaking this device, you need to install %@ then follow the instructions in the app.", comment: ""), installedApp.name)
|
|
|
|
self.pillButton.setTitle(String(format: NSLocalizedString("Install %@", comment: ""), installedApp.name), for: .normal)
|
|
|
|
case .finish:
|
|
guard let installedApp = self.installedApp else { break }
|
|
|
|
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish in %@", comment: ""), installedApp.name)
|
|
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("Follow the instructions in %@ to finish jailbreaking this device.", comment: ""), installedApp.name)
|
|
|
|
self.pillButton.setTitle(String(format: NSLocalizedString("Open %@", comment: ""), installedApp.name), for: .normal)
|
|
}
|
|
}
|
|
|
|
func present(_ error: Error, title: String)
|
|
{
|
|
DispatchQueue.main.async {
|
|
let nsError = error as NSError
|
|
|
|
let alertController = UIAlertController(title: nsError.localizedFailure ?? title, message: error.localizedDescription, preferredStyle: .alert)
|
|
alertController.addAction(.ok)
|
|
self.present(alertController, animated: true, completion: nil)
|
|
|
|
self.setProgress(nil, description: nil)
|
|
}
|
|
}
|
|
|
|
func setProgress(_ progress: Progress?, description: String?)
|
|
{
|
|
DispatchQueue.main.async {
|
|
self.pillButton.progress = progress
|
|
self.taskDescriptionLabel.text = description ?? " " // Use non-empty string to prevent label resizing itself.
|
|
}
|
|
}
|
|
|
|
func finish(with result: Result<Void, Error>)
|
|
{
|
|
do
|
|
{
|
|
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to remove temporary directory:", error)
|
|
}
|
|
|
|
if let observation = self.didEnterBackgroundObservation
|
|
{
|
|
NotificationCenter.default.removeObserver(observation)
|
|
}
|
|
|
|
self.completionHandler?(result)
|
|
self.completionHandler = nil
|
|
}
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
private extension PatchViewController
|
|
{
|
|
@IBAction func performButtonAction()
|
|
{
|
|
self.buttonHandler?()
|
|
}
|
|
|
|
@IBAction func cancel()
|
|
{
|
|
self.finish(with: .success(()))
|
|
|
|
self.cancellableProgress?.cancel()
|
|
}
|
|
|
|
@IBAction func installRegularJailbreak()
|
|
{
|
|
guard let app = self.patchApp else { return }
|
|
|
|
let title: String
|
|
let message: String
|
|
|
|
if UIDevice.current.isUntetheredJailbreakRequired
|
|
{
|
|
title = NSLocalizedString("Untethering Required", comment: "")
|
|
message = String(format: NSLocalizedString("%@ can not jailbreak this device unless you untether it first. Are you sure you want to install without untethering?", comment: ""), app.name)
|
|
}
|
|
else
|
|
{
|
|
title = NSLocalizedString("Untethering Recommended", comment: "")
|
|
message = String(format: NSLocalizedString("Untethering this jailbreak will prevent %@ from expiring, even after 7 days or rebooting the device. Are you sure you want to install without untethering?", comment: ""), app.name)
|
|
}
|
|
|
|
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
|
alertController.addAction(UIAlertAction(title: NSLocalizedString("Install Without Untethering", comment: ""), style: .default) { _ in
|
|
self.finish(with: .failure(OperationError.cancelled))
|
|
})
|
|
alertController.addAction(.cancel)
|
|
self.present(alertController, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
private extension PatchViewController
|
|
{
|
|
func startProcess()
|
|
{
|
|
guard let patchApp = self.patchApp else { return }
|
|
|
|
self.currentStep = .install
|
|
|
|
if let progress = AppManager.shared.installationProgress(for: patchApp)
|
|
{
|
|
// Cancel pending jailbreak app installation so we can start a new one.
|
|
progress.cancel()
|
|
}
|
|
|
|
let appURL = InstalledApp.fileURL(for: patchApp)
|
|
let cachedAppURL = self.temporaryDirectory.appendingPathComponent("Cached.app")
|
|
|
|
do
|
|
{
|
|
// Make copy of original app, so we can replace the cached patch app with it later.
|
|
try FileManager.default.copyItem(at: appURL, to: cachedAppURL, shouldReplace: true)
|
|
}
|
|
catch
|
|
{
|
|
self.present(error, title: NSLocalizedString("Could not back up jailbreak app.", comment: ""))
|
|
return
|
|
}
|
|
|
|
var unzippingError: Error?
|
|
let refreshGroup = AppManager.shared.install(patchApp, presentingViewController: self, context: self.context) { result in
|
|
do
|
|
{
|
|
_ = try result.get()
|
|
|
|
if let unzippingError = unzippingError
|
|
{
|
|
throw unzippingError
|
|
}
|
|
|
|
// Replace cached patch app with original app so we can resume installing it post-reboot.
|
|
try FileManager.default.copyItem(at: cachedAppURL, to: appURL, shouldReplace: true)
|
|
|
|
self.openApp()
|
|
}
|
|
catch
|
|
{
|
|
self.present(error, title: String(format: NSLocalizedString("Could not install %@ placeholder.", comment: ""), patchApp.name))
|
|
}
|
|
}
|
|
refreshGroup.beginInstallationHandler = { (installedApp) in
|
|
do
|
|
{
|
|
// Replace patch app name with correct name.
|
|
installedApp.name = patchApp.name
|
|
|
|
let ipaURL = installedApp.refreshedIPAURL
|
|
let resignedAppURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: self.temporaryDirectory)
|
|
|
|
self.resignedApp = ALTApplication(fileURL: resignedAppURL)
|
|
}
|
|
catch
|
|
{
|
|
print("Error unzipping app bundle:", error)
|
|
unzippingError = error
|
|
}
|
|
}
|
|
self.setProgress(refreshGroup.progress, description: nil)
|
|
|
|
self.cancellableProgress = refreshGroup.progress
|
|
}
|
|
|
|
func openApp()
|
|
{
|
|
guard let patchApp = self.patchApp else { return }
|
|
|
|
self.setProgress(nil, description: nil)
|
|
self.currentStep = .openApp
|
|
|
|
// This observation is willEnterForeground because patching starts immediately upon return.
|
|
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { (notification) in
|
|
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
|
self.patchApplication()
|
|
}
|
|
|
|
self.buttonHandler = { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
#if !targetEnvironment(simulator)
|
|
|
|
let openURL = InstalledApp.openAppURL(for: patchApp)
|
|
UIApplication.shared.open(openURL) { success in
|
|
guard !success else { return }
|
|
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
|
|
}
|
|
|
|
#endif
|
|
}
|
|
}
|
|
|
|
func patchApplication()
|
|
{
|
|
guard let resignedApp = self.resignedApp else { return }
|
|
|
|
self.currentStep = .patchApp
|
|
|
|
self.buttonHandler = { [weak self] in
|
|
self?.patchApplication()
|
|
}
|
|
|
|
let patchAppOperation = AppManager.shared.patch(resignedApp: resignedApp, presentingViewController: self, context: self.context) { result in
|
|
switch result
|
|
{
|
|
case .failure(let error): self.present(error, title: String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), resignedApp.name))
|
|
case .success: self.rebootDevice()
|
|
}
|
|
}
|
|
patchAppOperation.progressHandler = { (progress, description) in
|
|
self.setProgress(progress, description: description)
|
|
}
|
|
self.cancellableProgress = patchAppOperation.progress
|
|
}
|
|
|
|
func rebootDevice()
|
|
{
|
|
guard let patchApp = self.patchApp else { return }
|
|
|
|
self.setProgress(nil, description: nil)
|
|
self.currentStep = .reboot
|
|
|
|
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
|
|
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
|
|
|
var patchedApps = UserDefaults.standard.patchedApps ?? []
|
|
if !patchedApps.contains(patchApp.bundleIdentifier)
|
|
{
|
|
patchedApps.append(patchApp.bundleIdentifier)
|
|
UserDefaults.standard.patchedApps = patchedApps
|
|
}
|
|
|
|
self.finish(with: .success(()))
|
|
}
|
|
|
|
self.buttonHandler = { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
#if !targetEnvironment(simulator)
|
|
|
|
let openURL = InstalledApp.openAppURL(for: patchApp)
|
|
UIApplication.shared.open(openURL) { success in
|
|
guard !success else { return }
|
|
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
|
|
}
|
|
|
|
#endif
|
|
}
|
|
}
|
|
|
|
func refreshApp()
|
|
{
|
|
guard let installedApp = self.installedApp else { return }
|
|
|
|
self.currentStep = .refresh
|
|
|
|
self.buttonHandler = { [weak self] in
|
|
guard let self = self else { return }
|
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
|
|
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
|
|
tempApp.needsResign = true
|
|
|
|
let errorTitle = String(format: NSLocalizedString("Could not install %@.", comment: ""), tempApp.name)
|
|
|
|
do
|
|
{
|
|
try context.save()
|
|
|
|
installedApp.managedObjectContext?.perform {
|
|
// Refreshing ensures we don't attempt to patch the app again,
|
|
// since that is only checked when installing a new app.
|
|
let refreshGroup = AppManager.shared.refresh([installedApp], presentingViewController: self, group: nil)
|
|
refreshGroup.completionHandler = { [weak refreshGroup, weak self] (results) in
|
|
guard let self = self else { return }
|
|
|
|
do
|
|
{
|
|
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
|
|
_ = try result.get()
|
|
|
|
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
|
|
{
|
|
patchedApps.remove(at: index)
|
|
UserDefaults.standard.patchedApps = patchedApps
|
|
}
|
|
|
|
self.finish()
|
|
}
|
|
catch
|
|
{
|
|
self.present(error, title: errorTitle)
|
|
}
|
|
}
|
|
self.setProgress(refreshGroup.progress, description: String(format: NSLocalizedString("Installing %@...", comment: ""), installedApp.name))
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
self.present(error, title: errorTitle)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func finish()
|
|
{
|
|
guard let installedApp = self.installedApp else { return }
|
|
|
|
self.setProgress(nil, description: nil)
|
|
self.currentStep = .finish
|
|
|
|
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
|
|
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
|
|
self.finish(with: .success(()))
|
|
}
|
|
|
|
installedApp.managedObjectContext?.perform {
|
|
let appName = installedApp.name
|
|
let openURL = installedApp.openAppURL
|
|
|
|
self.buttonHandler = { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
#if !targetEnvironment(simulator)
|
|
|
|
UIApplication.shared.open(openURL) { success in
|
|
guard !success else { return }
|
|
self.present(OperationError.openAppFailed(name: appName), title: String(format: NSLocalizedString("Could not open %@.", comment: ""), appName))
|
|
}
|
|
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|