2019-05-31 18:24:08 -07:00
//
// A p p M a n a g 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 5 / 2 9 / 1 9 .
// C o p y r i g h t © 2 0 1 9 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 .
//
2023-03-01 00:48:36 -05:00
import Combine
2019-05-31 18:24:08 -07:00
import Foundation
2023-03-01 00:48:36 -05:00
import Intents
import MobileCoreServices
2019-05-31 18:24:08 -07:00
import UIKit
2019-09-21 22:31:10 -07:00
import UserNotifications
2020-09-15 13:51:29 -07:00
import WidgetKit
2023-03-10 20:28:52 -05:00
2019-05-31 18:24:08 -07:00
import AltSign
2023-03-01 14:36:52 -05:00
import SideKit
2023-03-01 00:48:36 -05:00
import SideStoreCore
2023-03-01 14:36:52 -05:00
import RoxasUIKit
2023-04-02 02:28:12 -04:00
import OSLog
#if canImport ( Logging )
import Logging
#endif
2019-05-31 18:24:08 -07:00
2023-03-01 19:09:33 -05:00
public extension AppManager {
2022-04-14 17:39:43 -07:00
static let didFetchSourceNotification = Notification . Name ( " io.altstore.AppManager.didFetchSource " )
static let didUpdatePatronsNotification = Notification . Name ( " io.altstore.AppManager.didUpdatePatrons " )
2023-03-01 00:48:36 -05:00
2019-09-21 22:31:10 -07:00
static let expirationWarningNotificationID = " altstore-expiration-warning "
2021-09-03 13:57:15 -05:00
static let enableJITResultNotificationID = " altstore-enable-jit "
2019-06-17 14:49:23 -07:00
}
2020-09-08 13:12:40 -07:00
@ available ( iOS 13 , * )
2023-03-01 19:09:33 -05:00
public final class AppManagerPublisher : ObservableObject {
2020-09-08 13:12:40 -07:00
@ Published
fileprivate ( set ) var installationProgress = [ String : Progress ] ( )
2023-03-01 00:48:36 -05:00
2020-09-08 13:12:40 -07:00
@ Published
fileprivate ( set ) var refreshProgress = [ String : Progress ] ( )
}
2023-03-01 00:48:36 -05:00
private func = = ( lhs : OperatingSystemVersion , rhs : OperatingSystemVersion ) -> Bool {
lhs . majorVersion = = rhs . majorVersion && lhs . minorVersion = = rhs . minorVersion && lhs . patchVersion = = rhs . patchVersion
2021-10-25 22:27:30 -07:00
}
2023-03-01 19:09:33 -05:00
public final class AppManager {
public static let shared = AppManager ( )
2023-03-01 00:48:36 -05:00
2022-04-14 17:39:43 -07:00
private ( set ) var updatePatronsResult : Result < Void , Error > ?
2023-03-01 00:48:36 -05:00
2019-06-05 18:05:21 -07:00
private let operationQueue = OperationQueue ( )
2020-03-06 17:08:35 -08:00
private let serialOperationQueue = OperationQueue ( )
2020-09-08 13:12:40 -07:00
private var installationProgress = [ String : Progress ] ( ) {
didSet {
guard #available ( iOS 13 , * ) else { return }
2023-03-01 00:48:36 -05:00
publisher . installationProgress = installationProgress
2020-09-08 13:12:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
2020-09-08 13:12:40 -07:00
private var refreshProgress = [ String : Progress ] ( ) {
didSet {
guard #available ( iOS 13 , * ) else { return }
2023-03-01 00:48:36 -05:00
publisher . refreshProgress = refreshProgress
2020-09-08 13:12:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
2022-08-17 15:22:43 -05:00
@ available ( iOS 13 , * )
private ( set ) var publisher : AppManagerPublisher {
get { _publisher as ! AppManagerPublisher }
set { _publisher = newValue }
}
2023-03-01 00:48:36 -05:00
2022-08-17 15:22:43 -05:00
@ available ( iOS 13 , * )
private ( set ) var cancellables : Set < AnyCancellable > {
get { _cancellables as ! Set < AnyCancellable > }
set { _cancellables = newValue }
}
2023-03-01 00:48:36 -05:00
2022-08-17 15:22:43 -05:00
private lazy var _publisher : Any = {
guard #available ( iOS 13 , * ) else { fatalError ( ) }
return AppManagerPublisher ( )
} ( )
2023-03-01 00:48:36 -05:00
2022-08-17 15:22:43 -05:00
private lazy var _cancellables : Any = {
guard #available ( iOS 13 , * ) else { fatalError ( ) }
return Set < AnyCancellable > ( )
} ( )
2023-03-01 00:48:36 -05:00
private init ( ) {
operationQueue . name = " com.altstore.AppManager.operationQueue "
serialOperationQueue . name = " com.altstore.AppManager.serialOperationQueue "
serialOperationQueue . maxConcurrentOperationCount = 1
if #available ( iOS 13 , * ) {
2020-09-08 13:12:40 -07:00
self . prepareSubscriptions ( )
}
}
2023-03-01 00:48:36 -05:00
2020-09-08 13:12:40 -07:00
@ available ( iOS 13 , * )
2023-03-01 00:48:36 -05:00
func prepareSubscriptions ( ) {
2020-09-10 11:27:44 -07:00
// / E v e r y t i m e r e f r e s h P r o g r e s s i s c h a n g e d , u p d a t e a l l I n s t a l l e d A p p s i n m e m o r y
// / s o t h a t a p p . i s R e f r e s h i n g = = r e f r e s h P r o g r e s s . k e y s . c o n t a i n s ( a p p . b u n d l e I D )
2023-03-01 00:48:36 -05:00
publisher . $ refreshProgress
2020-09-08 13:12:40 -07:00
. receive ( on : RunLoop . main )
. map ( \ . keys )
2023-03-01 00:48:36 -05:00
. flatMap { bundleIDs in
2020-09-08 13:12:40 -07:00
DatabaseManager . shared . viewContext . registeredObjects . publisher
. compactMap { $0 as ? InstalledApp }
2020-09-10 11:27:44 -07:00
. map { ( $0 , bundleIDs . contains ( $0 . bundleIdentifier ) ) }
2020-09-08 13:12:40 -07:00
}
2023-03-01 00:48:36 -05:00
. sink { installedApp , isRefreshing in
2020-09-10 11:27:44 -07:00
installedApp . isRefreshing = isRefreshing
2020-09-08 13:12:40 -07:00
}
2023-03-01 00:48:36 -05:00
. store ( in : & cancellables )
2019-05-31 18:24:08 -07:00
}
}
2023-03-01 19:09:33 -05:00
public extension AppManager {
2023-03-01 00:48:36 -05:00
func update ( ) {
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
2020-03-23 12:12:49 -07:00
#if targetEnvironment ( simulator )
// A p p s a r e n ' t e v e r a c t u a l l y i n s t a l l e d t o s i m u l a t o r , s o j u s t d o n o t h i n g r a t h e r t h a n d e l e t e t h e m f r o m d a t a b a s e .
#else
2023-03-01 00:48:36 -05:00
do {
let installedApps = InstalledApp . all ( in : context )
if UserDefaults . standard . legacySideloadedApps = = nil {
// F i r s t t i m e u p d a t i n g a p p s s i n c e u p d a t i n g A l t S t o r e t o u s e c u s t o m U T I s ,
// s o c a c h e a l l e x i s t i n g a p p s t e m p o r a r i l y t o p r e v e n t u s f r o m a c c i d e n t a l l y
// d e l e t i n g t h e m d u e t o t h e i r c u s t o m U T I n o t e x i s t i n g ( y e t ) .
let apps = installedApps . map { $0 . bundleIdentifier }
UserDefaults . standard . legacySideloadedApps = apps
2020-05-16 16:17:18 -07:00
}
2023-03-01 00:48:36 -05:00
let legacySideloadedApps = Set ( UserDefaults . standard . legacySideloadedApps ? ? [ ] )
for app in installedApps {
guard app . bundleIdentifier != StoreApp . altstoreAppID else {
self . scheduleExpirationWarningLocalNotification ( for : app )
continue
}
guard ! self . isActivelyManagingApp ( withBundleID : app . bundleIdentifier ) else { continue }
if ! UserDefaults . standard . isLegacyDeactivationSupported {
// W e c a n ' t ( a b ) u s e p r o v i s i o n i n g p r o f i l e s t o d e a c t i v a t e a p p s ,
// w h i c h m e a n s w e m u s t d e l e t e a p p s t o f r e e u p a c t i v e s l o t s .
// S o , o n l y c h e c k i f a c t i v e a p p s a r e i n s t a l l e d t o p r e v e n t
// f a l s e p o s i t i v e s w h e n c h e c k i n g i n a c t i v e a p p s .
guard app . isActive else { continue }
}
let uti = UTTypeCopyDeclaration ( app . installedAppUTI as CFString ) ? . takeRetainedValue ( ) as NSDictionary ?
if uti = = nil , ! legacySideloadedApps . contains ( app . bundleIdentifier ) {
// T h i s U T I i s n o t d e c l a r e d b y a n y a p p s , w h i c h m e a n s t h i s a p p h a s b e e n d e l e t e d b y t h e u s e r .
// T h i s a p p i s a l s o n o t a l e g a c y s i d e l o a d e d a p p , s o w e c a n a s s u m e i t ' s f i n e t o d e l e t e i t .
context . delete ( app )
if var patchedApps = UserDefaults . standard . patchedApps , let index = patchedApps . firstIndex ( of : app . bundleIdentifier ) {
patchedApps . remove ( at : index )
UserDefaults . standard . patchedApps = patchedApps
}
2021-10-25 22:27:30 -07:00
}
2020-03-23 12:12:49 -07:00
}
2023-03-01 00:48:36 -05:00
try context . save ( )
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Error while fetching installed apps. %@ " , type : . error , error . localizedDescription )
2020-03-23 12:12:49 -07:00
}
#endif
2023-03-01 00:48:36 -05:00
do {
2020-03-23 12:12:49 -07:00
let installedAppBundleIDs = InstalledApp . all ( in : context ) . map { $0 . bundleIdentifier }
2023-03-01 00:48:36 -05:00
2020-03-23 12:12:49 -07:00
let cachedAppDirectories = try FileManager . default . contentsOfDirectory ( at : InstalledApp . appsDirectoryURL ,
includingPropertiesForKeys : [ . isDirectoryKey , . nameKey ] ,
options : [ . skipsSubdirectoryDescendants , . skipsHiddenFiles ] )
2023-03-01 00:48:36 -05:00
for appDirectory in cachedAppDirectories {
do {
2020-03-23 12:12:49 -07:00
let resourceValues = try appDirectory . resourceValues ( forKeys : [ . isDirectoryKey , . nameKey ] )
guard let isDirectory = resourceValues . isDirectory , let bundleID = resourceValues . name else { continue }
2023-03-01 00:48:36 -05:00
if isDirectory && ! installedAppBundleIDs . contains ( bundleID ) && ! self . isActivelyManagingApp ( withBundleID : bundleID ) {
2023-03-02 00:40:11 -05:00
os_log ( " DELETING CACHED APP: %@ " , type : . info , bundleID )
2020-03-23 12:12:49 -07:00
try FileManager . default . removeItem ( at : appDirectory )
}
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to remove cached app directory. %@ " , type : . error , error . localizedDescription )
2020-03-23 12:12:49 -07:00
}
2019-06-04 13:53:21 -07:00
}
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to remove cached apps. %@ " , type : . error , error . localizedDescription )
2020-03-23 12:12:49 -07:00
}
2019-06-04 13:53:21 -07:00
}
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
@ discardableResult
2023-03-01 00:48:36 -05:00
func authenticate ( presentingViewController : UIViewController ? , context : AuthenticatedOperationContext = AuthenticatedOperationContext ( ) , completionHandler : @ escaping ( Result < ( ALTTeam , ALTCertificate , ALTAppleAPISession ) , Error > ) -> Void ) -> AuthenticationOperation {
if let operation = context . authenticationOperation {
2020-03-06 17:08:35 -08:00
return operation
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
let authenticationOperation = AuthenticationOperation ( context : context , presentingViewController : presentingViewController )
2023-03-01 00:48:36 -05:00
authenticationOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
2020-03-06 17:08:35 -08:00
case . success : break
2020-02-10 17:30:11 -08:00
}
2023-03-01 00:48:36 -05:00
2019-06-05 18:05:21 -07:00
completionHandler ( result )
}
2023-03-01 00:48:36 -05:00
run ( [ authenticationOperation ] , context : context )
2020-03-06 17:08:35 -08:00
return authenticationOperation
2019-06-05 18:05:21 -07:00
}
2023-03-01 00:48:36 -05:00
func deactivateApps ( for app : ALTApplication , presentingViewController : UIViewController , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
2021-09-15 14:27:16 -07:00
guard let activeAppsLimit = UserDefaults . standard . activeAppsLimit else { return completion ( . success ( ( ) ) ) }
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
DispatchQueue . main . async {
let activeApps = InstalledApp . fetchActiveApps ( in : DatabaseManager . shared . viewContext )
. filter { $0 . bundleIdentifier != app . bundleIdentifier } // D o n ' t c o u n t a p p t o w a r d s t o t a l i f i t m a t c h e s a c t i v a t i n g a p p
. sorted { ( $0 . name , $0 . refreshedDate ) < ( $1 . name , $1 . refreshedDate ) }
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
var title : String = NSLocalizedString ( " Cannot Activate More than 3 Apps " , comment : " " )
let message : String
2023-03-01 00:48:36 -05:00
if UserDefaults . standard . activeAppLimitIncludesExtensions {
if app . appExtensions . isEmpty {
2021-09-15 14:27:16 -07:00
message = NSLocalizedString ( " Non-developer Apple IDs are limited to 3 active apps and app extensions. Please choose an app to deactivate. " , comment : " " )
2023-03-01 00:48:36 -05:00
} else {
2021-09-15 14:27:16 -07:00
title = NSLocalizedString ( " Cannot Activate More than 3 Apps and App Extensions " , comment : " " )
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
let appExtensionText = app . appExtensions . count = = 1 ? NSLocalizedString ( " app extension " , comment : " " ) : NSLocalizedString ( " app extensions " , comment : " " )
message = String ( format : NSLocalizedString ( " Non-developer Apple IDs are limited to 3 active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate. " , comment : " " ) , app . name , NSNumber ( value : app . appExtensions . count ) , appExtensionText )
}
2023-03-01 00:48:36 -05:00
} else {
2021-09-15 14:27:16 -07:00
message = NSLocalizedString ( " Non-developer Apple IDs are limited to 3 active apps. Please choose an app to deactivate. " , comment : " " )
}
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
let activeAppsCount = activeApps . map { $0 . requiredActiveSlots } . reduce ( 0 , + )
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
let availableActiveApps = max ( activeAppsLimit - activeAppsCount , 0 )
let requiredActiveSlots = UserDefaults . standard . activeAppLimitIncludesExtensions ? ( 1 + app . appExtensions . count ) : 1
guard requiredActiveSlots > availableActiveApps else { return completion ( . success ( ( ) ) ) }
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
let alertController = UIAlertController ( title : title , message : message , preferredStyle : . alert )
2023-03-01 00:48:36 -05:00
alertController . addAction ( UIAlertAction ( title : UIAlertAction . cancel . title , style : UIAlertAction . cancel . style ) { _ in
2021-09-15 14:27:16 -07:00
completion ( . failure ( OperationError . cancelled ) )
} )
2023-03-01 00:48:36 -05:00
for activeApp in activeApps where activeApp . bundleIdentifier != StoreApp . altstoreAppID {
alertController . addAction ( UIAlertAction ( title : activeApp . name , style : . default ) { _ in
2021-09-15 14:27:16 -07:00
activeApp . isActive = false
2023-03-01 00:48:36 -05:00
self . deactivate ( activeApp , presentingViewController : presentingViewController ) { result in
switch result {
case let . failure ( error ) :
2021-09-15 14:27:16 -07:00
activeApp . managedObjectContext ? . perform {
activeApp . isActive = true
completion ( . failure ( error ) )
}
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
case . success :
self . deactivateApps ( for : app , presentingViewController : presentingViewController , completion : completion )
}
}
} )
}
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
presentingViewController . present ( alertController , animated : true , completion : nil )
}
}
2019-06-04 13:53:21 -07:00
}
2023-03-01 19:09:33 -05:00
public extension AppManager {
2022-04-14 16:24:11 -07:00
func fetchSource ( sourceURL : URL ,
managedObjectContext : NSManagedObjectContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( ) ,
dependencies : [ Foundation . Operation ] = [ ] ,
2023-03-01 00:48:36 -05:00
completionHandler : @ escaping ( Result < Source , Error > ) -> Void ) {
2022-04-14 16:24:11 -07:00
let fetchSourceOperation = FetchSourceOperation ( sourceURL : sourceURL , managedObjectContext : managedObjectContext )
2023-03-01 00:48:36 -05:00
fetchSourceOperation . resultHandler = { result in
switch result {
case let . failure ( error ) :
2020-03-24 13:27:44 -07:00
completionHandler ( . failure ( error ) )
2023-03-01 00:48:36 -05:00
case let . success ( source ) :
2020-03-24 13:27:44 -07:00
completionHandler ( . success ( source ) )
}
}
2023-03-01 00:48:36 -05:00
for dependency in dependencies {
2022-04-14 16:24:11 -07:00
fetchSourceOperation . addDependency ( dependency )
}
2023-03-01 00:48:36 -05:00
run ( [ fetchSourceOperation ] , context : nil )
2020-03-24 13:27:44 -07:00
}
2023-03-01 00:48:36 -05:00
func fetchSources ( completionHandler : @ escaping ( Result < ( Set < Source > , NSManagedObjectContext ) , FetchSourcesError > ) -> Void ) {
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
2020-03-24 13:27:44 -07:00
let sources = Source . all ( in : context )
2020-08-27 16:23:50 -07:00
guard ! sources . isEmpty else { return completionHandler ( . failure ( . init ( OperationError . noSources ) ) ) }
2023-03-01 00:48:36 -05:00
2020-03-24 13:27:44 -07:00
let dispatchGroup = DispatchGroup ( )
var fetchedSources = Set < Source > ( )
2023-03-01 00:48:36 -05:00
2020-08-27 16:23:50 -07:00
var errors = [ Source : Error ] ( )
2023-03-01 00:48:36 -05:00
2020-03-24 13:27:44 -07:00
let managedObjectContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( )
2023-03-01 00:48:36 -05:00
let operations = sources . map { source -> FetchSourceOperation in
2020-03-24 13:27:44 -07:00
dispatchGroup . enter ( )
2023-03-01 00:48:36 -05:00
2020-03-24 13:27:44 -07:00
let fetchSourceOperation = FetchSourceOperation ( sourceURL : source . sourceURL , managedObjectContext : managedObjectContext )
2023-03-01 00:48:36 -05:00
fetchSourceOperation . resultHandler = { result in
switch result {
case let . success ( source ) : fetchedSources . insert ( source )
case let . failure ( error ) :
2020-08-27 16:23:50 -07:00
let source = managedObjectContext . object ( with : source . objectID ) as ! Source
2020-08-27 16:39:03 -07:00
source . error = ( error as NSError ) . sanitizedForCoreData ( )
2020-08-27 16:23:50 -07:00
errors [ source ] = error
2020-03-24 13:27:44 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-24 13:27:44 -07:00
dispatchGroup . leave ( )
}
2023-03-01 00:48:36 -05:00
2020-03-24 13:27:44 -07:00
return fetchSourceOperation
2019-07-30 17:00:04 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-24 13:27:44 -07:00
dispatchGroup . notify ( queue : . global ( ) ) {
2020-08-27 16:23:50 -07:00
managedObjectContext . perform {
2023-03-01 00:48:36 -05:00
if ! errors . isEmpty {
2020-08-27 16:23:50 -07:00
let sources = Set ( sources . compactMap { managedObjectContext . object ( with : $0 . objectID ) as ? Source } )
completionHandler ( . failure ( . init ( sources : sources , errors : errors , context : managedObjectContext ) ) )
2023-03-01 00:48:36 -05:00
} else {
2020-08-27 16:23:50 -07:00
completionHandler ( . success ( ( fetchedSources , managedObjectContext ) ) )
2020-03-24 13:27:44 -07:00
}
}
2023-03-01 00:48:36 -05:00
2020-03-24 13:27:44 -07:00
NotificationCenter . default . post ( name : AppManager . didFetchSourceNotification , object : self )
2019-06-17 14:49:23 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-30 15:23:20 -07:00
self . run ( operations , context : nil )
2019-06-17 14:49:23 -07:00
}
}
2023-03-01 00:48:36 -05:00
2020-02-10 17:30:11 -08:00
func fetchAppIDs ( completionHandler : @ escaping ( Result < ( [ AppID ] , NSManagedObjectContext ) , Error > ) -> Void )
{
2020-03-06 17:08:35 -08:00
let authenticationOperation = self . authenticate ( presentingViewController : nil ) { ( result ) in
2023-02-25 14:36:37 -08:00
// r e s u l t c o n t a i n s n a m e , e m a i l , a u t h t o k e n , O T P a n d o t h e r p o s s i b l y p e r s o n a l / a c c o u n t s p e c i f i c i n f o . w e d o n ' t w a n t t h i s l o g g e d
2023-03-02 00:40:11 -05:00
// o s _ l o g ( " A u t h e n t i c a t e d f o r f e t c h i n g A p p I D s w i t h r e s u l t : % @ " , t y p e : . i n f o , r e s u l t )
2020-02-10 17:30:11 -08:00
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
let fetchAppIDsOperation = FetchAppIDsOperation ( context : authenticationOperation . context )
fetchAppIDsOperation . resultHandler = completionHandler
fetchAppIDsOperation . addDependency ( authenticationOperation )
2023-03-01 00:48:36 -05:00
run ( [ fetchAppIDsOperation ] , context : authenticationOperation . context )
2020-02-10 17:30:11 -08:00
}
2023-03-01 00:48:36 -05:00
2022-04-14 15:27:57 -07:00
@ discardableResult
2023-03-01 00:48:36 -05:00
func fetchTrustedSources ( completionHandler : @ escaping ( Result < [ FetchTrustedSourcesOperation . TrustedSource ] , Error > ) -> Void ) -> FetchTrustedSourcesOperation {
2022-04-14 15:27:57 -07:00
let fetchTrustedSourcesOperation = FetchTrustedSourcesOperation ( )
fetchTrustedSourcesOperation . resultHandler = completionHandler
2023-03-01 00:48:36 -05:00
run ( [ fetchTrustedSourcesOperation ] , context : nil )
2022-04-14 15:27:57 -07:00
return fetchTrustedSourcesOperation
}
2023-03-01 00:48:36 -05:00
func updatePatronsIfNeeded ( ) {
guard operationQueue . operations . allSatisfy ( { ! ( $0 is UpdatePatronsOperation ) } ) else {
2022-04-14 17:39:43 -07:00
// T h e r e ' s a l r e a d y a n U p d a t e P a t r o n s O p e r a t i o n r u n n i n g .
return
}
2023-03-01 00:48:36 -05:00
updatePatronsResult = nil
2022-04-14 17:39:43 -07:00
let updatePatronsOperation = UpdatePatronsOperation ( )
2023-03-01 00:48:36 -05:00
updatePatronsOperation . resultHandler = { result in
do {
2022-04-14 17:39:43 -07:00
try result . get ( )
self . updatePatronsResult = . success ( ( ) )
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Error updating Friend Zone Patrons: %@ " , type : . error , error . localizedDescription )
2022-04-14 17:39:43 -07:00
self . updatePatronsResult = . failure ( error )
}
2023-03-01 00:48:36 -05:00
2022-04-14 17:39:43 -07:00
NotificationCenter . default . post ( name : AppManager . didUpdatePatronsNotification , object : self )
}
2023-03-01 00:48:36 -05:00
run ( [ updatePatronsOperation ] , context : nil )
2022-04-14 17:39:43 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
@ discardableResult
2023-03-01 00:48:36 -05:00
func install < T : AppProtocol > ( _ app : T , presentingViewController : UIViewController ? , context : AuthenticatedOperationContext = AuthenticatedOperationContext ( ) , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> RefreshGroup {
2020-03-06 17:08:35 -08:00
let group = RefreshGroup ( context : context )
2023-03-01 00:48:36 -05:00
group . completionHandler = { results in
do {
2021-10-25 22:27:30 -07:00
guard let result = results . values . first else { throw context . error ? ? OperationError . unknown }
2019-06-18 18:31:59 -07:00
completionHandler ( result )
2023-03-01 00:48:36 -05:00
} catch {
2019-06-18 18:31:59 -07:00
completionHandler ( . failure ( error ) )
2019-05-31 18:24:08 -07:00
}
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
let operation = AppOperation . install ( app )
2023-03-01 00:48:36 -05:00
perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
2021-10-25 22:27:30 -07:00
return group
2019-05-31 18:24:08 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-31 14:31:34 -07:00
@ discardableResult
2023-03-01 00:48:36 -05:00
func update ( _ app : InstalledApp , presentingViewController : UIViewController ? , context : AuthenticatedOperationContext = AuthenticatedOperationContext ( ) , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress {
2020-03-31 14:31:34 -07:00
guard let storeApp = app . storeApp else {
completionHandler ( . failure ( OperationError . appNotFound ) )
return Progress . discreteProgress ( totalUnitCount : 1 )
}
2023-03-01 00:48:36 -05:00
2020-03-31 14:31:34 -07:00
let group = RefreshGroup ( context : context )
2023-03-01 00:48:36 -05:00
group . completionHandler = { results in
do {
2020-03-31 14:31:34 -07:00
guard let result = results . values . first else { throw OperationError . unknown }
completionHandler ( result )
2023-03-01 00:48:36 -05:00
} catch {
2020-03-31 14:31:34 -07:00
completionHandler ( . failure ( error ) )
}
}
2023-03-01 00:48:36 -05:00
2020-03-31 14:31:34 -07:00
let operation = AppOperation . update ( storeApp )
assert ( operation . app as AnyObject = = = storeApp ) // M a k e s u r e w e n e v e r a c c i d e n t a l l y " u p d a t e " t o a l r e a d y i n s t a l l e d a p p .
2023-03-01 00:48:36 -05:00
perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
2020-03-31 14:31:34 -07:00
return group . progress
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
@ discardableResult
2023-03-01 00:48:36 -05:00
func refresh ( _ installedApps : [ InstalledApp ] , presentingViewController : UIViewController ? , group : RefreshGroup ? = nil ) -> RefreshGroup {
2020-03-06 17:08:35 -08:00
let group = group ? ? RefreshGroup ( )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
let operations = installedApps . map { AppOperation . refresh ( $0 ) }
2023-03-01 00:48:36 -05:00
return perform ( operations , presentingViewController : presentingViewController , group : group )
2019-06-18 18:31:59 -07:00
}
2023-03-01 00:48:36 -05:00
func activate ( _ installedApp : InstalledApp , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) {
2020-05-16 16:29:28 -07:00
let group = RefreshGroup ( )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
let operation = AppOperation . activate ( installedApp )
2023-03-01 00:48:36 -05:00
perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
group . completionHandler = { results in
do {
2020-03-11 14:43:19 -07:00
guard let result = results . values . first else { throw OperationError . unknown }
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
let installedApp = try result . get ( )
2020-05-16 16:29:28 -07:00
assert ( installedApp . managedObjectContext != nil )
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
installedApp . managedObjectContext ? . perform {
installedApp . isActive = true
completionHandler ( . success ( installedApp ) )
}
2023-03-01 00:48:36 -05:00
} catch {
2020-03-11 14:43:19 -07:00
completionHandler ( . failure ( error ) )
}
}
}
2023-03-01 00:48:36 -05:00
func deactivate ( _ installedApp : InstalledApp , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) {
if UserDefaults . standard . isLegacyDeactivationSupported {
2020-05-16 16:17:18 -07:00
// N o r m a l l y w e p i p e e v e r y t h i n g d o w n i n t o p e r f o r m ( ) ,
// b u t t h e p r e - i O S 1 3 . 5 d e a c t i v a t i o n m e t h o d d o e s n ' t r e q u i r e
// a u t h e n t i c a t i o n , s o w e k e e p i t s e p a r a t e .
let context = OperationContext ( )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let deactivateAppOperation = DeactivateAppOperation ( app : installedApp , context : context )
2023-03-01 00:48:36 -05:00
deactivateAppOperation . resultHandler = { result in
2020-05-16 16:17:18 -07:00
completionHandler ( result )
}
2023-03-01 00:48:36 -05:00
run ( [ deactivateAppOperation ] , context : context , requiresSerialQueue : true )
} else {
2020-05-16 16:17:18 -07:00
let group = RefreshGroup ( )
2023-03-01 00:48:36 -05:00
group . completionHandler = { results in
do {
2020-05-16 16:17:18 -07:00
guard let result = results . values . first else { throw OperationError . unknown }
let installedApp = try result . get ( )
assert ( installedApp . managedObjectContext != nil )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
installedApp . managedObjectContext ? . perform {
completionHandler ( . success ( installedApp ) )
}
2023-03-01 00:48:36 -05:00
} catch {
2020-05-16 16:17:18 -07:00
completionHandler ( . failure ( error ) )
}
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let operation = AppOperation . deactivate ( installedApp )
2023-03-01 00:48:36 -05:00
perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
2020-03-11 14:43:19 -07:00
}
}
2023-03-01 00:48:36 -05:00
func backup ( _ installedApp : InstalledApp , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) {
2020-05-19 11:47:43 -07:00
let group = RefreshGroup ( )
2023-03-01 00:48:36 -05:00
group . completionHandler = { results in
do {
2020-05-19 11:47:43 -07:00
guard let result = results . values . first else { throw OperationError . unknown }
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
let installedApp = try result . get ( )
assert ( installedApp . managedObjectContext != nil )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
installedApp . managedObjectContext ? . perform {
completionHandler ( . success ( installedApp ) )
}
2023-03-01 00:48:36 -05:00
} catch {
2020-05-19 11:47:43 -07:00
completionHandler ( . failure ( error ) )
}
}
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
let operation = AppOperation . backup ( installedApp )
2023-03-01 00:48:36 -05:00
perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
2020-05-19 11:47:43 -07:00
}
2023-03-01 00:48:36 -05:00
func restore ( _ installedApp : InstalledApp , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) {
2020-05-16 16:39:02 -07:00
let group = RefreshGroup ( )
2023-03-01 00:48:36 -05:00
group . completionHandler = { results in
do {
2020-05-16 16:39:02 -07:00
guard let result = results . values . first else { throw OperationError . unknown }
2023-03-01 00:48:36 -05:00
2020-05-16 16:39:02 -07:00
let installedApp = try result . get ( )
assert ( installedApp . managedObjectContext != nil )
2023-03-01 00:48:36 -05:00
2020-05-16 16:39:02 -07:00
installedApp . managedObjectContext ? . perform {
installedApp . isActive = true
completionHandler ( . success ( installedApp ) )
}
2023-03-01 00:48:36 -05:00
} catch {
2020-05-16 16:39:02 -07:00
completionHandler ( . failure ( error ) )
}
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:39:02 -07:00
let operation = AppOperation . restore ( installedApp )
2023-03-01 00:48:36 -05:00
perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
2020-05-16 16:39:02 -07:00
}
2023-03-01 00:48:36 -05:00
func remove ( _ installedApp : InstalledApp , completionHandler : @ escaping ( Result < Void , Error > ) -> Void ) {
2020-05-16 15:34:10 -07:00
let authenticationContext = AuthenticatedOperationContext ( )
let appContext = InstallAppOperationContext ( bundleIdentifier : installedApp . bundleIdentifier , authenticatedContext : authenticationContext )
appContext . installedApp = installedApp
2023-03-01 00:48:36 -05:00
let removeAppOperation = RSTAsyncBlockOperation { operation in
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
2020-05-16 15:34:10 -07:00
let installedApp = context . object ( with : installedApp . objectID ) as ! InstalledApp
context . delete ( installedApp )
2023-03-01 00:48:36 -05:00
do { try context . save ( ) } catch { appContext . error = error }
2020-05-16 15:34:10 -07:00
operation . finish ( )
}
}
2023-03-01 00:48:36 -05:00
2020-05-16 15:34:10 -07:00
let removeAppBackupOperation = RemoveAppBackupOperation ( context : appContext )
2023-03-01 00:48:36 -05:00
removeAppBackupOperation . resultHandler = { result in
switch result {
2020-05-16 15:34:10 -07:00
case . success : break
2023-03-02 00:40:11 -05:00
case let . failure ( error ) : os_log ( " Failed to remove app backup. %@ " , type : . error , error . localizedDescription )
2020-05-16 15:34:10 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-16 15:34:10 -07:00
// T h r o w t h e e r r o r f r o m r e m o v e A p p O p e r a t i o n ,
// s i n c e t h a t ' s t h e e r r o r w e r e a l l y c a r e a b o u t .
2023-03-01 00:48:36 -05:00
if let error = appContext . error {
2020-05-16 15:34:10 -07:00
completionHandler ( . failure ( error ) )
2023-03-01 00:48:36 -05:00
} else {
2020-05-16 15:34:10 -07:00
completionHandler ( . success ( ( ) ) )
}
}
removeAppBackupOperation . addDependency ( removeAppOperation )
2023-03-01 00:48:36 -05:00
run ( [ removeAppOperation , removeAppBackupOperation ] , context : authenticationContext )
2021-09-03 13:57:15 -05:00
}
2023-03-01 00:48:36 -05:00
2021-09-03 13:57:15 -05:00
@ available ( iOS 14 , * )
2023-03-01 00:48:36 -05:00
func enableJIT ( for installedApp : InstalledApp , completionHandler : @ escaping ( Result < Void , Error > ) -> Void ) {
final class Context : OperationContext , EnableJITContext {
2021-09-03 13:57:15 -05:00
var installedApp : InstalledApp ?
}
2023-03-01 00:48:36 -05:00
2021-09-03 13:57:15 -05:00
let context = Context ( )
context . installedApp = installedApp
2023-03-01 00:48:36 -05:00
2021-09-03 13:57:15 -05:00
let enableJITOperation = EnableJITOperation ( context : context )
2023-03-01 00:48:36 -05:00
enableJITOperation . resultHandler = { result in
2021-09-03 13:57:15 -05:00
completionHandler ( result )
}
2023-03-01 00:48:36 -05:00
run ( [ enableJITOperation ] , context : context , requiresSerialQueue : true )
2020-05-16 15:34:10 -07:00
}
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
@ available ( iOS 14.0 , * )
2023-03-01 00:48:36 -05:00
func patch ( resignedApp : ALTApplication , presentingViewController _ : UIViewController , context authContext : AuthenticatedOperationContext , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> PatchAppOperation {
final class Context : InstallAppOperationContext , PatchAppContext { }
2021-10-25 22:27:30 -07:00
guard let originalBundleID = resignedApp . bundle . infoDictionary ? [ Bundle . Info . altBundleID ] as ? String else {
let context = Context ( bundleIdentifier : resignedApp . bundleIdentifier , authenticatedContext : authContext )
completionHandler ( . failure ( OperationError . invalidApp ) )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
return PatchAppOperation ( context : context )
}
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let context = Context ( bundleIdentifier : originalBundleID , authenticatedContext : authContext )
context . resignedApp = resignedApp
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let patchAppOperation = PatchAppOperation ( context : context )
let sendAppOperation = SendAppOperation ( context : context )
let installOperation = InstallAppOperation ( context : context )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let installationProgress = Progress . discreteProgress ( totalUnitCount : 100 )
installationProgress . addChild ( sendAppOperation . progress , withPendingUnitCount : 40 )
installationProgress . addChild ( installOperation . progress , withPendingUnitCount : 60 )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
/* P a t c h */
2023-03-01 00:48:36 -05:00
patchAppOperation . resultHandler = { [ weak patchAppOperation ] result in
switch result {
case let . failure ( error ) : context . error = error
2021-10-25 22:27:30 -07:00
case . success :
// K i n d a h a c k y t h a t w e ' r e c a l l i n g p a t c h A p p O p e r a t i o n ' s p r o g r e s s H a n d l e r m a n u a l l y , b u t Y O L O .
patchAppOperation ? . progressHandler ? ( installationProgress , NSLocalizedString ( " Patching placeholder app... " , comment : " " ) )
}
}
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
/* S e n d */
2023-03-01 00:48:36 -05:00
sendAppOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
case . success : print ( " App sent over AFC " )
2021-10-25 22:27:30 -07:00
}
}
sendAppOperation . addDependency ( patchAppOperation )
/* I n s t a l l */
2023-03-01 00:48:36 -05:00
installOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : completionHandler ( . failure ( error ) )
case let . success ( installedApp ) : completionHandler ( . success ( installedApp ) )
2021-10-25 22:27:30 -07:00
}
}
installOperation . addDependency ( sendAppOperation )
2023-03-01 00:48:36 -05:00
run ( [ patchAppOperation , sendAppOperation , installOperation ] , context : context . authenticatedContext )
2021-10-25 22:27:30 -07:00
return patchAppOperation
}
2023-03-01 00:48:36 -05:00
func installationProgress ( for app : AppProtocol ) -> Progress ? {
let progress = installationProgress [ app . bundleIdentifier ]
2019-07-19 16:10:30 -07:00
return progress
}
2023-03-01 00:48:36 -05:00
func refreshProgress ( for app : AppProtocol ) -> Progress ? {
let progress = refreshProgress [ app . bundleIdentifier ]
2019-07-16 14:22:45 -07:00
return progress
}
2019-06-18 18:31:59 -07:00
}
2023-03-01 19:09:33 -05:00
public extension AppManager {
2021-10-04 16:27:00 -07:00
@ discardableResult
2023-03-02 00:40:11 -05:00
func backgroundRefresh ( _ installedApps : [ InstalledApp ] , presentsNotifications : Bool = false , completionHandler : @ escaping ( Result < [ String : Result < InstalledApp , Error > ] , Error > ) -> Void ) -> BackgroundRefreshAppsOperation {
2020-09-08 12:29:44 -07:00
let backgroundRefreshAppsOperation = BackgroundRefreshAppsOperation ( installedApps : installedApps )
backgroundRefreshAppsOperation . resultHandler = completionHandler
backgroundRefreshAppsOperation . presentsFinishedNotification = presentsNotifications
2023-03-01 00:48:36 -05:00
run ( [ backgroundRefreshAppsOperation ] , context : nil )
2021-10-04 16:27:00 -07:00
return backgroundRefreshAppsOperation
2020-09-08 12:29:44 -07:00
}
}
2023-03-01 00:48:36 -05:00
private extension AppManager {
enum AppOperation {
2020-03-06 17:08:35 -08:00
case install ( AppProtocol )
2020-03-31 14:31:34 -07:00
case update ( AppProtocol )
2020-05-16 16:17:18 -07:00
case refresh ( InstalledApp )
2020-05-16 16:29:28 -07:00
case activate ( InstalledApp )
2020-05-16 16:17:18 -07:00
case deactivate ( InstalledApp )
2020-05-19 11:47:43 -07:00
case backup ( InstalledApp )
2020-05-16 16:39:02 -07:00
case restore ( InstalledApp )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
var app : AppProtocol {
2023-03-01 00:48:36 -05:00
switch self {
case let . install ( app ) , let . update ( app ) , let . refresh ( app as AppProtocol ) ,
2020-05-19 11:47:43 -07:00
. activate ( let app as AppProtocol ) , . deactivate ( let app as AppProtocol ) ,
. backup ( let app as AppProtocol ) , . restore ( let app as AppProtocol ) :
2020-05-16 16:17:18 -07:00
return app
2020-03-06 17:08:35 -08:00
}
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
var bundleIdentifier : String {
var bundleIdentifier : String !
2023-03-01 00:48:36 -05:00
if let context = ( app as ? NSManagedObject ) ? . managedObjectContext {
2020-03-06 17:08:35 -08:00
context . performAndWait { bundleIdentifier = self . app . bundleIdentifier }
2023-03-01 00:48:36 -05:00
} else {
bundleIdentifier = app . bundleIdentifier
2019-09-10 12:19:46 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
return bundleIdentifier
2019-09-10 12:19:46 -07:00
}
2020-03-06 17:08:35 -08:00
}
2023-03-01 00:48:36 -05:00
func isActivelyManagingApp ( withBundleID bundleID : String ) -> Bool {
let isActivelyManaging = installationProgress . keys . contains ( bundleID ) || refreshProgress . keys . contains ( bundleID )
2020-05-14 11:02:40 -07:00
return isActivelyManaging
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
@ discardableResult
2023-03-01 00:48:36 -05:00
private func perform ( _ operations : [ AppOperation ] , presentingViewController : UIViewController ? , group : RefreshGroup ) -> RefreshGroup {
2020-03-06 17:08:35 -08:00
let operations = operations . filter { self . progress ( for : $0 ) = = nil || self . progress ( for : $0 ) ? . isCancelled = = true }
2023-03-01 00:48:36 -05:00
for operation in operations {
2020-03-06 17:08:35 -08:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
set ( progress , for : operation )
2020-03-06 17:08:35 -08:00
}
2023-03-01 00:48:36 -05:00
if let viewController = presentingViewController {
2020-05-02 22:06:57 -07:00
group . context . presentingViewController = viewController
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* A u t h e n t i c a t e ( i f n e c e s s a r y ) */
var authenticationOperation : AuthenticationOperation ?
2023-03-01 00:48:36 -05:00
if group . context . session = = nil {
authenticationOperation = authenticate ( presentingViewController : presentingViewController , context : group . context ) { result in
switch result {
case let . failure ( error ) : group . context . error = error
2020-03-06 17:08:35 -08:00
case . success : break
2019-10-28 13:16:55 -07:00
}
}
2020-03-06 17:08:35 -08:00
}
2023-03-01 00:48:36 -05:00
func performAppOperations ( ) {
for operation in operations {
2020-03-06 17:08:35 -08:00
let progress = self . progress ( for : operation )
2023-03-01 00:48:36 -05:00
if let progress = progress {
2020-03-06 17:08:35 -08:00
group . progress . totalUnitCount += 1
group . progress . addChild ( progress , withPendingUnitCount : 1 )
2023-03-01 00:48:36 -05:00
if group . context . session != nil {
2020-03-06 17:08:35 -08:00
// F i n i s h e d a u t h e n t i c a t i n g , s o i n c r e a s e c o m p l e t e d u n i t c o u n t .
progress . completedUnitCount += 20
}
}
2023-03-01 00:48:36 -05:00
switch operation {
case let . install ( app ) , let . update ( app ) :
let installProgress = _install ( app , operation : operation , group : group ) { result in
2020-03-06 17:08:35 -08:00
self . finish ( operation , result : result , group : group , progress : progress )
}
2020-05-16 16:17:18 -07:00
progress ? . addChild ( installProgress , withPendingUnitCount : 80 )
2023-03-01 00:48:36 -05:00
case let . activate ( app ) where UserDefaults . standard . isLegacyDeactivationSupported : fallthrough
case let . refresh ( app ) :
2020-05-16 16:17:18 -07:00
// C h e c k i f b a c k u p a p p i s i n s t a l l e d i n p l a c e o f r e a l a p p .
let uti = UTTypeCopyDeclaration ( app . installedBackupAppUTI as CFString ) ? . takeRetainedValue ( ) as NSDictionary ?
2021-02-26 21:08:10 -06:00
2021-03-01 12:41:33 -06:00
if app . certificateSerialNumber != group . context . certificate ? . serialNumber ||
uti != nil ||
2023-03-01 00:48:36 -05:00
app . needsResign {
2020-10-01 11:51:39 -07:00
// R e s i g n a p p i n s t e a d o f j u s t r e f r e s h i n g p r o f i l e s b e c a u s e e i t h e r :
// * R e f r e s h i n g u s i n g d i f f e r e n t c e r t i f i c a t e
// * B a c k u p a p p i s s t i l l i n s t a l l e d
// * A p p e x p l i c i t l y n e e d s r e s i g n i n g
2021-03-01 12:41:33 -06:00
// * D e v i c e i s j a i l b r o k e n a n d u s i n g A l t D a e m o n o n i O S 1 4 . 0 o r l a t e r ( b / c r e f r e s h i n g w i t h p r o v i s i o n i n g p r o f i l e s i s b r o k e n )
2023-03-01 00:48:36 -05:00
let installProgress = _install ( app , operation : operation , group : group ) { result in
2020-05-16 16:17:18 -07:00
self . finish ( operation , result : result , group : group , progress : progress )
}
2020-10-01 11:51:39 -07:00
progress ? . addChild ( installProgress , withPendingUnitCount : 80 )
2023-03-01 00:48:36 -05:00
} else {
2020-10-01 11:51:39 -07:00
// R e f r e s h i n g w i t h s a m e c e r t i f i c a t e a s l a s t t i m e , a n d b a c k u p a p p i s n ' t s t i l l i n s t a l l e d ,
// s o w e c a n j u s t r e f r e s h p r o v i s i o n i n g p r o f i l e s .
2023-03-01 00:48:36 -05:00
let refreshProgress = _refresh ( app , operation : operation , group : group ) { result in
2020-05-16 16:17:18 -07:00
self . finish ( operation , result : result , group : group , progress : progress )
}
2020-10-01 11:51:39 -07:00
progress ? . addChild ( refreshProgress , withPendingUnitCount : 80 )
2020-05-16 16:17:18 -07:00
}
2023-03-01 00:48:36 -05:00
case let . activate ( app ) :
let activateProgress = _activate ( app , operation : operation , group : group ) { result in
2020-05-16 16:29:28 -07:00
self . finish ( operation , result : result , group : group , progress : progress )
}
progress ? . addChild ( activateProgress , withPendingUnitCount : 80 )
2023-03-01 00:48:36 -05:00
case let . deactivate ( app ) :
let deactivateProgress = _deactivate ( app , operation : operation , group : group ) { result in
2020-03-06 17:08:35 -08:00
self . finish ( operation , result : result , group : group , progress : progress )
}
2020-05-16 16:17:18 -07:00
progress ? . addChild ( deactivateProgress , withPendingUnitCount : 80 )
2023-03-01 00:48:36 -05:00
case let . backup ( app ) :
let backupProgress = _backup ( app , operation : operation , group : group ) { result in
2020-05-19 11:47:43 -07:00
self . finish ( operation , result : result , group : group , progress : progress )
}
progress ? . addChild ( backupProgress , withPendingUnitCount : 80 )
2023-03-01 00:48:36 -05:00
case let . restore ( app ) :
2020-05-16 16:39:02 -07:00
// R e s t o r i n g , w h i c h i s e f f e c t i v e l y j u s t a c t i v a t i n g a n a p p .
2023-03-01 00:48:36 -05:00
let activateProgress = _activate ( app , operation : operation , group : group ) { result in
2020-05-16 16:39:02 -07:00
self . finish ( operation , result : result , group : group , progress : progress )
}
progress ? . addChild ( activateProgress , withPendingUnitCount : 80 )
2020-03-06 17:08:35 -08:00
}
}
}
2023-03-01 00:48:36 -05:00
if let authenticationOperation = authenticationOperation {
2020-03-06 17:08:35 -08:00
let awaitAuthenticationOperation = BlockOperation {
2023-03-01 00:48:36 -05:00
if let managedObjectContext = operations . lazy . compactMap ( { ( $0 . app as ? NSManagedObject ) ? . managedObjectContext } ) . first {
2020-03-06 17:08:35 -08:00
managedObjectContext . perform { performAppOperations ( ) }
2023-03-01 00:48:36 -05:00
} else {
2020-03-06 17:08:35 -08:00
performAppOperations ( )
}
}
awaitAuthenticationOperation . addDependency ( authenticationOperation )
2023-03-01 00:48:36 -05:00
run ( [ awaitAuthenticationOperation ] , context : group . context , requiresSerialQueue : true )
} else {
2020-03-06 17:08:35 -08:00
performAppOperations ( )
2019-11-18 14:49:17 -08:00
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
return group
}
2023-03-01 00:48:36 -05:00
private func _install ( _ app : AppProtocol , operation appOperation : AppOperation , group : RefreshGroup , context : InstallAppOperationContext ? = nil , additionalEntitlements : [ ALTEntitlement : Any ] ? = nil , cacheApp : Bool = true , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress {
2020-03-06 17:08:35 -08:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let context = context ? ? InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
assert ( context . authenticatedContext = = = group . context )
2023-03-01 00:48:36 -05:00
context . beginInstallationHandler = { installedApp in
switch appOperation {
2020-03-31 14:31:34 -07:00
case . update where installedApp . bundleIdentifier = = StoreApp . altstoreAppID :
// A l t S t o r e w i l l q u i t b e f o r e i n s t a l l a t i o n f i n i s h e s ,
// s o a s s u m e i f w e g e t t h i s f a r t h e u p d a t e w i l l f i n i s h s u c c e s s f u l l y .
let event = AnalyticsManager . Event . updatedApp ( installedApp )
AnalyticsManager . shared . trackEvent ( event )
2023-03-01 00:48:36 -05:00
2020-03-31 14:31:34 -07:00
default : break
}
2023-03-01 00:48:36 -05:00
2020-03-31 14:31:34 -07:00
group . beginInstallationHandler ? ( installedApp )
}
2023-03-01 00:48:36 -05:00
2020-05-08 11:43:34 -07:00
var downloadingApp = app
2023-03-01 00:48:36 -05:00
if let installedApp = app as ? InstalledApp {
if let storeApp = installedApp . storeApp , ! FileManager . default . fileExists ( atPath : installedApp . fileURL . path ) {
2020-10-01 14:09:45 -07:00
// C a c h e d a p p h a s b e e n d e l e t e d , s o w e n e e d t o r e d o w n l o a d i t .
downloadingApp = storeApp
}
2023-03-01 00:48:36 -05:00
if installedApp . hasAlternateIcon {
2020-10-01 14:09:45 -07:00
context . alternateIconURL = installedApp . alternateIconURL
}
2020-05-08 11:43:34 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-15 14:55:15 -07:00
let downloadedAppURL = context . temporaryDirectory . appendingPathComponent ( " Cached.app " )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* D o w n l o a d */
2020-05-15 14:55:15 -07:00
let downloadOperation = DownloadAppOperation ( app : downloadingApp , destinationURL : downloadedAppURL , context : context )
2023-03-01 00:48:36 -05:00
downloadOperation . resultHandler = { result in
do {
2020-05-15 14:55:15 -07:00
let app = try result . get ( )
context . app = app
2023-03-01 00:48:36 -05:00
if cacheApp {
2020-05-15 14:55:15 -07:00
try FileManager . default . copyItem ( at : app . fileURL , to : InstalledApp . fileURL ( for : app ) , shouldReplace : true )
}
2023-03-01 00:48:36 -05:00
} catch {
2020-05-15 14:55:15 -07:00
context . error = error
2020-01-08 12:41:02 -08:00
}
}
2020-03-06 17:08:35 -08:00
progress . addChild ( downloadOperation . progress , withPendingUnitCount : 25 )
2023-03-01 00:48:36 -05:00
2020-05-02 22:06:57 -07:00
/* V e r i f y A p p */
let verifyOperation = VerifyAppOperation ( context : context )
2023-03-01 00:48:36 -05:00
verifyOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
2020-05-02 22:06:57 -07:00
case . success : break
}
}
verifyOperation . addDependency ( downloadOperation )
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
/* D e a c t i v a t e A p p s ( i f n e c e s s a r y ) */
2023-03-01 00:48:36 -05:00
let deactivateAppsOperation = RSTAsyncBlockOperation { [ weak self ] operation in
do {
2021-09-15 14:27:16 -07:00
// O n l y a t t e m p t t o d e a c t i v a t e a p p s i f w e ' r e i n s t a l l i n g a n e w a p p .
// W e h a n d l e d e a c t i v a t i n g a p p s s e p a r a t e l y w h e n a c t i v a t i n g a n a p p .
guard case . install = appOperation else {
operation . finish ( )
return
}
2023-03-01 00:48:36 -05:00
if let error = context . error {
2021-09-15 14:27:16 -07:00
throw error
}
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
guard let app = context . app , let presentingViewController = context . authenticatedContext . presentingViewController else { throw OperationError . invalidParameters }
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
self ? . deactivateApps ( for : app , presentingViewController : presentingViewController ) { result in
2023-03-01 00:48:36 -05:00
switch result {
case let . failure ( error ) : group . context . error = error
2021-09-15 14:27:16 -07:00
case . success : break
}
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
operation . finish ( )
}
2023-03-01 00:48:36 -05:00
} catch {
2021-09-15 14:27:16 -07:00
group . context . error = error
operation . finish ( )
}
}
deactivateAppsOperation . addDependency ( verifyOperation )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
/* P a t c h A p p */
let patchAppOperation = RSTAsyncBlockOperation { operation in
2023-03-01 00:48:36 -05:00
do {
2021-10-25 22:27:30 -07:00
// O n l y a t t e m p t t o p a t c h a p p i f w e ' r e i n s t a l l i n g a n e w a p p , n o t r e f r e s h i n g e x i s t i n g a p p .
// P o s t r e b o o t , w e i n s t a l l t h e c o r r e c t j a i l b r e a k a p p b y r e f r e s h i n g t h e p a t c h e d a p p ,
// s o t h i s c h e c k a v o i d s i n f i n i t e r e c u r s i o n .
guard case . install = appOperation else {
operation . finish ( )
return
}
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
guard let presentingViewController = context . presentingViewController , #available ( iOS 14 , * ) else { return operation . finish ( ) }
2023-03-01 00:48:36 -05:00
if let error = context . error {
2021-10-25 22:27:30 -07:00
throw error
}
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
guard let app = context . app else { throw OperationError . invalidParameters }
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
guard let isUntetherRequired = app . bundle . infoDictionary ? [ Bundle . Info . untetherRequired ] as ? Bool ,
let minimumiOSVersionString = app . bundle . infoDictionary ? [ Bundle . Info . untetherMinimumiOSVersion ] as ? String ,
let maximumiOSVersionString = app . bundle . infoDictionary ? [ Bundle . Info . untetherMaximumiOSVersion ] as ? String ,
case let minimumiOSVersion = OperatingSystemVersion ( string : minimumiOSVersionString ) ,
case let maximumiOSVersion = OperatingSystemVersion ( string : maximumiOSVersionString )
else { return operation . finish ( ) }
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let iOSVersion = ProcessInfo . processInfo . operatingSystemVersion
let iOSVersionSupported = ProcessInfo . processInfo . isOperatingSystemAtLeast ( minimumiOSVersion ) &&
2023-03-01 00:48:36 -05:00
( ! ProcessInfo . processInfo . isOperatingSystemAtLeast ( maximumiOSVersion ) || maximumiOSVersion = = iOSVersion )
2021-10-25 22:27:30 -07:00
guard isUntetherRequired , iOSVersionSupported , UIDevice . current . supportsFugu14 else { return operation . finish ( ) }
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
guard let patchAppLink = app . bundle . infoDictionary ? [ Bundle . Info . untetherURL ] as ? String ,
let patchAppURL = URL ( string : patchAppLink )
else { throw OperationError . invalidApp }
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let patchApp = AnyApp ( name : app . name , bundleIdentifier : app . bundleIdentifier , url : patchAppURL )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
DispatchQueue . main . async {
2023-03-02 01:09:10 -05:00
let storyboard = UIStoryboard ( name : " PatchApp " , bundle : Bundle . init ( for : PatchViewController . self ) )
2021-10-25 22:27:30 -07:00
let navigationController = storyboard . instantiateInitialViewController ( ) as ! UINavigationController
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let patchViewController = navigationController . topViewController as ! PatchViewController
patchViewController . patchApp = patchApp
2023-03-01 00:48:36 -05:00
patchViewController . completionHandler = { [ weak presentingViewController ] result in
switch result {
2021-10-25 22:27:30 -07:00
case . failure ( OperationError . cancelled ) : break // I g n o r e
2023-03-01 00:48:36 -05:00
case let . failure ( error ) : group . context . error = error
2021-10-25 22:27:30 -07:00
case . success : group . context . error = OperationError . cancelled
}
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
operation . finish ( )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
DispatchQueue . main . async {
presentingViewController ? . dismiss ( animated : true , completion : nil )
}
}
2023-03-01 00:48:36 -05:00
presentingViewController . present ( navigationController , animated : true , completion : nil )
2021-10-25 22:27:30 -07:00
}
2023-03-01 00:48:36 -05:00
} catch {
2021-10-25 22:27:30 -07:00
group . context . error = error
operation . finish ( )
}
}
patchAppOperation . addDependency ( deactivateAppsOperation )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* R e f r e s h A n i s e t t e D a t a */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation ( context : group . context )
2023-03-01 00:48:36 -05:00
refreshAnisetteDataOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
case let . success ( anisetteData ) : group . context . session ? . anisetteData = anisetteData
2020-01-21 15:12:48 -08:00
}
}
2021-10-25 22:27:30 -07:00
refreshAnisetteDataOperation . addDependency ( patchAppOperation )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* F e t c h P r o v i s i o n i n g P r o f i l e s */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation ( context : context )
2020-05-15 14:54:43 -07:00
fetchProvisioningProfilesOperation . additionalEntitlements = additionalEntitlements
2023-03-01 00:48:36 -05:00
fetchProvisioningProfilesOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
case let . success ( provisioningProfiles ) : context . provisioningProfiles = provisioningProfiles
2019-06-21 11:20:03 -07:00
}
2020-03-06 17:08:35 -08:00
}
fetchProvisioningProfilesOperation . addDependency ( refreshAnisetteDataOperation )
progress . addChild ( fetchProvisioningProfilesOperation . progress , withPendingUnitCount : 5 )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* R e s i g n */
let resignAppOperation = ResignAppOperation ( context : context )
2023-03-01 00:48:36 -05:00
resignAppOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
case let . success ( resignedApp ) : context . resignedApp = resignedApp
2019-06-21 11:20:03 -07:00
}
2020-03-06 17:08:35 -08:00
}
resignAppOperation . addDependency ( fetchProvisioningProfilesOperation )
progress . addChild ( resignAppOperation . progress , withPendingUnitCount : 20 )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* S e n d */
let sendAppOperation = SendAppOperation ( context : context )
2023-03-01 00:48:36 -05:00
sendAppOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
case . success : print ( " App reported as installed " )
2019-09-21 22:31:10 -07:00
}
2020-03-06 17:08:35 -08:00
}
sendAppOperation . addDependency ( resignAppOperation )
progress . addChild ( sendAppOperation . progress , withPendingUnitCount : 20 )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* I n s t a l l */
let installOperation = InstallAppOperation ( context : context )
2023-03-01 00:48:36 -05:00
installOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : completionHandler ( . failure ( error ) )
case let . success ( installedApp ) :
2020-05-16 16:17:18 -07:00
context . installedApp = installedApp
2023-03-01 00:48:36 -05:00
if let app = app as ? StoreApp , let storeApp = installedApp . managedObjectContext ? . object ( with : app . objectID ) as ? StoreApp {
2020-03-06 17:08:35 -08:00
installedApp . storeApp = storeApp
2019-07-19 16:09:45 -07:00
}
2023-03-01 00:48:36 -05:00
if let index = UserDefaults . standard . legacySideloadedApps ? . firstIndex ( of : installedApp . bundleIdentifier ) {
2020-03-06 17:08:35 -08:00
// N o l o n g e r a l e g a c y s i d e l o a d e d a p p , s o r e m o v e i t f r o m c a c h e d l i s t .
UserDefaults . standard . legacySideloadedApps ? . remove ( at : index )
2019-07-28 15:08:13 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
completionHandler ( . success ( installedApp ) )
2020-02-10 17:30:11 -08:00
}
}
2020-03-06 17:08:35 -08:00
progress . addChild ( installOperation . progress , withPendingUnitCount : 30 )
installOperation . addDependency ( sendAppOperation )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let operations = [ downloadOperation , verifyOperation , deactivateAppsOperation , patchAppOperation , refreshAnisetteDataOperation , fetchProvisioningProfilesOperation , resignAppOperation , sendAppOperation , installOperation ]
2020-03-06 17:08:35 -08:00
group . add ( operations )
2023-03-01 00:48:36 -05:00
run ( operations , context : group . context )
2020-03-06 17:08:35 -08:00
return progress
2019-06-04 18:29:50 -07:00
}
2023-03-01 00:48:36 -05:00
private func _refresh ( _ app : InstalledApp , operation : AppOperation , group : RefreshGroup , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress {
2020-03-06 17:08:35 -08:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
let context = AppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
2022-09-08 15:59:24 -05:00
context . app = ALTApplication ( fileURL : app . fileURL )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* F e t c h P r o v i s i o n i n g P r o f i l e s */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation ( context : context )
2023-03-01 00:48:36 -05:00
fetchProvisioningProfilesOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
case let . success ( provisioningProfiles ) : context . provisioningProfiles = provisioningProfiles
2020-03-06 17:08:35 -08:00
}
2019-07-16 14:22:45 -07:00
}
2020-03-06 17:08:35 -08:00
progress . addChild ( fetchProvisioningProfilesOperation . progress , withPendingUnitCount : 60 )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
/* R e f r e s h */
let refreshAppOperation = RefreshAppOperation ( context : context )
2023-03-01 00:48:36 -05:00
refreshAppOperation . resultHandler = { result in
switch result {
case let . success ( installedApp ) :
2020-03-11 14:43:19 -07:00
completionHandler ( . success ( installedApp ) )
2023-03-01 00:48:36 -05:00
2020-05-08 11:43:34 -07:00
case . failure ( ALTServerError . unknownRequest ) , . failure ( OperationError . appNotFound ) :
// F a l l b a c k t o i n s t a l l a t i o n i f A l t S e r v e r d o e s n ' t s u p p o r t n e w e r p r o v i s i o n i n g p r o f i l e r e q u e s t s ,
// O R i f t h e c a c h e d a p p c o u l d n o t b e f o u n d a n d w e m a y n e e d t o r e d o w n l o a d i t .
2020-03-19 11:53:53 -07:00
app . managedObjectContext ? . performAndWait { // M u s t p e r f o r m A n d W a i t t o e n s u r e w e a d d o p e r a t i o n s b e f o r e w e r e t u r n .
2023-03-01 00:48:36 -05:00
let installProgress = self . _install ( app , operation : operation , group : group ) { result in
2020-03-11 14:43:19 -07:00
completionHandler ( result )
}
progress . addChild ( installProgress , withPendingUnitCount : 40 )
}
2023-03-01 00:48:36 -05:00
case let . failure ( error ) :
2020-03-11 14:43:19 -07:00
completionHandler ( . failure ( error ) )
}
2019-06-21 11:20:03 -07:00
}
2020-03-06 17:08:35 -08:00
progress . addChild ( refreshAppOperation . progress , withPendingUnitCount : 40 )
refreshAppOperation . addDependency ( fetchProvisioningProfilesOperation )
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
let operations = [ fetchProvisioningProfilesOperation , refreshAppOperation ]
group . add ( operations )
2023-03-01 00:48:36 -05:00
run ( operations , context : group . context )
2020-05-16 16:17:18 -07:00
return progress
}
2023-03-01 00:48:36 -05:00
private func _activate ( _ app : InstalledApp , operation appOperation : AppOperation , group : RefreshGroup , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress {
2020-05-16 16:29:28 -07:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
let restoreContext = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
let appContext = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
let installBackupAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
let installBackupAppOperation = RSTAsyncBlockOperation { [ weak self ] operation in
2020-05-16 16:29:28 -07:00
app . managedObjectContext ? . perform {
guard let self = self else { return }
2023-03-01 00:48:36 -05:00
let progress = self . _installBackupApp ( for : app , operation : appOperation , group : group , context : restoreContext ) { result in
switch result {
case let . success ( installedApp ) : restoreContext . installedApp = installedApp
case let . failure ( error ) :
2020-05-16 16:29:28 -07:00
restoreContext . error = error
appContext . error = error
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
operation . finish ( )
}
installBackupAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
progress . addChild ( installBackupAppProgress , withPendingUnitCount : 30 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
let restoreAppOperation = BackupAppOperation ( action : . restore , context : restoreContext )
2023-03-01 00:48:36 -05:00
restoreAppOperation . resultHandler = { result in
switch result {
2020-05-16 16:29:28 -07:00
case . success : break
2023-03-01 00:48:36 -05:00
case let . failure ( error ) :
2020-05-16 16:29:28 -07:00
restoreContext . error = error
appContext . error = error
}
}
restoreAppOperation . addDependency ( installBackupAppOperation )
progress . addChild ( restoreAppOperation . progress , withPendingUnitCount : 15 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
let installAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
let installAppOperation = RSTAsyncBlockOperation { [ weak self ] operation in
2020-05-16 16:29:28 -07:00
app . managedObjectContext ? . perform {
guard let self = self else { return }
2023-03-01 00:48:36 -05:00
let progress = self . _install ( app , operation : appOperation , group : group , context : appContext ) { result in
switch result {
case let . success ( installedApp ) : appContext . installedApp = installedApp
case let . failure ( error ) : appContext . error = error
2020-05-16 16:29:28 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
operation . finish ( )
}
installAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
installAppOperation . addDependency ( restoreAppOperation )
progress . addChild ( installAppProgress , withPendingUnitCount : 50 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
let cleanUpProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
let cleanUpOperation = RSTAsyncBlockOperation { operation in
do {
2020-05-16 16:29:28 -07:00
let installedApp = try Result ( appContext . installedApp , appContext . error ) . get ( )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
var result : Result < Void , Error > !
installedApp . managedObjectContext ? . performAndWait {
result = Result { try installedApp . managedObjectContext ? . save ( ) }
}
try result . get ( )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
// S u c c e s s f u l l y s a v e d , s o _ n o w _ w e c a n r e m o v e b a c k u p .
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
let removeAppBackupOperation = RemoveAppBackupOperation ( context : appContext )
2023-03-01 00:48:36 -05:00
removeAppBackupOperation . resultHandler = { result in
2020-05-16 16:29:28 -07:00
installedApp . managedObjectContext ? . perform {
2023-03-01 00:48:36 -05:00
switch result {
case let . failure ( error ) :
2020-05-16 16:29:28 -07:00
// D o n ' t r e p o r t e r r o r , s i n c e i t d o e s n ' t r e a l l y m a t t e r .
2023-03-02 00:40:11 -05:00
os_log ( " Failed to delete app backup. %@ " , type : . error , error . localizedDescription )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
case . success : break
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
completionHandler ( . success ( installedApp ) )
operation . finish ( )
}
}
cleanUpProgress . addChild ( removeAppBackupOperation . progress , withPendingUnitCount : 100 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
group . add ( [ removeAppBackupOperation ] )
self . run ( [ removeAppBackupOperation ] , context : group . context )
2023-03-01 00:48:36 -05:00
} catch let error where restoreContext . installedApp != nil {
2020-05-16 16:29:28 -07:00
// A c t i v a t i o n f a i l e d , b u t r e s t o r e a p p w a s i n s t a l l e d , s o r e m o v e t h e a p p .
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
// R e m o v e e r r o r s o o p e r a t i o n d o e s n ' t q u i t e a r l y ,
restoreContext . error = nil
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
let removeAppOperation = RemoveAppOperation ( context : restoreContext )
2023-03-01 00:48:36 -05:00
removeAppOperation . resultHandler = { _ in
2020-05-16 16:29:28 -07:00
completionHandler ( . failure ( error ) )
operation . finish ( )
}
cleanUpProgress . addChild ( removeAppOperation . progress , withPendingUnitCount : 100 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
group . add ( [ removeAppOperation ] )
self . run ( [ removeAppOperation ] , context : group . context )
2023-03-01 00:48:36 -05:00
} catch {
2020-05-16 16:29:28 -07:00
// A c t i v a t i o n f a i l e d .
completionHandler ( . failure ( error ) )
operation . finish ( )
}
}
cleanUpOperation . addDependency ( installAppOperation )
progress . addChild ( cleanUpProgress , withPendingUnitCount : 5 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:29:28 -07:00
group . add ( [ installBackupAppOperation , restoreAppOperation , installAppOperation , cleanUpOperation ] )
2023-03-01 00:48:36 -05:00
run ( [ installBackupAppOperation , installAppOperation , restoreAppOperation , cleanUpOperation ] , context : group . context )
2020-05-16 16:29:28 -07:00
return progress
}
2023-03-01 00:48:36 -05:00
private func _deactivate ( _ app : InstalledApp , operation appOperation : AppOperation , group : RefreshGroup , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress {
2020-05-16 16:17:18 -07:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
let context = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let installBackupAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
let installBackupAppOperation = RSTAsyncBlockOperation { [ weak self ] operation in
2020-05-16 16:17:18 -07:00
app . managedObjectContext ? . perform {
guard let self = self else { return }
2023-03-01 00:48:36 -05:00
let progress = self . _installBackupApp ( for : app , operation : appOperation , group : group , context : context ) { result in
switch result {
case let . success ( installedApp ) : context . installedApp = installedApp
case let . failure ( error ) : context . error = error
2020-05-16 16:17:18 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
operation . finish ( )
}
installBackupAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
progress . addChild ( installBackupAppProgress , withPendingUnitCount : 70 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let backupAppOperation = BackupAppOperation ( action : . backup , context : context )
2023-03-01 00:48:36 -05:00
backupAppOperation . resultHandler = { result in
switch result {
case let . failure ( error ) : context . error = error
2020-05-16 16:17:18 -07:00
case . success : break
}
}
backupAppOperation . addDependency ( installBackupAppOperation )
progress . addChild ( backupAppOperation . progress , withPendingUnitCount : 15 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let removeAppOperation = RemoveAppOperation ( context : context )
2023-03-01 00:48:36 -05:00
removeAppOperation . resultHandler = { result in
2020-05-16 16:17:18 -07:00
completionHandler ( result )
}
removeAppOperation . addDependency ( backupAppOperation )
progress . addChild ( removeAppOperation . progress , withPendingUnitCount : 15 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
group . add ( [ installBackupAppOperation , backupAppOperation , removeAppOperation ] )
2023-03-01 00:48:36 -05:00
run ( [ installBackupAppOperation , backupAppOperation , removeAppOperation ] , context : group . context )
2020-05-16 16:17:18 -07:00
return progress
}
2023-03-01 00:48:36 -05:00
private func _backup ( _ app : InstalledApp , operation appOperation : AppOperation , group : RefreshGroup , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress {
2020-05-19 11:47:43 -07:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
let restoreContext = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
let appContext = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
let installBackupAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
let installBackupAppOperation = RSTAsyncBlockOperation { [ weak self ] operation in
2020-05-19 11:47:43 -07:00
app . managedObjectContext ? . perform {
guard let self = self else { return }
2023-03-01 00:48:36 -05:00
let progress = self . _installBackupApp ( for : app , operation : appOperation , group : group , context : restoreContext ) { result in
switch result {
case let . success ( installedApp ) : restoreContext . installedApp = installedApp
case let . failure ( error ) :
2020-05-19 11:47:43 -07:00
restoreContext . error = error
appContext . error = error
}
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
operation . finish ( )
}
installBackupAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
progress . addChild ( installBackupAppProgress , withPendingUnitCount : 30 )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
let backupAppOperation = BackupAppOperation ( action : . backup , context : restoreContext )
2023-03-01 00:48:36 -05:00
backupAppOperation . resultHandler = { result in
switch result {
2020-05-19 11:47:43 -07:00
case . success : break
2023-03-01 00:48:36 -05:00
case let . failure ( error ) :
2020-05-19 11:47:43 -07:00
restoreContext . error = error
appContext . error = error
}
}
backupAppOperation . addDependency ( installBackupAppOperation )
progress . addChild ( backupAppOperation . progress , withPendingUnitCount : 15 )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
let installAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
let installAppOperation = RSTAsyncBlockOperation { [ weak self ] operation in
2020-05-19 11:47:43 -07:00
app . managedObjectContext ? . perform {
guard let self = self else { return }
2023-03-01 00:48:36 -05:00
let progress = self . _install ( app , operation : appOperation , group : group , context : appContext ) { result in
2020-05-19 11:47:43 -07:00
completionHandler ( result )
operation . finish ( )
}
installAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
installAppOperation . addDependency ( backupAppOperation )
progress . addChild ( installAppProgress , withPendingUnitCount : 55 )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
group . add ( [ installBackupAppOperation , backupAppOperation , installAppOperation ] )
2023-03-01 00:48:36 -05:00
run ( [ installBackupAppOperation , installAppOperation , backupAppOperation ] , context : group . context )
2020-05-19 11:47:43 -07:00
return progress
}
2023-03-01 00:48:36 -05:00
private func _installBackupApp ( for app : InstalledApp , operation appOperation : AppOperation , group : RefreshGroup , context : InstallAppOperationContext , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress {
2020-05-16 16:17:18 -07:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
if let error = context . error {
2021-09-15 14:29:37 -07:00
completionHandler ( . failure ( error ) )
return progress
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
guard let application = ALTApplication ( fileURL : app . fileURL ) else {
completionHandler ( . failure ( OperationError . appNotFound ) )
return progress
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let prepareProgress = Progress . discreteProgress ( totalUnitCount : 1 )
2023-03-01 00:48:36 -05:00
let prepareOperation = RSTAsyncBlockOperation { operation in
2020-05-16 16:17:18 -07:00
app . managedObjectContext ? . perform {
2023-03-01 00:48:36 -05:00
do {
2020-05-16 16:17:18 -07:00
let temporaryDirectoryURL = context . temporaryDirectory . appendingPathComponent ( " AltBackup- " + UUID ( ) . uuidString )
try FileManager . default . createDirectory ( at : temporaryDirectoryURL , withIntermediateDirectories : true , attributes : nil )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
guard let altbackupFileURL = Bundle . main . url ( forResource : " AltBackup " , withExtension : " ipa " ) else { throw OperationError . appNotFound }
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let unzippedAppBundleURL = try FileManager . default . unzipAppBundle ( at : altbackupFileURL , toDirectory : temporaryDirectoryURL )
guard let unzippedAppBundle = Bundle ( url : unzippedAppBundleURL ) else { throw OperationError . invalidApp }
2023-03-01 00:48:36 -05:00
if var infoDictionary = unzippedAppBundle . infoDictionary {
2020-05-16 16:17:18 -07:00
// R e p l a c e n a m e + b u n d l e i d e n t i f i e r s o A l t S t o r e t r e a t s i t a s t h e s a m e a p p .
infoDictionary [ " CFBundleDisplayName " ] = app . name
infoDictionary [ kCFBundleIdentifierKey as String ] = app . bundleIdentifier
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
// A d d a p p - s p e c i f i c e x p o r t e d U T I s o w e c a n c h e c k l a t e r i f t h i s t e m p o r a r y b a c k u p a p p i s s t i l l i n s t a l l e d o r n o t .
let installedAppUTI = [ " UTTypeConformsTo " : [ ] ,
" UTTypeDescription " : " AltStore Backup App " ,
" UTTypeIconFiles " : [ ] ,
" UTTypeIdentifier " : app . installedBackupAppUTI ,
2023-03-01 00:48:36 -05:00
" UTTypeTagSpecification " : [ : ] ] as [ String : Any ]
2020-05-16 16:17:18 -07:00
var exportedUTIs = infoDictionary [ Bundle . Info . exportedUTIs ] as ? [ [ String : Any ] ] ? ? [ ]
exportedUTIs . append ( installedAppUTI )
infoDictionary [ Bundle . Info . exportedUTIs ] = exportedUTIs
2023-03-01 00:48:36 -05:00
if let cachedApp = ALTApplication ( fileURL : app . fileURL ) , let icon = cachedApp . icon ? . resizing ( to : CGSize ( width : 180 , height : 180 ) ) {
2020-05-17 22:22:25 -07:00
let iconFileURL = unzippedAppBundleURL . appendingPathComponent ( " AppIcon.png " )
2023-03-01 00:48:36 -05:00
if let iconData = icon . pngData ( ) {
do {
2020-05-17 22:22:25 -07:00
try iconData . write ( to : iconFileURL , options : . atomic )
2023-03-01 00:48:36 -05:00
2020-05-17 22:22:25 -07:00
let bundleIcons = [ " CFBundlePrimaryIcon " : [ " CFBundleIconFiles " : [ iconFileURL . lastPathComponent ] ] ]
infoDictionary [ " CFBundleIcons " ] = bundleIcons
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to write app icon data. %@ " , type : . error , error . localizedErrorCode )
2020-05-17 22:22:25 -07:00
}
}
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
try ( infoDictionary as NSDictionary ) . write ( to : unzippedAppBundle . infoPlistURL )
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
guard let backupApp = ALTApplication ( fileURL : unzippedAppBundleURL ) else { throw OperationError . invalidApp }
context . app = backupApp
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
prepareProgress . completedUnitCount += 1
2023-03-01 00:48:36 -05:00
} catch {
2020-05-16 16:17:18 -07:00
print ( error )
2023-03-01 00:48:36 -05:00
2021-09-15 14:29:37 -07:00
context . error = error
2020-05-16 16:17:18 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
operation . finish ( )
}
}
progress . addChild ( prepareProgress , withPendingUnitCount : 20 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let installProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
let installOperation = RSTAsyncBlockOperation { [ weak self ] operation in
2020-05-16 16:17:18 -07:00
guard let self = self else { return }
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
guard let backupApp = context . app else {
context . error = OperationError . invalidApp
operation . finish ( )
return
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
var appGroups = application . entitlements [ . appGroups ] as ? [ String ] ? ? [ ]
appGroups . append ( Bundle . baseAltStoreAppGroupID )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
let additionalEntitlements : [ ALTEntitlement : Any ] = [ . appGroups : appGroups ]
2023-03-01 00:48:36 -05:00
let progress = self . _install ( backupApp , operation : appOperation , group : group , context : context , additionalEntitlements : additionalEntitlements , cacheApp : false ) { result in
2020-05-16 16:17:18 -07:00
completionHandler ( result )
operation . finish ( )
}
installProgress . addChild ( progress , withPendingUnitCount : 100 )
}
installOperation . addDependency ( prepareOperation )
progress . addChild ( installProgress , withPendingUnitCount : 80 )
2023-03-01 00:48:36 -05:00
2020-05-16 16:17:18 -07:00
group . add ( [ prepareOperation , installOperation ] )
2023-03-01 00:48:36 -05:00
run ( [ prepareOperation , installOperation ] , context : group . context )
2020-03-06 17:08:35 -08:00
return progress
2019-06-21 11:20:03 -07:00
}
2023-03-01 00:48:36 -05:00
func finish ( _ operation : AppOperation , result : Result < InstalledApp , Error > , group : RefreshGroup , progress : Progress ? ) {
2020-03-06 17:08:35 -08:00
// M u s t r e m o v e b e f o r e s a v i n g i n s t a l l e d A p p .
2023-03-01 00:48:36 -05:00
if let currentProgress = self . progress ( for : operation ) , currentProgress = = progress {
2020-03-06 17:08:35 -08:00
// O n l y r e m o v e p r o g r e s s i f i t h a s n ' t b e e n r e p l a c e d b y a n o t h e r o n e .
2023-03-01 00:48:36 -05:00
set ( nil , for : operation )
2020-03-06 17:08:35 -08:00
}
2023-03-01 00:48:36 -05:00
do {
2020-03-06 17:08:35 -08:00
let installedApp = try result . get ( )
group . set ( . success ( installedApp ) , forAppWithBundleIdentifier : installedApp . bundleIdentifier )
2023-03-01 00:48:36 -05:00
if installedApp . bundleIdentifier = = StoreApp . altstoreAppID {
scheduleExpirationWarningLocalNotification ( for : installedApp )
2019-06-21 11:20:03 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-31 14:31:34 -07:00
let event : AnalyticsManager . Event ?
2023-03-01 00:48:36 -05:00
switch operation {
2020-03-31 14:31:34 -07:00
case . install : event = . installedApp ( installedApp )
case . refresh : event = . refreshedApp ( installedApp )
case . update where installedApp . bundleIdentifier = = StoreApp . altstoreAppID :
// A l t S t o r e q u i t s b e f o r e u p d a t e f i n i s h e s , s o w e ' v e p r e e m p t i v e l y l o g g e d t h i s u p d a t e e v e n t .
// I n c a s e A l t S t o r e d o e s n ' t q u i t , s u c h a s w h e n u p d a t e h a s a d i f f e r e n t b u n d l e i d e n t i f i e r ,
// m a k e s u r e w e d o n ' t l o g t h i s u p d a t e e v e n t a s e c o n d t i m e .
event = nil
2023-03-01 00:48:36 -05:00
2020-03-31 14:31:34 -07:00
case . update : event = . updatedApp ( installedApp )
2020-05-19 11:47:43 -07:00
case . activate , . deactivate , . backup , . restore : event = nil
2020-03-31 14:31:34 -07:00
}
2023-03-01 00:48:36 -05:00
if let event = event {
2020-03-31 14:31:34 -07:00
AnalyticsManager . shared . trackEvent ( event )
}
2023-03-01 00:48:36 -05:00
if #available ( iOS 14 , * ) {
2022-09-22 13:53:28 -05:00
WidgetCenter . shared . reloadAllTimelines ( )
2020-09-15 13:51:29 -07:00
}
2023-03-01 00:48:36 -05:00
2023-03-02 00:40:11 -05:00
do { try installedApp . managedObjectContext ? . save ( ) } catch { os_log ( " Error saving installed app. %@ " , type : . error , error . localizedDescription ) }
2023-03-01 00:48:36 -05:00
} catch {
2020-03-06 17:08:35 -08:00
group . set ( . failure ( error ) , forAppWithBundleIdentifier : operation . bundleIdentifier )
2023-03-01 00:48:36 -05:00
log ( error , for : operation )
2019-06-04 18:29:50 -07:00
}
}
2023-03-01 00:48:36 -05:00
func scheduleExpirationWarningLocalNotification ( for app : InstalledApp ) {
2019-09-21 22:31:10 -07:00
let notificationDate = app . expirationDate . addingTimeInterval ( - 1 * 60 * 60 * 24 ) // 2 4 h o u r s b e f o r e e x p i r a t i o n .
2023-03-01 00:48:36 -05:00
2019-09-21 22:31:10 -07:00
let timeIntervalUntilNotification = notificationDate . timeIntervalSinceNow
guard timeIntervalUntilNotification > 0 else {
// C r a s h e s i f w e p a s s n e g a t i v e v a l u e t o U N T i m e I n t e r v a l N o t i f i c a t i o n T r i g g e r i n i t i a l i z e r .
return
}
2023-03-01 00:48:36 -05:00
2019-09-21 22:31:10 -07:00
let trigger = UNTimeIntervalNotificationTrigger ( timeInterval : timeIntervalUntilNotification , repeats : false )
2023-03-01 00:48:36 -05:00
2019-09-21 22:31:10 -07:00
let content = UNMutableNotificationContent ( )
content . title = NSLocalizedString ( " AltStore Expiring Soon " , comment : " " )
content . body = NSLocalizedString ( " AltStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring. " , comment : " " )
content . sound = . default
2023-03-01 00:48:36 -05:00
2019-09-21 22:31:10 -07:00
let request = UNNotificationRequest ( identifier : AppManager . expirationWarningNotificationID , content : content , trigger : trigger )
UNUserNotificationCenter . current ( ) . add ( request )
}
2023-03-01 00:48:36 -05:00
func log ( _ error : Error , for operation : AppOperation ) {
2022-09-09 17:44:15 -05:00
// S a n i t i z e N S E r r o r o n s a m e t h r e a d b e f o r e p e r f o r m i n g b a c k g r o u n d t a s k .
let sanitizedError = ( error as NSError ) . sanitizedForCoreData ( )
2023-03-01 00:48:36 -05:00
2022-09-09 17:44:15 -05:00
let loggedErrorOperation : LoggedError . Operation = {
2023-03-01 00:48:36 -05:00
switch operation {
2022-09-09 17:44:15 -05:00
case . install : return . install
case . update : return . update
case . refresh : return . refresh
case . activate : return . activate
case . deactivate : return . deactivate
case . backup : return . backup
case . restore : return . restore
}
} ( )
2023-03-01 00:48:36 -05:00
2022-09-09 17:44:15 -05:00
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
var app = operation . app
2023-03-01 00:48:36 -05:00
if let managedApp = app as ? NSManagedObject , let tempApp = context . object ( with : managedApp . objectID ) as ? AppProtocol {
2022-09-09 17:44:15 -05:00
app = tempApp
}
2023-03-01 00:48:36 -05:00
do {
2022-09-09 17:44:15 -05:00
_ = LoggedError ( error : sanitizedError , app : app , operation : loggedErrorOperation , context : context )
try context . save ( )
2023-03-01 00:48:36 -05:00
} catch let saveError {
2023-03-02 00:40:11 -05:00
os_log ( " [ALTLog] Failed to log error %@ code %@ for %@: %@ " , type : . error ,
sanitizedError . domain ,
sanitizedError . code ,
app . bundleIdentifier ,
saveError . localizedErrorCode )
2022-09-09 17:44:15 -05:00
}
}
}
2023-03-01 00:48:36 -05:00
func run ( _ operations : [ Foundation . Operation ] , context : OperationContext ? , requiresSerialQueue : Bool = false ) {
2021-03-09 13:13:23 -06:00
// F i n d " I n s t a l l A l t S t o r e " o p e r a t i o n i f i t a l r e a d y e x i s t s i n ` c o n t e x t `
// s o w e c a n e n s u r e i t r u n s a f t e r a n y a d d i t i o n a l s e r i a l o p e r a t i o n s i n ` o p e r a t i o n s ` .
let installAltStoreOperation = context ? . operations . allObjects . lazy . compactMap { $0 as ? InstallAppOperation } . first { $0 . context . bundleIdentifier = = StoreApp . altstoreAppID }
2023-03-01 00:48:36 -05:00
for operation in operations {
switch operation {
2020-03-06 17:08:35 -08:00
case _ where requiresSerialQueue : fallthrough
2021-03-01 12:45:57 -06:00
case is InstallAppOperation , is RefreshAppOperation , is BackupAppOperation :
2023-03-01 00:48:36 -05:00
if let installAltStoreOperation = operation as ? InstallAppOperation , installAltStoreOperation . context . bundleIdentifier = = StoreApp . altstoreAppID {
2021-03-09 13:13:23 -06:00
// A d d d e p e n d e n c i e s o n p r e v i o u s s e r i a l o p e r a t i o n s i n ` c o n t e x t ` t o e n s u r e r e - i n s t a l l i n g A l t S t o r e g o e s l a s t .
let previousSerialOperations = context ? . operations . allObjects . filter { self . serialOperationQueue . operations . contains ( $0 ) }
previousSerialOperations ? . forEach { installAltStoreOperation . addDependency ( $0 ) }
2023-03-01 00:48:36 -05:00
} else if let installAltStoreOperation = installAltStoreOperation {
2021-03-09 13:13:23 -06:00
// R e - i n s t a l l i n g A l t S t o r e s h o u l d _ a l w a y s _ b e t h e l a s t s e r i a l o p e r a t i o n i n ` c o n t e x t ` .
installAltStoreOperation . addDependency ( operation )
2021-03-01 12:45:57 -06:00
}
2023-03-01 00:48:36 -05:00
serialOperationQueue . addOperation ( operation )
default : operationQueue . addOperation ( operation )
2020-03-06 17:08:35 -08:00
}
2023-03-01 00:48:36 -05:00
2020-03-30 15:23:20 -07:00
context ? . operations . add ( operation )
2020-03-06 17:08:35 -08:00
}
}
2023-03-01 00:48:36 -05:00
func progress ( for operation : AppOperation ) -> Progress ? {
switch operation {
case . install , . update : return installationProgress [ operation . bundleIdentifier ]
case . refresh , . activate , . deactivate , . backup , . restore : return refreshProgress [ operation . bundleIdentifier ]
2020-03-06 17:08:35 -08:00
}
}
2023-03-01 00:48:36 -05:00
func set ( _ progress : Progress ? , for operation : AppOperation ) {
switch operation {
case . install , . update : installationProgress [ operation . bundleIdentifier ] = progress
case . refresh , . activate , . deactivate , . backup , . restore : refreshProgress [ operation . bundleIdentifier ] = progress
2020-03-06 17:08:35 -08:00
}
}
2019-05-31 18:24:08 -07:00
}