2019-06-10 15:03:47 -07:00
//
// I n s t a l l A p p O p e r a t i o n . s w i f t
// A l t S t o r e
//
2019-06-21 11:20:03 -07:00
// C r e a t e d b y R i l e y T e s t u t o n 6 / 1 9 / 1 9 .
2019-06-10 15:03:47 -07:00
// 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 Network
2020-09-03 16:39:08 -07:00
import AltStoreCore
2019-07-28 15:08:13 -07:00
import AltSign
2019-06-21 11:20:03 -07:00
import Roxas
2023-04-01 16:02:12 -07:00
import minimuxer
2019-06-10 15:03:47 -07:00
@objc ( InstallAppOperation )
2023-01-04 09:52:12 -05:00
final class InstallAppOperation : ResultOperation < InstalledApp >
2019-06-10 15:03:47 -07:00
{
2020-03-06 17:08:35 -08:00
let context : InstallAppOperationContext
2019-06-10 15:03:47 -07:00
2019-12-11 12:26:48 -08:00
private var didCleanUp = false
2020-03-06 17:08:35 -08:00
init ( context : InstallAppOperationContext )
2019-06-10 15:03:47 -07:00
{
2019-06-21 11:20:03 -07:00
self . context = context
2019-06-10 15:03:47 -07:00
super . init ( )
2019-06-21 11:20:03 -07:00
self . progress . totalUnitCount = 100
2019-06-10 15:03:47 -07:00
}
override func main ( )
{
super . main ( )
2019-06-21 11:20:03 -07:00
if let error = self . context . error
{
self . finish ( . failure ( error ) )
return
}
2019-06-10 15:03:47 -07:00
2019-06-21 11:20:03 -07:00
guard
2020-03-06 17:08:35 -08:00
let certificate = self . context . certificate ,
2023-10-17 17:34:12 +03:00
let resignedApp = self . context . resignedApp ,
let provisioningProfiles = self . context . provisioningProfiles
2024-11-09 14:35:18 +05:30
else {
return self . finish ( . failure ( OperationError . invalidParameters ( " InstallAppOperation.main: self.context.certificate or self.context.resignedApp or self.context.provisioningProfiles is nil " ) ) )
}
2024-12-07 17:45:09 +05:30
2023-05-26 19:12:13 -05:00
@ Managed var appVersion = self . context . appVersion
let storeBuildVersion = $ appVersion . buildVersion
2019-07-28 15:08:13 -07:00
let backgroundContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( )
backgroundContext . perform {
2023-11-28 00:44:47 +09:00
2020-01-24 15:03:16 -08:00
/* A p p */
2019-07-28 15:08:13 -07:00
let installedApp : InstalledApp
// F e t c h + u p d a t e r a t h e r t h a n i n s e r t + r e s o l v e m e r g e c o n f l i c t s t o p r e v e n t p o t e n t i a l c o n t e x t - l e v e l c o n f l i c t s .
if let app = InstalledApp . first ( satisfying : NSPredicate ( format : " %K == %@ " , # keyPath ( InstalledApp . bundleIdentifier ) , self . context . bundleIdentifier ) , in : backgroundContext )
2019-06-10 15:03:47 -07:00
{
2019-07-28 15:08:13 -07:00
installedApp = app
}
else
{
2023-05-26 19:12:13 -05:00
installedApp = InstalledApp ( resignedApp : resignedApp ,
originalBundleIdentifier : self . context . bundleIdentifier ,
certificateSerialNumber : certificate . serialNumber ,
storeBuildVersion : storeBuildVersion ,
context : backgroundContext )
2019-07-28 15:08:13 -07:00
}
2023-05-26 19:12:13 -05:00
installedApp . update ( resignedApp : resignedApp , certificateSerialNumber : certificate . serialNumber , storeBuildVersion : storeBuildVersion )
2020-10-01 11:51:39 -07:00
installedApp . needsResign = false
2020-01-24 15:03:16 -08:00
if let team = DatabaseManager . shared . activeTeam ( in : backgroundContext )
{
installedApp . team = team
}
/* A p p E x t e n s i o n s */
2020-01-21 16:53:34 -08:00
var installedExtensions = Set < InstalledExtension > ( )
if
let bundle = Bundle ( url : resignedApp . fileURL ) ,
let directory = bundle . builtInPlugInsURL ,
let enumerator = FileManager . default . enumerator ( at : directory , includingPropertiesForKeys : nil , options : [ . skipsSubdirectoryDescendants ] )
{
for case let fileURL as URL in enumerator
{
guard let appExtensionBundle = Bundle ( url : fileURL ) else { continue }
guard let appExtension = ALTApplication ( fileURL : appExtensionBundle . bundleURL ) else { continue }
2020-01-24 15:03:16 -08:00
let parentBundleID = self . context . bundleIdentifier
let resignedParentBundleID = resignedApp . bundleIdentifier
let resignedBundleID = appExtension . bundleIdentifier
let originalBundleID = resignedBundleID . replacingOccurrences ( of : resignedParentBundleID , with : parentBundleID )
2022-12-03 17:25:15 -05:00
print ( " `parentBundleID`: \( parentBundleID ) " )
print ( " `resignedParentBundleID`: \( resignedParentBundleID ) " )
print ( " `resignedBundleID`: \( resignedBundleID ) " )
print ( " `originalBundleID`: \( originalBundleID ) " )
2020-01-21 16:53:34 -08:00
let installedExtension : InstalledExtension
2020-01-24 15:03:16 -08:00
if let appExtension = installedApp . appExtensions . first ( where : { $0 . bundleIdentifier = = originalBundleID } )
2020-01-21 16:53:34 -08:00
{
installedExtension = appExtension
}
else
{
2020-01-24 15:03:16 -08:00
installedExtension = InstalledExtension ( resignedAppExtension : appExtension , originalBundleIdentifier : originalBundleID , context : backgroundContext )
2020-01-21 16:53:34 -08:00
}
2020-01-24 15:03:16 -08:00
installedExtension . update ( resignedAppExtension : appExtension )
2020-01-21 16:53:34 -08:00
installedExtensions . insert ( installedExtension )
}
}
installedApp . appExtensions = installedExtensions
2024-11-04 04:00:39 +05:30
// R e m o v e s t a l e " P l u g I n s " ( E x t e n s i o n s ) f r o m c u r r e n t l y i n s t a l l e d A p p
if let installedAppExns = ALTApplication ( fileURL : installedApp . fileURL ) ? . appExtensions {
let currentAppExns = Set ( installedApp . appExtensions ) . map { $0 . bundleIdentifier }
let staleAppExns = installedAppExns . filter { ! currentAppExns . contains ( $0 . bundleIdentifier ) }
for staleAppExn in staleAppExns {
do {
try FileManager . default . removeItem ( at : staleAppExn . fileURL )
print ( " InstallAppOperation.appExtensions: removed stale app-extension: \( staleAppExn . fileURL ) " )
} catch {
print ( " InstallAppOperation.appExtensions processing error Error: \( error ) " )
}
}
}
2020-01-21 16:53:34 -08:00
2021-10-25 22:27:30 -07:00
self . context . beginInstallationHandler ? ( installedApp )
2019-12-11 12:26:48 -08:00
// T e m p o r a r y d i r e c t o r y a n d r e s i g n e d . i p a n o l o n g e r n e e d e d , s o d e l e t e t h e m n o w t o e n s u r e A l t S t o r e d o e s n ' t q u i t b e f o r e w e g e t t h e c h a n c e t o .
self . cleanUp ( )
2023-10-17 17:34:12 +03:00
if let sideloadedAppsLimit = UserDefaults . standard . activeAppsLimit , provisioningProfiles . contains ( where : { $1 . isFreeProvisioningProfile = = true } )
2020-03-11 14:43:19 -07:00
{
// W h e n i n s t a l l i n g t h e s e n e w p r o f i l e s , A l t S e r v e r w i l l r e m o v e a l l n o n - a c t i v e p r o f i l e s t o e n s u r e w e r e m a i n u n d e r l i m i t .
let fetchRequest = InstalledApp . activeAppsFetchRequest ( )
fetchRequest . includesPendingChanges = false
var activeApps = InstalledApp . fetch ( fetchRequest , in : backgroundContext )
if ! activeApps . contains ( installedApp )
{
2020-05-17 23:36:30 -07:00
let activeAppsCount = activeApps . map { $0 . requiredActiveSlots } . reduce ( 0 , + )
2020-03-11 14:43:19 -07:00
2020-05-17 23:36:30 -07:00
let availableActiveApps = max ( sideloadedAppsLimit - activeAppsCount , 0 )
if installedApp . requiredActiveSlots <= availableActiveApps
2020-03-11 14:43:19 -07:00
{
// T h i s a p p h a s n o t b e e n e x p l i c i t l y a c t i v a t e d , b u t t h e r e a r e e n o u g h s l o t s a v a i l a b l e ,
// s o i m p l i c i t l y a c t i v a t e i t .
installedApp . isActive = true
activeApps . append ( installedApp )
}
else
{
installedApp . isActive = false
}
}
2023-10-17 17:34:12 +03:00
}
else
{
installedApp . isActive = true
2020-03-11 14:43:19 -07:00
}
2023-04-13 07:20:36 -07:00
var installing = true
2023-07-24 16:58:09 -04:00
if installedApp . storeApp ? . bundleIdentifier . range ( of : Bundle . Info . appbundleIdentifier ) != nil {
2023-05-06 19:25:37 -07:00
// R e i n s t a l l i n g o u r s e l f w i l l h a n g u n t i l w e l e a v e t h e a p p , s o w e n e e d t o e x i t i t w i t h o u t f o r c e c l o s i n g
2023-04-13 07:20:36 -07:00
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 3 ) {
2023-05-06 19:25:37 -07:00
if UIApplication . shared . applicationState != . active {
print ( " We are not in the foreground, let's not do anything " )
return
}
if ! installing {
2023-04-13 07:20:36 -07:00
print ( " Installing finished " )
2023-05-06 19:25:37 -07:00
return
}
print ( " We are still installing after 3 seconds " )
2023-07-27 05:10:27 -04:00
UNUserNotificationCenter . current ( ) . getNotificationSettings { settings in
switch ( settings . authorizationStatus ) {
case . authorized , . ephemeral , . provisional :
print ( " Notifications are enabled " )
let content = UNMutableNotificationContent ( )
content . title = " Refreshing... "
2023-07-27 23:42:32 -07:00
content . body = " SideStore will automatically move to the homescreen to finish refreshing! "
2023-07-27 05:10:27 -04:00
let notification = UNNotificationRequest ( identifier : Bundle . Info . appbundleIdentifier + " .FinishRefreshNotification " , content : content , trigger : UNTimeIntervalNotificationTrigger ( timeInterval : 2 , repeats : false ) )
UNUserNotificationCenter . current ( ) . add ( notification )
break
default :
print ( " Notifications are not enabled " )
2023-07-27 23:42:32 -07:00
let alert = UIAlertController ( title : " Finish Refresh " , message : " Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this. " , preferredStyle : . alert )
2023-07-27 05:10:27 -04:00
alert . addAction ( UIAlertAction ( title : NSLocalizedString ( " Continue " , comment : " " ) , style : . default , handler : { _ in
print ( " Going home " )
UIApplication . shared . perform ( #selector ( NSXPCConnection . suspend ) )
} ) )
DispatchQueue . main . async {
let keyWindow = UIApplication . shared . windows . filter { $0 . isKeyWindow } . first
if var topController = keyWindow ? . rootViewController {
while let presentedViewController = topController . presentedViewController {
topController = presentedViewController
}
topController . present ( alert , animated : true )
} else {
print ( " No key window? Let's just go home " )
}
}
}
}
2023-07-27 06:06:09 -04:00
UIApplication . shared . perform ( #selector ( NSXPCConnection . suspend ) )
2023-04-13 07:20:36 -07:00
}
}
2023-09-17 10:37:49 -07:00
do {
try install_ipa ( installedApp . bundleIdentifier )
installing = false
installedApp . refreshedDate = Date ( )
self . finish ( . success ( installedApp ) )
} catch let error {
installing = false
self . finish ( . failure ( error ) )
2019-06-10 15:03:47 -07:00
}
}
}
2019-12-11 12:26:48 -08:00
override func finish ( _ result : Result < InstalledApp , Error > )
{
self . cleanUp ( )
2020-06-04 19:53:10 -07:00
// O n l y r e m o v e r e f r e s h e d I P A w h e n f i n i s h e d .
if let app = self . context . app
{
let fileURL = InstalledApp . refreshedIPAURL ( for : app )
do
{
2024-11-10 16:22:50 +05:30
if ( FileManager . default . fileExists ( atPath : fileURL . path ) ) {
try FileManager . default . removeItem ( at : fileURL )
print ( " Removed refreshed IPA " )
}
2020-06-04 19:53:10 -07:00
}
catch
{
2023-04-01 16:02:12 -07:00
print ( " Failed to remove refreshed .ipa: \( error ) " )
2020-06-04 19:53:10 -07:00
}
}
2019-12-11 12:26:48 -08:00
super . finish ( result )
}
}
private extension InstallAppOperation
{
func cleanUp ( )
{
guard ! self . didCleanUp else { return }
self . didCleanUp = true
do
{
try FileManager . default . removeItem ( at : self . context . temporaryDirectory )
}
catch
{
2024-12-07 17:45:09 +05:30
print ( " Failed to remove temporary directory. " , error )
2019-12-11 12:26:48 -08:00
}
}
2019-06-10 15:03:47 -07:00
}