mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
496 lines
20 KiB
Swift
496 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
|
|
|
|
extension PatchViewController
|
|
{
|
|
enum Step
|
|
{
|
|
case confirm
|
|
case install
|
|
case openApp
|
|
case patchApp
|
|
case reboot
|
|
case refresh
|
|
case finish
|
|
}
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
final 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
|
|
{
|
|
Logger.fugu14.error("Failed to create temporary directory \(self.temporaryDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
|
}
|
|
|
|
self.update()
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool)
|
|
{
|
|
super.viewWillAppear(animated)
|
|
|
|
if self.installedApp != nil
|
|
{
|
|
self.refreshApp()
|
|
}
|
|
}
|
|
}
|
|
|
|
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 SideStore 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 SideStore 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
|
|
{
|
|
Logger.fugu14.error("Failed to remove temporary directory \(self.temporaryDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
|
}
|
|
|
|
if let observation = self.didEnterBackgroundObservation
|
|
{
|
|
NotificationCenter.default.removeObserver(observation)
|
|
}
|
|
|
|
self.completionHandler?(result)
|
|
self.completionHandler = nil
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
{
|
|
Logger.fugu14.error("Error unzipping app bundle: \(error.localizedDescription, privacy: .public)")
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|