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 .
//
import Foundation
import UIKit
2024-08-17 10:28:42 +09:00
import SwiftUI
2019-09-21 22:31:10 -07:00
import UserNotifications
2019-12-17 19:17:45 -08:00
import MobileCoreServices
2020-09-08 13:12:40 -07:00
import Intents
import Combine
2020-09-15 13:51:29 -07:00
import WidgetKit
2019-05-31 18:24:08 -07:00
2024-08-06 10:43:52 +09:00
import minimuxer
2020-09-03 16:39:08 -07:00
import AltStoreCore
2019-05-31 18:24:08 -07:00
import AltSign
import Roxas
2019-06-17 14:49:23 -07:00
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 " )
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 "
2022-11-16 17:41:21 -06:00
enum PreferredAppVersion
{
case latestSupportedVersion
case latestAvailableVersionWithFallback
}
2019-06-17 14:49:23 -07:00
}
2020-09-08 13:12:40 -07:00
@ available ( iOS 13 , * )
2023-01-04 09:52:12 -05:00
final class AppManagerPublisher : ObservableObject
2020-09-08 13:12:40 -07:00
{
@ Published
fileprivate ( set ) var installationProgress = [ String : Progress ] ( )
@ Published
fileprivate ( set ) var refreshProgress = [ String : Progress ] ( )
}
2023-01-04 09:52:12 -05:00
final class AppManager
2019-05-31 18:24:08 -07:00
{
static let shared = AppManager ( )
2022-04-14 17:39:43 -07:00
private ( set ) var updatePatronsResult : Result < Void , Error > ?
2024-08-17 10:28:42 +09: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 }
self . publisher . installationProgress = self . installationProgress
}
}
private var refreshProgress = [ String : Progress ] ( ) {
didSet {
guard #available ( iOS 13 , * ) else { return }
self . publisher . refreshProgress = self . refreshProgress
}
}
2019-05-31 18:24:08 -07:00
2022-11-21 17:04:50 -06:00
private lazy var progressLock : UnsafeMutablePointer < os_unfair_lock > = {
// C a n ' t s a f e l y p a s s & o s _ u n f a i r _ l o c k t o o s _ u n f a i r _ l o c k f u n c t i o n s i n S w i f t ,
// s o p a s s U n s a f e M u t a b l e P o i n t e r i n s t e a d w h i c h i s g u a r a n t e e d t o b e s a f e .
// h t t p s : / / s t a c k o v e r f l o w . c o m / a / 6 8 6 1 5 0 4 2
let lock = UnsafeMutablePointer < os_unfair_lock > . allocate ( capacity : 1 )
lock . initialize ( to : . init ( ) )
return lock
} ( )
2022-08-17 15:22:43 -05:00
@ available ( iOS 13 , * )
private ( set ) var publisher : AppManagerPublisher {
get { _publisher as ! AppManagerPublisher }
set { _publisher = newValue }
}
@ available ( iOS 13 , * )
private ( set ) var cancellables : Set < AnyCancellable > {
get { _cancellables as ! Set < AnyCancellable > }
set { _cancellables = newValue }
}
private lazy var _publisher : Any = {
guard #available ( iOS 13 , * ) else { fatalError ( ) }
return AppManagerPublisher ( )
} ( )
private lazy var _cancellables : Any = {
guard #available ( iOS 13 , * ) else { fatalError ( ) }
return Set < AnyCancellable > ( )
} ( )
2019-07-16 14:22:45 -07:00
2019-05-31 18:24:08 -07:00
private init ( )
{
2019-06-21 11:20:03 -07:00
self . operationQueue . name = " com.altstore.AppManager.operationQueue "
2020-03-06 17:08:35 -08:00
self . serialOperationQueue . name = " com.altstore.AppManager.serialOperationQueue "
self . serialOperationQueue . maxConcurrentOperationCount = 1
2020-09-08 13:12:40 -07:00
if #available ( iOS 13 , * )
{
self . prepareSubscriptions ( )
}
}
2022-11-21 17:04:50 -06:00
deinit
{
// S h o u l d n e v e r b e c a l l e d , b u t d o b o o k k e e p i n g a n y w a y .
self . progressLock . deinitialize ( count : 1 )
self . progressLock . deallocate ( )
}
2020-09-08 13:12:40 -07:00
@ available ( iOS 13 , * )
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 )
2020-09-08 13:12:40 -07:00
self . publisher . $ refreshProgress
. receive ( on : RunLoop . main )
. map ( \ . keys )
. flatMap { ( bundleIDs ) in
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
}
2020-09-10 11:27:44 -07:00
. sink { ( installedApp , isRefreshing ) in
installedApp . isRefreshing = isRefreshing
2020-09-08 13:12:40 -07:00
}
. store ( in : & self . cancellables )
2019-05-31 18:24:08 -07:00
}
}
2019-06-04 13:53:21 -07:00
extension AppManager
{
2019-06-04 18:29:50 -07:00
func update ( )
2019-06-04 13:53:21 -07:00
{
2020-03-23 12:12:49 -07:00
DatabaseManager . shared . persistentContainer . performBackgroundTask { ( context ) in
#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
do
{
let installedApps = InstalledApp . all ( in : context )
2020-03-11 17:29:32 -07:00
2020-03-23 12:12:49 -07:00
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
}
let legacySideloadedApps = Set ( UserDefaults . standard . legacySideloadedApps ? ? [ ] )
for app in installedApps
{
guard app . bundleIdentifier != StoreApp . altstoreAppID else {
self . scheduleExpirationWarningLocalNotification ( for : app )
continue
}
2020-05-14 11:02:40 -07:00
guard ! self . isActivelyManagingApp ( withBundleID : app . bundleIdentifier ) else { continue }
2020-05-16 16:17:18 -07:00
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 }
}
2020-03-23 12:12:49 -07:00
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 )
2021-10-25 22:27:30 -07:00
if var patchedApps = UserDefaults . standard . patchedApps , let index = patchedApps . firstIndex ( of : app . bundleIdentifier )
{
patchedApps . remove ( at : index )
UserDefaults . standard . patchedApps = patchedApps
}
2020-03-23 12:12:49 -07:00
}
}
try context . save ( )
}
catch
2020-01-13 11:22:40 -08:00
{
2020-03-23 12:12:49 -07:00
print ( " Error while fetching installed apps. " , error )
2020-01-13 11:22:40 -08:00
}
2020-03-23 12:12:49 -07:00
#endif
2020-01-13 11:22:40 -08:00
2020-03-23 12:12:49 -07:00
do
2019-06-04 13:53:21 -07:00
{
2020-03-23 12:12:49 -07:00
let installedAppBundleIDs = InstalledApp . all ( in : context ) . map { $0 . bundleIdentifier }
let cachedAppDirectories = try FileManager . default . contentsOfDirectory ( at : InstalledApp . appsDirectoryURL ,
includingPropertiesForKeys : [ . isDirectoryKey , . nameKey ] ,
options : [ . skipsSubdirectoryDescendants , . skipsHiddenFiles ] )
for appDirectory in cachedAppDirectories
2020-03-11 17:29:32 -07:00
{
2020-03-23 12:12:49 -07:00
do
{
let resourceValues = try appDirectory . resourceValues ( forKeys : [ . isDirectoryKey , . nameKey ] )
guard let isDirectory = resourceValues . isDirectory , let bundleID = resourceValues . name else { continue }
2020-05-14 11:02:40 -07:00
if isDirectory && ! installedAppBundleIDs . contains ( bundleID ) && ! self . isActivelyManagingApp ( withBundleID : bundleID )
2020-03-23 12:12:49 -07:00
{
2020-05-07 13:10:01 -07:00
print ( " DELETING CACHED APP: " , bundleID )
2020-03-23 12:12:49 -07:00
try FileManager . default . removeItem ( at : appDirectory )
}
}
catch
{
print ( " Failed to remove cached app directory. " , error )
}
2019-06-04 13:53:21 -07:00
}
}
2020-03-23 12:12:49 -07:00
catch
{
print ( " Failed to remove cached apps. " , error )
}
2019-06-04 13:53:21 -07:00
}
}
2019-06-05 18:05:21 -07:00
2020-03-06 17:08:35 -08:00
@ discardableResult
func authenticate ( presentingViewController : UIViewController ? , context : AuthenticatedOperationContext = AuthenticatedOperationContext ( ) , completionHandler : @ escaping ( Result < ( ALTTeam , ALTCertificate , ALTAppleAPISession ) , Error > ) -> Void ) -> AuthenticationOperation
{
if let operation = context . authenticationOperation
{
return operation
}
let authenticationOperation = AuthenticationOperation ( context : context , presentingViewController : presentingViewController )
2019-06-05 18:05:21 -07:00
authenticationOperation . resultHandler = { ( result ) in
2020-02-10 17:30:11 -08:00
switch result
{
2020-03-06 17:08:35 -08:00
case . failure ( let error ) : context . error = error
case . success : break
2020-02-10 17:30:11 -08:00
}
2019-06-05 18:05:21 -07:00
completionHandler ( result )
}
2020-02-10 17:30:11 -08:00
2020-03-30 15:23:20 -07:00
self . run ( [ authenticationOperation ] , context : context )
2020-03-06 17:08:35 -08:00
return authenticationOperation
2019-06-05 18:05:21 -07:00
}
2021-09-15 14:27:16 -07:00
func deactivateApps ( for app : ALTApplication , presentingViewController : UIViewController , completion : @ escaping ( Result < Void , Error > ) -> Void )
{
2024-10-24 00:41:29 -04:00
guard ! UserDefaults . standard . isAppLimitDisabled , let activeAppsLimit = UserDefaults . standard . activeAppsLimit else { return completion ( . success ( ( ) ) ) }
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 ) }
var title : String = NSLocalizedString ( " Cannot Activate More than 3 Apps " , comment : " " )
let message : String
if UserDefaults . standard . activeAppLimitIncludesExtensions
{
if app . appExtensions . isEmpty
{
message = NSLocalizedString ( " Non-developer Apple IDs are limited to 3 active apps and app extensions. Please choose an app to deactivate. " , comment : " " )
}
else
{
title = NSLocalizedString ( " Cannot Activate More than 3 Apps and App Extensions " , comment : " " )
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 )
}
}
else
{
message = NSLocalizedString ( " Non-developer Apple IDs are limited to 3 active apps. Please choose an app to deactivate. " , comment : " " )
}
let activeAppsCount = activeApps . map { $0 . requiredActiveSlots } . reduce ( 0 , + )
let availableActiveApps = max ( activeAppsLimit - activeAppsCount , 0 )
let requiredActiveSlots = UserDefaults . standard . activeAppLimitIncludesExtensions ? ( 1 + app . appExtensions . count ) : 1
guard requiredActiveSlots > availableActiveApps else { return completion ( . success ( ( ) ) ) }
let alertController = UIAlertController ( title : title , message : message , preferredStyle : . alert )
alertController . addAction ( UIAlertAction ( title : UIAlertAction . cancel . title , style : UIAlertAction . cancel . style ) { ( action ) in
completion ( . failure ( OperationError . cancelled ) )
} )
for activeApp in activeApps where activeApp . bundleIdentifier != StoreApp . altstoreAppID
{
alertController . addAction ( UIAlertAction ( title : activeApp . name , style : . default ) { ( action ) in
activeApp . isActive = false
self . deactivate ( activeApp , presentingViewController : presentingViewController ) { ( result ) in
switch result
{
case . failure ( let error ) :
activeApp . managedObjectContext ? . perform {
activeApp . isActive = true
completion ( . failure ( error ) )
}
case . success :
self . deactivateApps ( for : app , presentingViewController : presentingViewController , completion : completion )
}
}
} )
}
presentingViewController . present ( alertController , animated : true , completion : nil )
}
}
2023-02-07 16:11:39 -06:00
func clearAppCache ( completion : @ escaping ( Result < Void , Error > ) -> Void )
{
let clearAppCacheOperation = ClearAppCacheOperation ( )
clearAppCacheOperation . resultHandler = { result in
completion ( result )
}
self . run ( [ clearAppCacheOperation ] , context : nil )
}
2024-08-06 10:43:52 +09:00
func log ( _ error : Error , operation : LoggedError . Operation , app : AppProtocol )
{
switch error {
case ~ OperationError . Code . cancelled : return // D o n ' t l o g c a n c e l l e d e v e n t s
default : break
}
// 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 ) . sanitizedForSerialization ( )
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
var app = app
if let managedApp = app as ? NSManagedObject , let tempApp = context . object ( with : managedApp . objectID ) as ? AppProtocol
{
app = tempApp
}
do
{
_ = LoggedError ( error : sanitizedError , app : app , operation : operation , context : context )
try context . save ( )
}
catch let saveError
{
print ( " [ALTLog] Failed to log error \( sanitizedError . domain ) code \( sanitizedError . code ) for \( app . bundleIdentifier ) : " , saveError )
}
}
}
2019-06-04 13:53:21 -07:00
}
2019-05-31 18:24:08 -07:00
extension AppManager
2019-06-17 14:49:23 -07:00
{
2022-04-14 16:24:11 -07:00
func fetchSource ( sourceURL : URL ,
managedObjectContext : NSManagedObjectContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( ) ,
dependencies : [ Foundation . Operation ] = [ ] ,
completionHandler : @ escaping ( Result < Source , Error > ) -> Void )
2020-03-24 13:27:44 -07:00
{
2022-04-14 16:24:11 -07:00
let fetchSourceOperation = FetchSourceOperation ( sourceURL : sourceURL , managedObjectContext : managedObjectContext )
2020-03-24 13:27:44 -07:00
fetchSourceOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) :
completionHandler ( . failure ( error ) )
case . success ( let source ) :
completionHandler ( . success ( source ) )
}
}
2022-04-14 16:24:11 -07:00
for dependency in dependencies
{
fetchSourceOperation . addDependency ( dependency )
}
2020-03-30 15:23:20 -07:00
self . run ( [ fetchSourceOperation ] , context : nil )
2020-03-24 13:27:44 -07:00
}
2020-08-27 16:23:50 -07:00
func fetchSources ( completionHandler : @ escaping ( Result < ( Set < Source > , NSManagedObjectContext ) , FetchSourcesError > ) -> Void )
2019-06-17 14:49:23 -07:00
{
2019-07-30 17:00:04 -07:00
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 ) ) ) }
2020-03-24 13:27:44 -07:00
let dispatchGroup = DispatchGroup ( )
var fetchedSources = Set < Source > ( )
2020-08-27 16:23:50 -07:00
var errors = [ Source : Error ] ( )
2020-03-24 13:27:44 -07:00
let managedObjectContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( )
let operations = sources . map { ( source ) -> FetchSourceOperation in
dispatchGroup . enter ( )
let fetchSourceOperation = FetchSourceOperation ( sourceURL : source . sourceURL , managedObjectContext : managedObjectContext )
fetchSourceOperation . resultHandler = { ( result ) in
switch result
{
case . success ( let source ) : fetchedSources . insert ( source )
2022-11-22 13:02:19 -06:00
case . failure ( let nsError as NSError ) :
2020-08-27 16:23:50 -07:00
let source = managedObjectContext . object ( with : source . objectID ) as ! Source
2022-11-22 13:02:19 -06:00
let title = String ( format : NSLocalizedString ( " Unable to Refresh “%@” Source " , comment : " " ) , source . name )
let error = nsError . withLocalizedTitle ( title )
2020-08-27 16:23:50 -07:00
errors [ source ] = error
2022-11-22 13:02:19 -06:00
source . error = error . sanitizedForSerialization ( )
2020-03-24 13:27:44 -07:00
}
dispatchGroup . leave ( )
}
return fetchSourceOperation
2019-07-30 17:00:04 -07:00
}
2020-03-24 13:27:44 -07:00
dispatchGroup . notify ( queue : . global ( ) ) {
2020-08-27 16:23:50 -07:00
managedObjectContext . perform {
if ! errors . isEmpty
{
let sources = Set ( sources . compactMap { managedObjectContext . object ( with : $0 . objectID ) as ? Source } )
completionHandler ( . failure ( . init ( sources : sources , errors : errors , context : managedObjectContext ) ) )
}
else
{
completionHandler ( . success ( ( fetchedSources , managedObjectContext ) ) )
2020-03-24 13:27:44 -07:00
}
2022-11-16 14:11:11 -06:00
NotificationCenter . default . post ( name : AppManager . didFetchSourceNotification , object : self )
2020-03-24 13:27:44 -07:00
}
2019-06-17 14:49:23 -07:00
}
2020-03-24 13:27:44 -07:00
2020-03-30 15:23:20 -07:00
self . run ( operations , context : nil )
2019-06-17 14:49:23 -07: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
// p r i n t ( " 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 : " , r e s u l t )
2020-02-10 17:30:11 -08:00
}
2020-03-06 17:08:35 -08:00
let fetchAppIDsOperation = FetchAppIDsOperation ( context : authenticationOperation . context )
fetchAppIDsOperation . resultHandler = completionHandler
fetchAppIDsOperation . addDependency ( authenticationOperation )
2020-03-30 15:23:20 -07:00
self . run ( [ fetchAppIDsOperation ] , context : authenticationOperation . context )
2020-02-10 17:30:11 -08:00
}
2020-03-06 17:08:35 -08:00
2022-04-14 15:27:57 -07:00
@ discardableResult
func fetchTrustedSources ( completionHandler : @ escaping ( Result < [ FetchTrustedSourcesOperation . TrustedSource ] , Error > ) -> Void ) -> FetchTrustedSourcesOperation
{
let fetchTrustedSourcesOperation = FetchTrustedSourcesOperation ( )
fetchTrustedSourcesOperation . resultHandler = completionHandler
self . run ( [ fetchTrustedSourcesOperation ] , context : nil )
return fetchTrustedSourcesOperation
}
2022-04-14 17:39:43 -07:00
func updatePatronsIfNeeded ( )
{
guard self . operationQueue . operations . allSatisfy ( { ! ( $0 is UpdatePatronsOperation ) } ) else {
// 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
}
self . updatePatronsResult = nil
let updatePatronsOperation = UpdatePatronsOperation ( )
updatePatronsOperation . resultHandler = { ( result ) in
do
{
try result . get ( )
self . updatePatronsResult = . success ( ( ) )
}
catch
{
print ( " Error updating Friend Zone Patrons: " , error )
self . updatePatronsResult = . failure ( error )
}
NotificationCenter . default . post ( name : AppManager . didUpdatePatronsNotification , object : self )
}
self . run ( [ updatePatronsOperation ] , context : nil )
}
2020-03-06 17:08:35 -08:00
@ discardableResult
2021-10-25 22:27:30 -07:00
func install < T : AppProtocol > ( _ app : T , presentingViewController : UIViewController ? , context : AuthenticatedOperationContext = AuthenticatedOperationContext ( ) , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> RefreshGroup
2019-05-31 18:24:08 -07:00
{
2020-03-06 17:08:35 -08:00
let group = RefreshGroup ( context : context )
group . completionHandler = { ( results ) in
2019-06-18 18:31:59 -07:00
do
2019-05-31 18:24:08 -07:00
{
2024-08-06 10:43:52 +09:00
guard let result = results . values . first else { throw context . error ? ? OperationError . unknown ( ) }
2019-06-18 18:31:59 -07:00
completionHandler ( result )
}
catch
{
completionHandler ( . failure ( error ) )
2019-05-31 18:24:08 -07:00
}
}
2019-06-10 15:03:47 -07:00
2020-03-06 17:08:35 -08:00
let operation = AppOperation . install ( app )
self . perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
2019-07-16 14:22:45 -07:00
2021-10-25 22:27:30 -07:00
return group
2019-05-31 18:24:08 -07:00
}
2019-06-04 18:29:50 -07:00
2020-03-31 14:31:34 -07:00
@ discardableResult
2022-11-16 17:41:21 -06:00
func update ( _ installedApp : InstalledApp , to preferredAppVersion : PreferredAppVersion = . latestSupportedVersion , presentingViewController : UIViewController ? , context : AuthenticatedOperationContext = AuthenticatedOperationContext ( ) , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress
2020-03-31 14:31:34 -07:00
{
2022-11-16 17:41:21 -06:00
let preferredApp : AppProtocol ?
switch preferredAppVersion
{
case . latestSupportedVersion : preferredApp = installedApp . storeApp ? . latestSupportedVersion
case . latestAvailableVersionWithFallback : preferredApp = installedApp . storeApp // U s e S t o r e A p p d i r e c t l y t o c o r r e c t l y h a n d l e m i n / m a x O S v e r s i o n s i n D o w n l o a d A p p O p e r a t i o n .
}
guard let app = preferredApp else {
completionHandler ( . failure ( OperationError . appNotFound ( name : installedApp . name ) ) )
2020-03-31 14:31:34 -07:00
return Progress . discreteProgress ( totalUnitCount : 1 )
}
let group = RefreshGroup ( context : context )
group . completionHandler = { ( results ) in
do
{
2024-08-06 10:43:52 +09:00
guard let result = results . values . first else { throw OperationError . unknown ( ) }
2020-03-31 14:31:34 -07:00
completionHandler ( result )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
2022-11-16 17:41:21 -06:00
let operation = AppOperation . update ( app )
assert ( operation . app as AnyObject !== installedApp ) // 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 .
2020-03-31 14:31:34 -07:00
self . perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
return group . progress
}
2020-03-06 17:08:35 -08:00
@ discardableResult
func refresh ( _ installedApps : [ InstalledApp ] , presentingViewController : UIViewController ? , group : RefreshGroup ? = nil ) -> RefreshGroup
2019-06-04 18:29:50 -07:00
{
2020-03-06 17:08:35 -08:00
let group = group ? ? RefreshGroup ( )
2019-07-19 16:10:30 -07:00
2020-03-06 17:08:35 -08:00
let operations = installedApps . map { AppOperation . refresh ( $0 ) }
return self . perform ( operations , presentingViewController : presentingViewController , group : group )
2019-06-18 18:31:59 -07:00
}
2019-07-16 14:22:45 -07:00
2020-03-11 14:43:19 -07:00
func activate ( _ installedApp : InstalledApp , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void )
{
2020-05-16 16:29:28 -07:00
let group = RefreshGroup ( )
let operation = AppOperation . activate ( installedApp )
self . perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
2020-03-11 14:43:19 -07:00
group . completionHandler = { ( results ) in
do
{
2024-08-06 10:43:52 +09:00
guard let result = results . values . first else { throw OperationError . unknown ( ) }
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 )
2020-03-11 14:43:19 -07:00
installedApp . managedObjectContext ? . perform {
installedApp . isActive = true
completionHandler ( . success ( installedApp ) )
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
2020-05-16 16:17:18 -07:00
func deactivate ( _ installedApp : InstalledApp , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void )
2020-03-11 14:43:19 -07:00
{
2020-05-16 16:17:18 -07:00
if UserDefaults . standard . isLegacyDeactivationSupported
{
// 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 ( )
let deactivateAppOperation = DeactivateAppOperation ( app : installedApp , context : context )
deactivateAppOperation . resultHandler = { ( result ) in
completionHandler ( result )
}
self . run ( [ deactivateAppOperation ] , context : context , requiresSerialQueue : true )
}
else
{
let group = RefreshGroup ( )
group . completionHandler = { ( results ) in
do
{
2024-08-06 10:43:52 +09:00
guard let result = results . values . first else { throw OperationError . unknown ( ) }
2020-05-16 16:17:18 -07:00
let installedApp = try result . get ( )
assert ( installedApp . managedObjectContext != nil )
installedApp . managedObjectContext ? . perform {
completionHandler ( . success ( installedApp ) )
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
let operation = AppOperation . deactivate ( installedApp )
self . perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
2020-03-11 14:43:19 -07:00
}
}
2020-05-19 11:47:43 -07:00
func backup ( _ installedApp : InstalledApp , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void )
{
let group = RefreshGroup ( )
group . completionHandler = { ( results ) in
do
{
2024-08-06 10:43:52 +09:00
guard let result = results . values . first else { throw OperationError . unknown ( ) }
2020-05-19 11:47:43 -07:00
let installedApp = try result . get ( )
assert ( installedApp . managedObjectContext != nil )
installedApp . managedObjectContext ? . perform {
completionHandler ( . success ( installedApp ) )
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
let operation = AppOperation . backup ( installedApp )
self . perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
}
2020-05-16 16:39:02 -07:00
func restore ( _ installedApp : InstalledApp , presentingViewController : UIViewController ? , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void )
{
let group = RefreshGroup ( )
group . completionHandler = { ( results ) in
do
{
2024-08-06 10:43:52 +09:00
guard let result = results . values . first else { throw OperationError . unknown ( ) }
2020-05-16 16:39:02 -07:00
let installedApp = try result . get ( )
assert ( installedApp . managedObjectContext != nil )
installedApp . managedObjectContext ? . perform {
installedApp . isActive = true
completionHandler ( . success ( installedApp ) )
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
let operation = AppOperation . restore ( installedApp )
self . perform ( [ operation ] , presentingViewController : presentingViewController , group : group )
}
2020-05-16 15:34:10 -07:00
func remove ( _ installedApp : InstalledApp , completionHandler : @ escaping ( Result < Void , Error > ) -> Void )
{
let authenticationContext = AuthenticatedOperationContext ( )
let appContext = InstallAppOperationContext ( bundleIdentifier : installedApp . bundleIdentifier , authenticatedContext : authenticationContext )
appContext . installedApp = installedApp
let removeAppOperation = RSTAsyncBlockOperation { ( operation ) in
DatabaseManager . shared . persistentContainer . performBackgroundTask { ( context ) in
let installedApp = context . object ( with : installedApp . objectID ) as ! InstalledApp
context . delete ( installedApp )
do { try context . save ( ) }
catch { appContext . error = error }
operation . finish ( )
}
}
let removeAppBackupOperation = RemoveAppBackupOperation ( context : appContext )
removeAppBackupOperation . resultHandler = { ( result ) in
switch result
{
case . success : break
case . failure ( let error ) : print ( " Failed to remove app backup. " , error )
}
// 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 .
if let error = appContext . error
{
completionHandler ( . failure ( error ) )
}
else
{
completionHandler ( . success ( ( ) ) )
}
}
removeAppBackupOperation . addDependency ( removeAppOperation )
self . run ( [ removeAppOperation , removeAppBackupOperation ] , context : authenticationContext )
2021-09-03 13:57:15 -05:00
}
@ available ( iOS 14 , * )
func enableJIT ( for installedApp : InstalledApp , completionHandler : @ escaping ( Result < Void , Error > ) -> Void )
{
2023-01-04 09:52:12 -05:00
final class Context : OperationContext , EnableJITContext
2021-09-03 13:57:15 -05:00
{
var installedApp : InstalledApp ?
}
let context = Context ( )
context . installedApp = installedApp
let enableJITOperation = EnableJITOperation ( context : context )
enableJITOperation . resultHandler = { ( result ) in
2024-08-06 10:43:52 +09:00
switch result {
case . success : completionHandler ( . success ( ( ) ) )
case . failure ( let nsError as NSError ) :
2023-01-24 14:07:30 -06:00
let localizedTitle = String ( format : NSLocalizedString ( " Failed to Enable JIT for %@ " , comment : " " ) , appName )
2024-08-06 10:43:52 +09:00
let error = nsError . withLocalizedTitle ( localizedTitle )
2023-01-24 14:07:30 -06:00
2024-08-06 10:43:52 +09:00
self . log ( error , operation : . enableJIT , app : installedApp )
2023-01-24 14:07:30 -06:00
completionHandler ( . failure ( error ) )
2024-08-06 10:43:52 +09:00
}
2021-09-03 13:57:15 -05:00
}
2023-01-24 14:07:30 -06:00
enableJITOperation . addDependency ( findServerOperation )
2021-09-03 13:57:15 -05:00
self . run ( [ enableJITOperation ] , context : context , requiresSerialQueue : true )
2020-05-16 15:34:10 -07:00
}
2021-10-25 22:27:30 -07:00
@ available ( iOS 14.0 , * )
func patch ( resignedApp : ALTApplication , presentingViewController : UIViewController , context authContext : AuthenticatedOperationContext , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> PatchAppOperation
{
2023-01-04 09:52:12 -05:00
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 ) )
return PatchAppOperation ( context : context )
}
let context = Context ( bundleIdentifier : originalBundleID , authenticatedContext : authContext )
context . resignedApp = resignedApp
let patchAppOperation = PatchAppOperation ( context : context )
let sendAppOperation = SendAppOperation ( context : context )
let installOperation = InstallAppOperation ( context : context )
let installationProgress = Progress . discreteProgress ( totalUnitCount : 100 )
installationProgress . addChild ( sendAppOperation . progress , withPendingUnitCount : 40 )
installationProgress . addChild ( installOperation . progress , withPendingUnitCount : 60 )
/* P a t c h */
patchAppOperation . resultHandler = { [ weak patchAppOperation ] ( result ) in
switch result
{
case . failure ( let error ) : context . error = error
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 : " " ) )
}
}
/* S e n d */
sendAppOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : context . error = error
2022-11-02 17:58:59 -07:00
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 */
installOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : completionHandler ( . failure ( error ) )
case . success ( let installedApp ) : completionHandler ( . success ( installedApp ) )
}
}
installOperation . addDependency ( sendAppOperation )
self . run ( [ patchAppOperation , sendAppOperation , installOperation ] , context : context . authenticatedContext )
return patchAppOperation
}
2019-07-28 15:08:13 -07:00
func installationProgress ( for app : AppProtocol ) -> Progress ?
2019-07-16 14:22:45 -07:00
{
2022-11-21 17:04:50 -06:00
os_unfair_lock_lock ( self . progressLock )
defer { os_unfair_lock_unlock ( self . progressLock ) }
2019-07-28 15:08:13 -07:00
let progress = self . installationProgress [ app . bundleIdentifier ]
2019-07-19 16:10:30 -07:00
return progress
}
2019-07-28 15:08:13 -07:00
func refreshProgress ( for app : AppProtocol ) -> Progress ?
2019-07-19 16:10:30 -07:00
{
2022-11-21 17:04:50 -06:00
os_unfair_lock_lock ( self . progressLock )
defer { os_unfair_lock_unlock ( self . progressLock ) }
2019-07-28 15:08:13 -07:00
let progress = self . refreshProgress [ app . bundleIdentifier ]
2019-07-16 14:22:45 -07:00
return progress
}
2023-02-07 16:11:39 -06:00
func isActivelyManagingApp ( withBundleID bundleID : String ) -> Bool
{
let isActivelyManaging = self . installationProgress . keys . contains ( bundleID ) || self . refreshProgress . keys . contains ( bundleID )
return isActivelyManaging
}
2019-06-18 18:31:59 -07:00
}
2020-09-08 12:29:44 -07:00
extension AppManager
{
2021-10-04 16:27:00 -07:00
@ discardableResult
2022-12-19 23:26:15 -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
self . run ( [ backgroundRefreshAppsOperation ] , context : nil )
2021-10-04 16:27:00 -07:00
return backgroundRefreshAppsOperation
2020-09-08 12:29:44 -07:00
}
}
2019-06-18 18:31:59 -07:00
private extension AppManager
{
2020-03-06 17:08:35 -08:00
enum AppOperation
2019-06-18 18:31:59 -07:00
{
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 )
2019-06-21 11:20:03 -07:00
2020-03-06 17:08:35 -08:00
var app : AppProtocol {
switch self
{
2020-05-19 11:47:43 -07:00
case . install ( let app ) , . update ( let app ) , . refresh ( let app as AppProtocol ) ,
. 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
}
}
var bundleIdentifier : String {
var bundleIdentifier : String !
if let context = ( self . app as ? NSManagedObject ) ? . managedObjectContext
2019-09-10 12:19:46 -07:00
{
2020-03-06 17:08:35 -08:00
context . performAndWait { bundleIdentifier = self . app . bundleIdentifier }
2019-09-10 12:19:46 -07:00
}
2020-03-06 17:08:35 -08:00
else
{
bundleIdentifier = self . app . bundleIdentifier
}
return bundleIdentifier
2019-09-10 12:19:46 -07:00
}
2024-08-06 10:43:52 +09:00
var loggedErrorOperation : LoggedError . Operation {
switch self {
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
}
}
2020-03-06 17:08:35 -08:00
}
@ discardableResult
private func perform ( _ operations : [ AppOperation ] , presentingViewController : UIViewController ? , group : RefreshGroup ) -> RefreshGroup
{
let operations = operations . filter { self . progress ( for : $0 ) = = nil || self . progress ( for : $0 ) ? . isCancelled = = true }
2019-09-10 12:19:46 -07:00
2020-03-06 17:08:35 -08:00
for operation in operations
{
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
self . set ( progress , for : operation )
}
2019-11-18 14:49:17 -08:00
2020-05-02 22:06:57 -07:00
if let viewController = presentingViewController
{
group . context . presentingViewController = viewController
}
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 ?
if group . context . session = = nil
2019-10-28 13:16:55 -07:00
{
2020-03-06 17:08:35 -08:00
authenticationOperation = self . authenticate ( presentingViewController : presentingViewController , context : group . context ) { ( result ) in
2019-10-28 13:16:55 -07:00
switch result
{
2020-03-06 17:08:35 -08:00
case . failure ( let error ) : group . context . error = error
case . success : break
2019-10-28 13:16:55 -07:00
}
}
2020-03-06 17:08:35 -08:00
}
func performAppOperations ( )
{
for operation in operations
{
let progress = self . progress ( for : operation )
if let progress = progress
{
group . progress . totalUnitCount += 1
group . progress . addChild ( progress , withPendingUnitCount : 1 )
if group . context . session != nil
{
// 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
}
}
switch operation
{
2020-05-16 16:17:18 -07:00
case . install ( let app ) , . update ( let app ) :
let installProgress = self . _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 )
2020-03-06 17:08:35 -08:00
2020-05-16 16:29:28 -07:00
case . activate ( let app ) where UserDefaults . standard . isLegacyDeactivationSupported : fallthrough
2020-05-16 16:17:18 -07:00
case . refresh ( let app ) :
// 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
2023-04-11 21:12:01 -07:00
if app . certificateSerialNumber != group . context . certificate ? . serialNumber ||
2021-03-01 12:41:33 -06:00
uti != nil ||
2023-04-11 21:50:15 -07:00
app . needsResign ||
// W e n e e d t o r e i n s t a l l o u r s e l v e s o n r e f r e s h t o e n s u r e t h e n e w p r o v i s i o n i n g p r o f i l e i s u s e d
app . bundleIdentifier = = StoreApp . altstoreAppID
2020-05-16 16:17:18 -07:00
{
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 )
2020-05-16 16:17:18 -07:00
2020-10-01 11:51:39 -07:00
let installProgress = self . _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 )
2020-05-16 16:17:18 -07: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 .
2020-05-16 16:17:18 -07:00
2020-10-01 11:51:39 -07:00
let refreshProgress = self . _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
}
2020-05-16 16:29:28 -07:00
case . activate ( let app ) :
let activateProgress = self . _activate ( app , operation : operation , group : group ) { ( result ) in
self . finish ( operation , result : result , group : group , progress : progress )
}
progress ? . addChild ( activateProgress , withPendingUnitCount : 80 )
2020-05-16 16:17:18 -07:00
case . deactivate ( let app ) :
let deactivateProgress = self . _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 )
2020-05-16 16:39:02 -07:00
2020-05-19 11:47:43 -07:00
case . backup ( let app ) :
let backupProgress = self . _backup ( app , operation : operation , group : group ) { ( result ) in
self . finish ( operation , result : result , group : group , progress : progress )
}
progress ? . addChild ( backupProgress , withPendingUnitCount : 80 )
2020-05-16 16:39:02 -07:00
case . restore ( let app ) :
// 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 .
let activateProgress = self . _activate ( app , operation : operation , group : group ) { ( result ) in
self . finish ( operation , result : result , group : group , progress : progress )
}
progress ? . addChild ( activateProgress , withPendingUnitCount : 80 )
2020-03-06 17:08:35 -08:00
}
}
}
if let authenticationOperation = authenticationOperation
{
let awaitAuthenticationOperation = BlockOperation {
if let managedObjectContext = operations . lazy . compactMap ( { ( $0 . app as ? NSManagedObject ) ? . managedObjectContext } ) . first
{
managedObjectContext . perform { performAppOperations ( ) }
}
else
{
performAppOperations ( )
}
}
awaitAuthenticationOperation . addDependency ( authenticationOperation )
2020-03-30 15:23:20 -07:00
self . run ( [ awaitAuthenticationOperation ] , context : group . context , requiresSerialQueue : true )
2019-11-18 14:49:17 -08:00
}
else
{
2023-11-28 00:44:47 +09:00
DispatchQueue . main . schedule {
UIApplication . shared . isIdleTimerDisabled = UserDefaults . standard . isIdleTimeoutDisableEnabled
}
2020-03-06 17:08:35 -08:00
performAppOperations ( )
2023-11-28 00:44:47 +09:00
DispatchQueue . main . schedule {
UIApplication . shared . isIdleTimerDisabled = false
}
2019-11-18 14:49:17 -08:00
}
2019-06-21 11:20:03 -07:00
2020-03-06 17:08:35 -08:00
return group
}
2024-11-30 00:50:10 -05:00
func removeAppExtensions (
from application : ALTApplication ,
existingApp : InstalledApp ? ,
extensions : Set < ALTApplication > ,
_ presentingViewController : UIViewController ? ,
completion : @ escaping ( Result < Void , Error > ) -> Void
) {
2024-11-10 23:31:31 +05:30
2024-11-30 00:50:10 -05:00
// A p p - E x t e n s i o n s : E n s u r e e x i s t i n g a p p ' s e x t e n s i o n s a n d c u r r e n t l y i n s t a l l i n g a p p ' s e x t e n s i o n s m u s t m a t c h
if let existingApp {
_ = RSTAsyncBlockOperation { _ in
let existingAppEx : Set < InstalledExtension > = existingApp . appExtensions
let currentAppEx : Set < ALTApplication > = application . appExtensions
let currentAppExNames = currentAppEx . map { appEx in appEx . bundleIdentifier }
let existingAppExNames = existingAppEx . map { appEx in appEx . bundleIdentifier }
let excessExtensions = currentAppEx . filter {
! ( existingAppExNames . contains ( $0 . bundleIdentifier ) )
}
let isMatching = ( currentAppEx . count = = existingAppEx . count ) && excessExtensions . isEmpty
let diagnosticsMsg = " AppManager.removeAppExtensions: App Extensions in existingApp and currentApp are matching: \( isMatching ) \n "
+ " AppManager.removeAppExtensions: existingAppEx: \( existingAppExNames ) ; currentAppEx: \( String ( describing : currentAppExNames ) ) \n "
print ( diagnosticsMsg )
// i f b a c k g r o u n d m o d e , t h e n r e m o v e o n l y t h e e x c e s s e x t e n s i o n s
guard let presentingViewController : UIViewController = presentingViewController else {
// p e r f o r m s i l e n t e x t e n s i o n s c l e a n u p f o r t h o s e t h a t a r e n ' t a l r e a d y p r e s e n t i n e x i s t i n g a p p
print ( " \n Performing background mode Extensions removal \n " )
print ( " AppManager.removeAppExtensions: Excess Extensions: \( excessExtensions ) " )
do {
for appExtension in excessExtensions {
print ( " Deleting extension \( appExtension . bundleIdentifier ) " )
try FileManager . default . removeItem ( at : appExtension . fileURL )
}
return completion ( . success ( ( ) ) )
} catch {
return completion ( . failure ( error ) )
}
2024-11-10 23:31:31 +05:30
}
}
}
2024-10-17 04:47:42 -04:00
guard ! application . appExtensions . isEmpty else { return completion ( . success ( ( ) ) ) }
2024-08-16 12:57:42 +09:00
2024-11-30 00:50:10 -05:00
DispatchQueue . main . async {
let firstSentence : String
if UserDefaults . standard . activeAppLimitIncludesExtensions
2024-08-16 12:57:42 +09:00
{
2024-11-30 00:50:10 -05:00
firstSentence = NSLocalizedString ( " Non-developer Apple IDs are limited to 3 active apps and app extensions. " , comment : " " )
2024-08-16 12:57:42 +09:00
}
2024-11-30 00:50:10 -05:00
else
2024-08-16 12:57:42 +09:00
{
2024-11-30 00:50:10 -05:00
firstSentence = NSLocalizedString ( " Non-developer Apple IDs are limited to creating 10 App IDs per week. " , comment : " " )
2024-08-16 12:57:42 +09:00
}
2024-11-30 00:50:10 -05:00
let message = firstSentence + " " + NSLocalizedString ( " Would you like to remove this app's extensions so they don't count towards your limit? There are \( extensions . count ) Extensions " , comment : " " )
let alertController = UIAlertController ( title : NSLocalizedString ( " App Contains Extensions " , comment : " " ) , message : message , preferredStyle : . alert )
alertController . addAction ( UIAlertAction ( title : UIAlertAction . cancel . title , style : UIAlertAction . cancel . style , handler : { ( action ) in
completion ( . failure ( OperationError . cancelled ) )
} ) )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Keep App Extensions " , comment : " " ) , style : . default ) { ( action ) in
completion ( . success ( ( ) ) )
} )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Remove App Extensions " , comment : " " ) , style : . destructive ) { ( action ) in
2024-08-17 10:28:42 +09:00
do
{
2024-11-30 00:50:10 -05:00
for appExtension in application . appExtensions
2024-08-17 10:28:42 +09:00
{
print ( " Deleting extension \( appExtension . bundleIdentifier ) " )
try FileManager . default . removeItem ( at : appExtension . fileURL )
}
2024-11-30 00:50:10 -05:00
2024-08-17 10:28:42 +09:00
completion ( . success ( ( ) ) )
}
catch
{
completion ( . failure ( error ) )
}
2024-11-30 00:50:10 -05:00
} )
2024-08-17 10:28:42 +09:00
2024-11-30 00:50:10 -05:00
if let presentingViewController {
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Choose App Extensions " , comment : " " ) , style : . default ) { ( action ) in
let popoverContentController = AppExtensionViewHostingController ( extensions : extensions ) { ( selection ) in
do
{
for appExtension in selection
{
print ( " Deleting extension \( appExtension . bundleIdentifier ) " )
try FileManager . default . removeItem ( at : appExtension . fileURL )
}
completion ( . success ( ( ) ) )
}
catch
{
completion ( . failure ( error ) )
}
return nil
}
let suiview = popoverContentController . view !
suiview . translatesAutoresizingMaskIntoConstraints = false
popoverContentController . modalPresentationStyle = . popover
if let popoverPresentationController = popoverContentController . popoverPresentationController {
popoverPresentationController . sourceView = presentingViewController . view
popoverPresentationController . sourceRect = CGRect ( x : 50 , y : 50 , width : 4 , height : 4 )
popoverPresentationController . delegate = popoverContentController
presentingViewController . present ( popoverContentController , animated : true )
}
} )
2024-08-17 10:28:42 +09:00
2024-11-30 00:50:10 -05:00
presentingViewController . present ( alertController , animated : true )
2024-08-17 10:28:42 +09:00
}
2024-08-16 12:57:42 +09:00
}
}
2024-12-24 17:39:17 +09:00
private func _install ( _ app : AppProtocol , operation appOperation : AppOperation , group : RefreshGroup , context : InstallAppOperationContext ? = nil , additionalEntitlements : [ ALTEntitlement : Any ] = [ . increasedDebuggingMemoryLimit : ALTEntitlement . increasedDebuggingMemoryLimit , . increasedMemoryLimit : ALTEntitlement . increasedMemoryLimit , . extendedVirtualAddressing : ALTEntitlement . extendedVirtualAddressing ] , cacheApp : Bool = true , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress
2020-03-06 17:08:35 -08:00
{
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2020-05-16 16:17:18 -07:00
let context = context ? ? InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
assert ( context . authenticatedContext = = = group . context )
2020-03-31 14:31:34 -07:00
context . beginInstallationHandler = { ( installedApp ) in
2021-09-15 14:27:16 -07:00
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 )
default : break
}
group . beginInstallationHandler ? ( installedApp )
}
2020-03-06 17:08:35 -08:00
2020-05-08 11:43:34 -07:00
var downloadingApp = app
2020-10-01 14:09:45 -07:00
if let installedApp = app as ? InstalledApp
2020-05-08 11:43:34 -07:00
{
2020-10-01 14:09:45 -07:00
if let storeApp = installedApp . storeApp , ! FileManager . default . fileExists ( atPath : installedApp . fileURL . path )
{
// 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
}
if installedApp . hasAlternateIcon
{
context . alternateIconURL = installedApp . alternateIconURL
}
2020-05-08 11:43:34 -07:00
}
2020-05-15 14:55:15 -07:00
let downloadedAppURL = context . temporaryDirectory . appendingPathComponent ( " Cached.app " )
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 )
2020-03-06 17:08:35 -08:00
downloadOperation . resultHandler = { ( result ) in
2020-05-15 14:55:15 -07:00
do
2020-01-08 12:41:02 -08:00
{
2020-05-15 14:55:15 -07:00
let app = try result . get ( )
context . app = app
if cacheApp
{
try FileManager . default . copyItem ( at : app . fileURL , to : InstalledApp . fileURL ( for : app ) , shouldReplace : true )
}
}
catch
{
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 )
2020-01-08 12:41:02 -08:00
2020-05-16 16:17:18 -07:00
2020-05-02 22:06:57 -07:00
/* V e r i f y A p p */
let verifyOperation = VerifyAppOperation ( context : context )
verifyOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : context . error = error
case . success : break
}
}
verifyOperation . addDependency ( downloadOperation )
2020-03-06 17:08:35 -08:00
2024-08-16 12:57:42 +09:00
/* R e m o v e A p p E x t e n s i o n s */
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [ weak self ] ( operation ) in
do
{
if let error = context . error
{
throw error
}
2024-10-17 05:18:10 -04:00
/*
2024-08-17 00:16:48 +09:00
guard case . install = appOperation else {
operation . finish ( )
return
}
2024-10-17 05:18:10 -04:00
*/
2024-11-09 14:35:18 +05:30
guard let extensions = context . app ? . appExtensions else {
throw OperationError . invalidParameters ( " AppManager._install.removeAppExtensionsOperation: context.app?.appExtensions is nil " )
}
2024-08-16 12:57:42 +09:00
2024-11-10 23:31:31 +05:30
guard let currentApp = context . app else {
throw OperationError . invalidParameters ( " AppManager._install.removeAppExtensionsOperation: context.app is nil " )
2024-11-09 14:35:18 +05:30
}
2024-08-16 12:57:42 +09:00
2024-11-10 23:31:31 +05:30
self ? . removeAppExtensions ( from : currentApp ,
existingApp : app as ? InstalledApp ,
extensions : extensions ,
context . authenticatedContext . presentingViewController
) { result in
2024-08-16 12:57:42 +09:00
switch result {
case . success ( ) : break
case . failure ( let error ) : context . error = error
}
operation . finish ( )
}
}
catch
{
2024-11-10 16:41:44 +05:30
context . error = error
2024-08-16 12:57:42 +09:00
operation . finish ( )
}
}
removeAppExtensionsOperation . addDependency ( verifyOperation )
2020-05-16 16:17:18 -07:00
2023-10-17 17:34:12 +03: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 )
refreshAnisetteDataOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : context . error = error
case . success ( let anisetteData ) : group . context . session ? . anisetteData = anisetteData
}
}
2024-08-16 12:57:42 +09:00
refreshAnisetteDataOperation . addDependency ( removeAppExtensionsOperation )
2023-10-17 17:34:12 +03: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 )
fetchProvisioningProfilesOperation . additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : context . error = error
2024-08-06 10:43:52 +09:00
case . success ( let provisioningProfiles ) :
context . provisioningProfiles = provisioningProfiles
print ( " PROVISIONING PROFILES \( context . provisioningProfiles ) " )
2023-10-17 17:34:12 +03:00
}
}
fetchProvisioningProfilesOperation . addDependency ( refreshAnisetteDataOperation )
progress . addChild ( fetchProvisioningProfilesOperation . progress , withPendingUnitCount : 5 )
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 ) */
let deactivateAppsOperation = RSTAsyncBlockOperation { [ weak self ] ( operation ) in
do
{
// 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
}
if let error = context . error
{
throw error
}
2023-10-17 17:34:12 +03:00
2024-11-09 14:35:18 +05:30
guard let profiles = context . provisioningProfiles else {
throw OperationError . invalidParameters ( " AppManager._install.deactivateAppsOperation: context.provisioningProfiles is nil " )
}
2023-10-17 17:34:12 +03:00
if ! profiles . contains ( where : { $1 . isFreeProvisioningProfile = = true } ) {
operation . finish ( )
return
}
2021-09-15 14:27:16 -07:00
2024-11-09 14:35:18 +05:30
guard
let app = context . app ,
let presentingViewController = context . authenticatedContext . presentingViewController
else {
throw OperationError . invalidParameters ( " AppManager._install.deactivateAppsOperation: self.context.app or context.authenticatedContext.presentingViewController is nil " )
}
2021-09-15 14:27:16 -07:00
self ? . deactivateApps ( for : app , presentingViewController : presentingViewController ) { result in
switch result
{
case . failure ( let error ) : group . context . error = error
case . success : break
}
operation . finish ( )
}
}
catch
{
group . context . error = error
operation . finish ( )
}
}
2023-10-17 17:34:12 +03:00
deactivateAppsOperation . addDependency ( fetchProvisioningProfilesOperation )
2021-09-15 14:27:16 -07:00
2021-10-25 22:27:30 -07:00
/* P a t c h A p p */
let patchAppOperation = RSTAsyncBlockOperation { operation in
do
{
// 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
}
guard let presentingViewController = context . presentingViewController , #available ( iOS 14 , * ) else { return operation . finish ( ) }
if let error = context . error
{
throw error
}
2024-11-09 14:35:18 +05:30
guard let app = context . app else {
throw OperationError . invalidParameters ( " AppManager._install.patchAppOperation: context.app is nil " )
}
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 ( ) }
let iOSVersion = ProcessInfo . processInfo . operatingSystemVersion
let iOSVersionSupported = ProcessInfo . processInfo . isOperatingSystemAtLeast ( minimumiOSVersion ) &&
( ! ProcessInfo . processInfo . isOperatingSystemAtLeast ( maximumiOSVersion ) || maximumiOSVersion = = iOSVersion )
guard isUntetherRequired , iOSVersionSupported , UIDevice . current . supportsFugu14 else { return operation . finish ( ) }
guard let patchAppLink = app . bundle . infoDictionary ? [ Bundle . Info . untetherURL ] as ? String ,
let patchAppURL = URL ( string : patchAppLink )
else { throw OperationError . invalidApp }
let patchApp = AnyApp ( name : app . name , bundleIdentifier : app . bundleIdentifier , url : patchAppURL )
DispatchQueue . main . async {
let storyboard = UIStoryboard ( name : " PatchApp " , bundle : nil )
let navigationController = storyboard . instantiateInitialViewController ( ) as ! UINavigationController
let patchViewController = navigationController . topViewController as ! PatchViewController
patchViewController . patchApp = patchApp
patchViewController . completionHandler = { [ weak presentingViewController ] ( result ) in
switch result
{
case . failure ( OperationError . cancelled ) : break // I g n o r e
case . failure ( let error ) : group . context . error = error
case . success : group . context . error = OperationError . cancelled
}
operation . finish ( )
DispatchQueue . main . async {
presentingViewController ? . dismiss ( animated : true , completion : nil )
}
}
2022-09-22 13:53:28 -05:00
presentingViewController . present ( navigationController , animated : true , completion : nil )
2021-10-25 22:27:30 -07:00
}
}
catch
{
group . context . error = error
operation . finish ( )
}
}
patchAppOperation . addDependency ( deactivateAppsOperation )
2020-03-06 17:08:35 -08:00
/* R e s i g n */
let resignAppOperation = ResignAppOperation ( context : context )
resignAppOperation . resultHandler = { ( result ) in
switch result
2019-06-21 11:20:03 -07:00
{
2020-03-06 17:08:35 -08:00
case . failure ( let error ) : context . error = error
case . success ( let resignedApp ) : context . resignedApp = resignedApp
2019-06-21 11:20:03 -07:00
}
2020-03-06 17:08:35 -08:00
}
2023-10-17 17:34:12 +03:00
resignAppOperation . addDependency ( patchAppOperation )
2020-03-06 17:08:35 -08:00
progress . addChild ( resignAppOperation . progress , withPendingUnitCount : 20 )
/* S e n d */
let sendAppOperation = SendAppOperation ( context : context )
sendAppOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : context . error = error
2022-11-02 17:58:59 -07:00
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 )
/* I n s t a l l */
let installOperation = InstallAppOperation ( context : context )
installOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : completionHandler ( . failure ( error ) )
case . success ( let installedApp ) :
2020-05-16 16:17:18 -07:00
context . installedApp = installedApp
2020-03-06 17:08:35 -08:00
if let app = app as ? StoreApp , let storeApp = installedApp . managedObjectContext ? . object ( with : app . objectID ) as ? StoreApp
2019-07-19 16:09:45 -07:00
{
2020-03-06 17:08:35 -08:00
installedApp . storeApp = storeApp
2019-07-19 16:09:45 -07:00
}
2020-03-06 17:08:35 -08:00
if let index = UserDefaults . standard . legacySideloadedApps ? . firstIndex ( of : installedApp . bundleIdentifier )
2019-07-28 15:08:13 -07:00
{
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
}
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 )
2020-02-10 17:30:11 -08:00
2024-08-16 12:57:42 +09:00
let operations = [ downloadOperation , verifyOperation , removeAppExtensionsOperation , refreshAnisetteDataOperation , fetchProvisioningProfilesOperation , deactivateAppsOperation , patchAppOperation , resignAppOperation , sendAppOperation , installOperation ]
2020-03-06 17:08:35 -08:00
group . add ( operations )
2020-03-30 15:23:20 -07:00
self . run ( operations , context : group . context )
2019-06-10 15:03:47 -07:00
2020-03-06 17:08:35 -08:00
return progress
2019-06-04 18:29:50 -07:00
}
2019-06-18 18:31:59 -07:00
2020-03-31 14:31:34 -07:00
private func _refresh ( _ app : InstalledApp , operation : AppOperation , group : RefreshGroup , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress
2019-05-31 18:24:08 -07:00
{
2020-03-06 17:08:35 -08:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
let context = AppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
2022-09-08 15:59:24 -05:00
context . app = ALTApplication ( fileURL : app . fileURL )
2020-05-16 16:17:18 -07:00
2024-11-28 18:36:17 +05:30
// A p p - E x t e n s i o n s : E n s u r e D B d a t a a n d d i s k s t a t e m u s t m a t c h
let dbAppEx : Set < InstalledExtension > = Set ( app . appExtensions )
let diskAppEx : Set < ALTApplication > = Set ( context . app ! . appExtensions )
let diskAppExNames = diskAppEx . map { $0 . bundleIdentifier }
let dbAppExNames = dbAppEx . map { $0 . bundleIdentifier }
let isMatching = Set ( dbAppExNames ) = = Set ( diskAppExNames )
let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
2024-11-04 04:00:39 +05:30
2024-11-04 14:35:13 +05:30
let errMessage = " AppManager.refresh: App Extensions in DB and Disk are matching: \( isMatching ) \n "
+ " AppManager.refresh: dbAppEx: \( dbAppExNames ) ; diskAppEx: \( String ( describing : diskAppExNames ) ) \n "
print ( errMessage )
2024-11-04 04:00:39 +05:30
if ( ! isMatching ) {
2024-11-04 14:35:13 +05:30
completionHandler ( . failure ( OperationError . refreshAppFailed ( message : errMessage ) ) )
2024-11-04 04:00:39 +05:30
}
op . finish ( )
}
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 )
fetchProvisioningProfilesOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : context . error = error
case . success ( let provisioningProfiles ) : context . provisioningProfiles = provisioningProfiles
}
2019-07-16 14:22:45 -07:00
}
2020-03-06 17:08:35 -08:00
progress . addChild ( fetchProvisioningProfilesOperation . progress , withPendingUnitCount : 60 )
2024-11-04 04:00:39 +05:30
fetchProvisioningProfilesOperation . addDependency ( validateAppExtensionsOperation )
2020-05-16 16:17:18 -07:00
2020-03-06 17:08:35 -08:00
/* R e f r e s h */
let refreshAppOperation = RefreshAppOperation ( context : context )
refreshAppOperation . resultHandler = { ( result ) in
2020-03-11 14:43:19 -07:00
switch result
{
case . success ( let installedApp ) :
completionHandler ( . success ( installedApp ) )
2024-08-06 10:43:52 +09:00
case . failure ( MinimuxerError . ProfileInstall ) :
completionHandler ( . failure ( OperationError . noWiFi ) )
case . failure ( ALTServerError . unknownRequest ) , . failure ( OperationError . appNotFound ( name : app . name ) ) :
2020-05-08 11:43:34 -07:00
// 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 .
2024-08-06 10:43:52 +09:00
if minimuxer . ready ( ) {
let installProgress = self . _install ( app , operation : operation , group : group ) { ( result ) in
completionHandler ( result )
}
progress . addChild ( installProgress , withPendingUnitCount : 40 )
} else {
completionHandler ( . failure ( OperationError . noWiFi ) )
2020-03-11 14:43:19 -07:00
}
}
case . failure ( let error ) :
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 )
2024-11-04 04:00:39 +05:30
let operations = [ validateAppExtensionsOperation , fetchProvisioningProfilesOperation , refreshAppOperation ]
2020-03-06 17:08:35 -08:00
group . add ( operations )
2020-03-30 15:23:20 -07:00
self . run ( operations , context : group . context )
2020-05-16 16:17:18 -07:00
return progress
}
2020-05-16 16:29:28 -07:00
private func _activate ( _ app : InstalledApp , operation appOperation : AppOperation , group : RefreshGroup , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress
{
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
let restoreContext = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
let appContext = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
let installBackupAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
let installBackupAppOperation = RSTAsyncBlockOperation { [ weak self ] ( operation ) in
app . managedObjectContext ? . perform {
guard let self = self else { return }
let progress = self . _installBackupApp ( for : app , operation : appOperation , group : group , context : restoreContext ) { ( result ) in
switch result
{
case . success ( let installedApp ) : restoreContext . installedApp = installedApp
case . failure ( let error ) :
restoreContext . error = error
appContext . error = error
}
operation . finish ( )
}
installBackupAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
progress . addChild ( installBackupAppProgress , withPendingUnitCount : 30 )
let restoreAppOperation = BackupAppOperation ( action : . restore , context : restoreContext )
restoreAppOperation . resultHandler = { ( result ) in
switch result
{
case . success : break
case . failure ( let error ) :
restoreContext . error = error
appContext . error = error
}
}
restoreAppOperation . addDependency ( installBackupAppOperation )
progress . addChild ( restoreAppOperation . progress , withPendingUnitCount : 15 )
let installAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
let installAppOperation = RSTAsyncBlockOperation { [ weak self ] ( operation ) in
app . managedObjectContext ? . perform {
guard let self = self else { return }
let progress = self . _install ( app , operation : appOperation , group : group , context : appContext ) { ( result ) in
switch result
{
case . success ( let installedApp ) : appContext . installedApp = installedApp
case . failure ( let error ) : appContext . error = error
}
operation . finish ( )
}
installAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
installAppOperation . addDependency ( restoreAppOperation )
progress . addChild ( installAppProgress , withPendingUnitCount : 50 )
let cleanUpProgress = Progress . discreteProgress ( totalUnitCount : 100 )
let cleanUpOperation = RSTAsyncBlockOperation { ( operation ) in
do
{
let installedApp = try Result ( appContext . installedApp , appContext . error ) . get ( )
var result : Result < Void , Error > !
installedApp . managedObjectContext ? . performAndWait {
result = Result { try installedApp . managedObjectContext ? . save ( ) }
}
try result . get ( )
// 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 .
let removeAppBackupOperation = RemoveAppBackupOperation ( context : appContext )
removeAppBackupOperation . resultHandler = { ( result ) in
installedApp . managedObjectContext ? . perform {
switch result
{
case . failure ( let error ) :
// 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 .
print ( " Failed to delete app backup. " , error )
case . success : break
}
completionHandler ( . success ( installedApp ) )
operation . finish ( )
}
}
cleanUpProgress . addChild ( removeAppBackupOperation . progress , withPendingUnitCount : 100 )
group . add ( [ removeAppBackupOperation ] )
self . run ( [ removeAppBackupOperation ] , context : group . context )
}
catch let error where restoreContext . installedApp != nil
{
// 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 .
// 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
let removeAppOperation = RemoveAppOperation ( context : restoreContext )
removeAppOperation . resultHandler = { ( result ) in
completionHandler ( . failure ( error ) )
operation . finish ( )
}
cleanUpProgress . addChild ( removeAppOperation . progress , withPendingUnitCount : 100 )
group . add ( [ removeAppOperation ] )
self . run ( [ removeAppOperation ] , context : group . context )
}
catch
{
// 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 )
group . add ( [ installBackupAppOperation , restoreAppOperation , installAppOperation , cleanUpOperation ] )
self . run ( [ installBackupAppOperation , installAppOperation , restoreAppOperation , cleanUpOperation ] , context : group . context )
return progress
}
2020-05-16 16:17:18 -07:00
private func _deactivate ( _ app : InstalledApp , operation appOperation : AppOperation , group : RefreshGroup , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress
{
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
let context = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
let installBackupAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
let installBackupAppOperation = RSTAsyncBlockOperation { [ weak self ] ( operation ) in
app . managedObjectContext ? . perform {
guard let self = self else { return }
let progress = self . _installBackupApp ( for : app , operation : appOperation , group : group , context : context ) { ( result ) in
switch result
{
case . success ( let installedApp ) : context . installedApp = installedApp
case . failure ( let error ) : context . error = error
}
operation . finish ( )
}
installBackupAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
progress . addChild ( installBackupAppProgress , withPendingUnitCount : 70 )
let backupAppOperation = BackupAppOperation ( action : . backup , context : context )
backupAppOperation . resultHandler = { ( result ) in
switch result
{
case . failure ( let error ) : context . error = error
case . success : break
}
}
backupAppOperation . addDependency ( installBackupAppOperation )
progress . addChild ( backupAppOperation . progress , withPendingUnitCount : 15 )
let removeAppOperation = RemoveAppOperation ( context : context )
removeAppOperation . resultHandler = { ( result ) in
completionHandler ( result )
}
removeAppOperation . addDependency ( backupAppOperation )
progress . addChild ( removeAppOperation . progress , withPendingUnitCount : 15 )
group . add ( [ installBackupAppOperation , backupAppOperation , removeAppOperation ] )
self . run ( [ installBackupAppOperation , backupAppOperation , removeAppOperation ] , context : group . context )
return progress
}
2020-05-19 11:47:43 -07:00
private func _backup ( _ app : InstalledApp , operation appOperation : AppOperation , group : RefreshGroup , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress
{
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
let restoreContext = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
let appContext = InstallAppOperationContext ( bundleIdentifier : app . bundleIdentifier , authenticatedContext : group . context )
let installBackupAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
let installBackupAppOperation = RSTAsyncBlockOperation { [ weak self ] ( operation ) in
app . managedObjectContext ? . perform {
guard let self = self else { return }
let progress = self . _installBackupApp ( for : app , operation : appOperation , group : group , context : restoreContext ) { ( result ) in
switch result
{
case . success ( let installedApp ) : restoreContext . installedApp = installedApp
case . failure ( let error ) :
restoreContext . error = error
appContext . error = error
}
operation . finish ( )
}
installBackupAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
progress . addChild ( installBackupAppProgress , withPendingUnitCount : 30 )
let backupAppOperation = BackupAppOperation ( action : . backup , context : restoreContext )
backupAppOperation . resultHandler = { ( result ) in
switch result
{
case . success : break
case . failure ( let error ) :
restoreContext . error = error
appContext . error = error
}
}
backupAppOperation . addDependency ( installBackupAppOperation )
progress . addChild ( backupAppOperation . progress , withPendingUnitCount : 15 )
let installAppProgress = Progress . discreteProgress ( totalUnitCount : 100 )
let installAppOperation = RSTAsyncBlockOperation { [ weak self ] ( operation ) in
app . managedObjectContext ? . perform {
guard let self = self else { return }
let progress = self . _install ( app , operation : appOperation , group : group , context : appContext ) { ( result ) in
completionHandler ( result )
operation . finish ( )
}
installAppProgress . addChild ( progress , withPendingUnitCount : 100 )
}
}
installAppOperation . addDependency ( backupAppOperation )
progress . addChild ( installAppProgress , withPendingUnitCount : 55 )
group . add ( [ installBackupAppOperation , backupAppOperation , installAppOperation ] )
self . run ( [ installBackupAppOperation , installAppOperation , backupAppOperation ] , context : group . context )
return progress
}
2020-05-16 16:17:18 -07:00
private func _installBackupApp ( for app : InstalledApp , operation appOperation : AppOperation , group : RefreshGroup , context : InstallAppOperationContext , completionHandler : @ escaping ( Result < InstalledApp , Error > ) -> Void ) -> Progress
{
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2021-09-15 14:29:37 -07:00
if let error = context . error
{
completionHandler ( . failure ( error ) )
return progress
}
2020-05-16 16:17:18 -07:00
guard let application = ALTApplication ( fileURL : app . fileURL ) else {
2024-08-06 10:43:52 +09:00
completionHandler ( . failure ( OperationError . appNotFound ( name : app . name ) ) )
2020-05-16 16:17:18 -07:00
return progress
}
let prepareProgress = Progress . discreteProgress ( totalUnitCount : 1 )
let prepareOperation = RSTAsyncBlockOperation { ( operation ) in
app . managedObjectContext ? . perform {
do
{
let temporaryDirectoryURL = context . temporaryDirectory . appendingPathComponent ( " AltBackup- " + UUID ( ) . uuidString )
try FileManager . default . createDirectory ( at : temporaryDirectoryURL , withIntermediateDirectories : true , attributes : nil )
2024-08-06 10:43:52 +09:00
guard let altbackupFileURL = Bundle . main . url ( forResource : " AltBackup " , withExtension : " ipa " ) else { throw OperationError . appNotFound ( name : app . name ) }
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 }
if var infoDictionary = unzippedAppBundle . infoDictionary
{
// 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
// 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 ,
" UTTypeTagSpecification " : [ : ] ] as [ String : Any ]
var exportedUTIs = infoDictionary [ Bundle . Info . exportedUTIs ] as ? [ [ String : Any ] ] ? ? [ ]
exportedUTIs . append ( installedAppUTI )
infoDictionary [ Bundle . Info . exportedUTIs ] = exportedUTIs
2020-05-17 22:22:25 -07:00
if let cachedApp = ALTApplication ( fileURL : app . fileURL ) , let icon = cachedApp . icon ? . resizing ( to : CGSize ( width : 180 , height : 180 ) )
{
let iconFileURL = unzippedAppBundleURL . appendingPathComponent ( " AppIcon.png " )
if let iconData = icon . pngData ( )
{
do
{
try iconData . write ( to : iconFileURL , options : . atomic )
let bundleIcons = [ " CFBundlePrimaryIcon " : [ " CFBundleIconFiles " : [ iconFileURL . lastPathComponent ] ] ]
infoDictionary [ " CFBundleIcons " ] = bundleIcons
}
catch
{
print ( " Failed to write app icon data. " , error )
}
}
}
2020-05-16 16:17:18 -07:00
try ( infoDictionary as NSDictionary ) . write ( to : unzippedAppBundle . infoPlistURL )
}
guard let backupApp = ALTApplication ( fileURL : unzippedAppBundleURL ) else { throw OperationError . invalidApp }
context . app = backupApp
prepareProgress . completedUnitCount += 1
}
catch
{
print ( error )
2021-09-15 14:29:37 -07:00
context . error = error
2020-05-16 16:17:18 -07:00
}
operation . finish ( )
}
}
progress . addChild ( prepareProgress , withPendingUnitCount : 20 )
let installProgress = Progress . discreteProgress ( totalUnitCount : 100 )
let installOperation = RSTAsyncBlockOperation { [ weak self ] ( operation ) in
guard let self = self else { return }
guard let backupApp = context . app else {
context . error = OperationError . invalidApp
operation . finish ( )
return
}
var appGroups = application . entitlements [ . appGroups ] as ? [ String ] ? ? [ ]
appGroups . append ( Bundle . baseAltStoreAppGroupID )
let additionalEntitlements : [ ALTEntitlement : Any ] = [ . appGroups : appGroups ]
let progress = self . _install ( backupApp , operation : appOperation , group : group , context : context , additionalEntitlements : additionalEntitlements , cacheApp : false ) { ( result ) in
completionHandler ( result )
operation . finish ( )
}
installProgress . addChild ( progress , withPendingUnitCount : 100 )
}
installOperation . addDependency ( prepareOperation )
progress . addChild ( installProgress , withPendingUnitCount : 80 )
group . add ( [ prepareOperation , installOperation ] )
self . run ( [ prepareOperation , installOperation ] , context : group . context )
2020-03-06 17:08:35 -08:00
return progress
2019-06-21 11:20:03 -07:00
}
2020-03-06 17:08:35 -08:00
func finish ( _ operation : AppOperation , result : Result < InstalledApp , Error > , group : RefreshGroup , progress : Progress ? )
2019-06-21 11:20:03 -07:00
{
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 .
if let currentProgress = self . progress ( for : operation ) , currentProgress = = progress
{
// 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 .
self . set ( nil , for : operation )
}
do
{
let installedApp = try result . get ( )
group . set ( . success ( installedApp ) , forAppWithBundleIdentifier : installedApp . bundleIdentifier )
2019-07-19 16:10:30 -07:00
2020-03-06 17:08:35 -08:00
if installedApp . bundleIdentifier = = StoreApp . altstoreAppID
2019-06-21 11:20:03 -07:00
{
2020-03-06 17:08:35 -08:00
self . scheduleExpirationWarningLocalNotification ( for : installedApp )
2019-06-21 11:20:03 -07:00
}
2020-03-06 17:08:35 -08:00
2020-03-31 14:31:34 -07:00
let event : AnalyticsManager . Event ?
switch operation
{
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
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
}
if let event = event
{
AnalyticsManager . shared . trackEvent ( event )
}
2020-09-15 13:51:29 -07:00
if #available ( iOS 14 , * )
2022-09-22 13:53:28 -05:00
{
WidgetCenter . shared . reloadAllTimelines ( )
2020-09-15 13:51:29 -07:00
}
2020-03-06 17:08:35 -08:00
do { try installedApp . managedObjectContext ? . save ( ) }
catch { print ( " Error saving installed app. " , error ) }
}
2024-08-06 10:43:52 +09:00
catch let nsError as NSError
2020-03-06 17:08:35 -08:00
{
2024-08-06 10:43:52 +09:00
var appName : String !
if let app = operation . app as ? ( NSManagedObject & AppProtocol ) {
if let context = app . managedObjectContext {
context . performAndWait {
appName = app . name
}
} else {
appName = NSLocalizedString ( " Unknown App " , comment : " " )
}
} else {
appName = operation . app . name
}
let localizedTitle : String
switch operation {
case . install : localizedTitle = String ( format : NSLocalizedString ( " Failed to Install %@ " , comment : " " ) , appName )
case . refresh : localizedTitle = String ( format : NSLocalizedString ( " Failed to Refresh %@ " , comment : " " ) , appName )
case . update : localizedTitle = String ( format : NSLocalizedString ( " Failed to Update %@ " , comment : " " ) , appName )
case . activate : localizedTitle = String ( format : NSLocalizedString ( " Failed to Activate %@ " , comment : " " ) , appName )
case . deactivate : localizedTitle = String ( format : NSLocalizedString ( " Failed to Deactivate %@ " , comment : " " ) , appName )
case . backup : localizedTitle = String ( format : NSLocalizedString ( " Failed to Backup %@ " , comment : " " ) , appName )
case . restore : localizedTitle = String ( format : NSLocalizedString ( " Failed to Restore %@ Backup " , comment : " " ) , appName )
}
let error = nsError . withLocalizedTitle ( localizedTitle )
2020-03-06 17:08:35 -08:00
group . set ( . failure ( error ) , forAppWithBundleIdentifier : operation . bundleIdentifier )
2022-09-09 17:44:15 -05:00
2024-08-06 10:43:52 +09:00
self . log ( error , operation : operation . loggedErrorOperation , app : operation . app )
2019-06-04 18:29:50 -07:00
}
}
2019-09-21 22:31:10 -07:00
func scheduleExpirationWarningLocalNotification ( for app : InstalledApp )
{
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 .
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
}
let trigger = UNTimeIntervalNotificationTrigger ( timeInterval : timeIntervalUntilNotification , repeats : false )
let content = UNMutableNotificationContent ( )
2024-07-17 16:21:39 -07:00
content . title = NSLocalizedString ( " SideStore Expiring Soon " , comment : " " )
content . body = NSLocalizedString ( " SideStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring. " , comment : " " )
2019-09-21 22:31:10 -07:00
content . sound = . default
let request = UNNotificationRequest ( identifier : AppManager . expirationWarningNotificationID , content : content , trigger : trigger )
UNUserNotificationCenter . current ( ) . add ( request )
}
2020-03-06 17:08:35 -08:00
2023-01-24 14:07:30 -06:00
func log ( _ error : Error , operation : LoggedError . Operation , app : AppProtocol )
2024-11-10 02:54:18 +05:30
{
2022-11-14 15:35:57 -06:00
switch error
{
case ~ OperationError . Code . cancelled : return // D o n ' t l o g O p e r a t i o n E r r o r . c a n c e l l e d
default : break
}
2024-11-10 02:54:18 +05:30
// 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 ) . sanitizedForSerialization ( )
let loggedErrorOperation : LoggedError . Operation = {
switch operation
{
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-01-24 14:07:30 -06:00
2024-11-10 02:54:18 +05:30
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
2023-01-24 14:07:30 -06:00
var app = app
2024-11-10 02:54:18 +05:30
if let managedApp = app as ? NSManagedObject , let tempApp = context . object ( with : managedApp . objectID ) as ? AppProtocol
{
app = tempApp
}
do
{
2023-01-24 14:07:30 -06:00
_ = LoggedError ( error : sanitizedError , app : app , operation : operation , context : context )
2024-11-10 02:54:18 +05:30
try context . save ( )
}
catch let saveError
{
print ( " [ALTLog] Failed to log error \( sanitizedError . domain ) code \( sanitizedError . code ) for \( app . bundleIdentifier ) : " , saveError )
}
}
}
2020-03-30 15:23:20 -07:00
func run ( _ operations : [ Foundation . Operation ] , context : OperationContext ? , requiresSerialQueue : Bool = false )
2020-03-06 17:08:35 -08:00
{
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 }
2021-03-01 12:45:57 -06:00
2020-03-06 17:08:35 -08:00
for operation in operations
{
switch operation
{
case _ where requiresSerialQueue : fallthrough
2021-03-01 12:45:57 -06:00
case is InstallAppOperation , is RefreshAppOperation , is BackupAppOperation :
2021-03-09 13:13:23 -06:00
if let installAltStoreOperation = operation as ? InstallAppOperation , installAltStoreOperation . context . bundleIdentifier = = StoreApp . altstoreAppID
2021-03-01 12:45:57 -06:00
{
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 ) }
}
else if let installAltStoreOperation = installAltStoreOperation
{
// 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
}
self . serialOperationQueue . addOperation ( operation )
2020-05-16 16:17:18 -07:00
default : self . operationQueue . addOperation ( operation )
2020-03-06 17:08:35 -08:00
}
2020-03-30 15:23:20 -07:00
context ? . operations . add ( operation )
2020-03-06 17:08:35 -08:00
}
}
func progress ( for operation : AppOperation ) -> Progress ?
{
2022-11-21 17:04:50 -06:00
os_unfair_lock_lock ( self . progressLock )
defer { os_unfair_lock_unlock ( self . progressLock ) }
2020-03-06 17:08:35 -08:00
switch operation
{
2020-03-31 14:31:34 -07:00
case . install , . update : return self . installationProgress [ operation . bundleIdentifier ]
2020-05-19 11:47:43 -07:00
case . refresh , . activate , . deactivate , . backup , . restore : return self . refreshProgress [ operation . bundleIdentifier ]
2020-03-06 17:08:35 -08:00
}
}
func set ( _ progress : Progress ? , for operation : AppOperation )
{
2022-11-21 17:04:50 -06:00
os_unfair_lock_lock ( self . progressLock )
defer { os_unfair_lock_unlock ( self . progressLock ) }
2020-03-06 17:08:35 -08:00
switch operation
{
2020-03-31 14:31:34 -07:00
case . install , . update : self . installationProgress [ operation . bundleIdentifier ] = progress
2020-05-19 11:47:43 -07:00
case . refresh , . activate , . deactivate , . backup , . restore : self . refreshProgress [ operation . bundleIdentifier ] = progress
2020-03-06 17:08:35 -08:00
}
}
2019-05-31 18:24:08 -07:00
}