mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 23:03:27 +01:00
* [Shared] Revises ALTLocalizedError protocol * Refactors errors to conform to revised ALTLocalizedError protocol * [Missing Commit] Remaining changes for ALTLocalizedError * [AltServer] Refactors errors to conform to revised ALTLocalizedError protocol * [Missing Commit] Declares ALTLocalizedTitleErrorKey + ALTLocalizedDescriptionKey * Updates Objective-C errors to match revised ALTLocalizedError * [Missing Commit] Unnecessary ALTLocalizedDescription logic * [Shared] Refactors NSError.withLocalizedFailure to properly support ALTLocalizedError * [Shared] Supports adding localized titles to errors via NSError.withLocalizedTitle() * Revises ErrorResponse logic to support arbitrary errors and user info values * [Missed Commit] Renames CodableServerError to CodableError * Merges ConnectionError into OperationError * [Missed Commit] Doesn’t check ALTWrappedError’s userInfo for localizedDescription * [Missed] Fixes incorrect errorDomain for ALTErrorEnums * [Missed] Removes nonexistent ALTWrappedError.h * Includes source file and line number in OperationError.unknown failureReason * Adds localizedTitle to AppManager operation errors * Fixes adding localizedTitle + localizedFailure to ALTWrappedError * Updates ToastView to use error’s localizedTitle as title * [Shared] Adds NSError.formattedDetailedDescription(with:) Returns formatted NSAttributedString containing all user info values intended for displaying to the user. * [Shared] Updates Error.localizedErrorCode to say “code” instead of “error” * Conforms ALTLocalizedError to CustomStringConvertible * Adds “View More Details” option to Error Log context menu to view detailed error description * [Shared] Fixes NSError.formattedDetailedDescription appearing black in dark mode * [AltServer] Updates error alert to match revised error logic Uses error’s localizedTitle as alert title. * [AltServer] Adds “View More Details” button to error alert to view detailed error info * [AltServer] Renames InstallError to OperationError and conforms to ALTErrorEnum * [Shared] Removes CodableError support for Date user info values Not currently used, and we don’t want to accidentally parse a non-Date as a Date in the meantime. * [Shared] Includes dynamic UserInfoValueProvider values in NSError.formattedDetailedDescription() * [Shared] Includes source file + line in NSError.formattedDetailedDescription() Automatically captures source file + line when throwing ALTErrorEnums. * [Shared] Captures source file + line for unknown errors * Removes sourceFunction from OperationError * Adds localizedTitle to AuthenticationViewController errors * [Shared] Moves nested ALTWrappedError logic to ALTWrappedError initializer * [AltServer] Removes now-redundant localized failure from JIT errors All JIT errors now have a localizedTitle which effectively says the same thing. * Makes OperationError.Code start at 1000 “Connection errors” subsection starts at 1200. * [Shared] Updates Error domains to revised [Source].[ErrorType] format * Updates ALTWrappedError.localizedDescription to prioritize using wrapped NSLocalizedDescription as failure reason * Makes ALTAppleAPIError codes start at 3000 * [AltServer] Adds relevant localizedFailures to ALTDeviceManager.installApplication() errors * Revises OperationError failureReasons and recovery suggestions All failure reasons now read correctly when preceded by a failure reason and “because”. * Revises ALTServerError error messages All failure reasons now read correctly when preceded by a failure reason and “because”. * Most failure reasons now read correctly when preceded by a failure reason and “because”. * ALTServerErrorUnderlyingError forwards all user info provider calls to underlying error. * Revises error messages for ALTAppleAPIErrorIncorrectCredentials * [Missed] Removes NSError+AltStore.swift from AltStore target * [Shared] Updates AltServerErrorDomain to revised [Source].[ErrorType] format * [Shared] Removes “code” from Error.localizedErrorCode * [Shared] Makes ALTServerError codes (appear to) start at 2000 We can’t change the actual error codes without breaking backwards compatibility, so instead we just add 2000 whenever we display ALTServerError codes to the user. * Moves VerificationError.errorFailure to VerifyAppOperation * Supports custom failure reason for OperationError.unknown * [Shared] Changes AltServerErrorDomain to “AltServer.ServerError” * [Shared] Converts ALTWrappedError to Objective-C class NSError subclasses must be written in ObjC for Swift.Error <-> NSError bridging to work correctly. # Conflicts: # AltStore.xcodeproj/project.pbxproj * Fixes decoding CodableError nested user info values
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
|
|
}
|
|
}
|
|
}
|
|
}
|