2021-10-25 22:27:30 -07:00
//
// P a t c h V i e w C o n t r o l l e r . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 1 0 / 2 0 / 2 1 .
// C o p y r i g h t © 2 0 2 1 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
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 , * )
2023-01-04 09:52:12 -05:00
final class PatchViewController : UIViewController
2021-10-25 22:27:30 -07:00
{
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
{
2023-10-18 14:09:06 -05:00
Logger . fugu14 . error ( " Failed to create temporary directory \( self . temporaryDirectory . lastPathComponent , privacy : . public ) . \( error . localizedDescription , privacy : . public ) " )
2021-10-25 22:27:30 -07:00
}
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 : " " )
2022-11-05 23:50:07 -07:00
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 \n Installing an untethered jailbreak requires a few extra steps, but SideStore will walk you through the process. \n \n Would you like to continue? " , comment : " " ) , app . name )
2021-10-25 22:27:30 -07:00
}
else
{
self . placeholderView . textLabel . text = NSLocalizedString ( " Jailbreak Supports Untethering " , comment : " " )
2022-11-05 23:50:07 -07:00
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 \n Installing an untethered jailbreak requires a few extra steps, but SideStore will walk you through the process. \n \n Would you like to continue? " , comment : " " ) , app . name )
2021-10-25 22:27:30 -07:00
}
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 \n This 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 ? ? " " // U s e n o n - e m p t y s t r i n g t o p r e v e n t l a b e l r e s i z i n g i t s e l f .
}
}
func finish ( with result : Result < Void , Error > )
{
do
{
try FileManager . default . removeItem ( at : self . temporaryDirectory )
}
catch
{
2023-10-18 14:09:06 -05:00
Logger . fugu14 . error ( " Failed to remove temporary directory \( self . temporaryDirectory . lastPathComponent , privacy : . public ) . \( error . localizedDescription , privacy : . public ) " )
2021-10-25 22:27:30 -07:00
}
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 )
{
// C a n c e l p e n d i n g j a i l b r e a k a p p i n s t a l l a t i o n s o w e c a n s t a r t a n e w o n e .
progress . cancel ( )
}
let appURL = InstalledApp . fileURL ( for : patchApp )
let cachedAppURL = self . temporaryDirectory . appendingPathComponent ( " Cached.app " )
do
{
// M a k e c o p y o f o r i g i n a l a p p , s o w e c a n r e p l a c e t h e c a c h e d p a t c h a p p w i t h i t l a t e r .
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
}
// R e p l a c e c a c h e d p a t c h a p p w i t h o r i g i n a l a p p s o w e c a n r e s u m e i n s t a l l i n g i t p o s t - r e b o o t .
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
{
// R e p l a c e p a t c h a p p n a m e w i t h c o r r e c t n a m e .
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
{
2023-10-18 14:09:06 -05:00
Logger . fugu14 . error ( " Error unzipping app bundle: \( error . localizedDescription , privacy : . public ) " )
2021-10-25 22:27:30 -07:00
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
// T h i s o b s e r v a t i o n i s w i l l E n t e r F o r e g r o u n d b e c a u s e p a t c h i n g s t a r t s i m m e d i a t e l y u p o n r e t u r n .
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 {
// R e f r e s h i n g e n s u r e s w e d o n ' t a t t e m p t t o p a t c h t h e a p p a g a i n ,
// s i n c e t h a t i s o n l y c h e c k e d w h e n i n s t a l l i n g a n e w a p p .
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
{
2024-08-06 10:43:52 +09:00
guard let ( bundleIdentifier , result ) = results . first else { throw refreshGroup ? . context . error ? ? OperationError . unknown ( ) }
2021-10-25 22:27:30 -07:00
_ = 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
}
}
}
}