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 ,
2022-11-02 17:58:59 -07:00
let resignedApp = self . context . resignedApp
2019-06-21 11:20:03 -07:00
else { return self . finish ( . failure ( OperationError . invalidParameters ) ) }
2019-07-28 15:08:13 -07:00
let backgroundContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( )
backgroundContext . perform {
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
{
2020-03-06 17:08:35 -08:00
installedApp = InstalledApp ( resignedApp : resignedApp , originalBundleIdentifier : self . context . bundleIdentifier , certificateSerialNumber : certificate . serialNumber , context : backgroundContext )
2019-07-28 15:08:13 -07:00
}
2020-03-06 17:08:35 -08:00
installedApp . update ( resignedApp : resignedApp , certificateSerialNumber : certificate . serialNumber )
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
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 ( )
2020-03-11 14:43:19 -07:00
var activeProfiles : Set < String > ?
if let sideloadedAppsLimit = UserDefaults . standard . activeAppsLimit
{
// 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
}
}
activeProfiles = Set ( activeApps . flatMap { ( installedApp ) -> [ String ] in
let appExtensionProfiles = installedApp . appExtensions . map { $0 . resignedBundleIdentifier }
return [ installedApp . resignedBundleIdentifier ] + appExtensionProfiles
} )
}
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 06:06:09 -04:00
content . body = " To finish refreshing SideStore must go to the home screen. Please reopen after! "
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:17:58 -07:00
let alert = UIAlertController ( title : " Finish Refresh " , message : " SideStore will automatically be moved to the background to finish refreshing. Please reopen SideStore after the process is finished. " , 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-07-11 01:44:11 -04:00
var attempts = 10
while ( attempts != 0 ) {
print ( " Install ipa attempts left: \( attempts ) " )
do {
try install_ipa ( installedApp . bundleIdentifier )
installing = false
installedApp . refreshedDate = Date ( )
2023-07-24 16:58:09 -04:00
self . finish ( . success ( installedApp ) )
break
2023-07-11 01:44:11 -04:00
} catch {
2023-07-27 05:08:03 -04:00
attempts -= 1
if ( attempts <= 0 ) {
2023-07-11 01:44:11 -04:00
installing = false
2023-07-24 16:58:09 -04:00
self . finish ( . failure ( MinimuxerError . InstallApp ) )
}
2023-07-11 01:44:11 -04:00
}
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
{
try FileManager . default . removeItem ( at : fileURL )
2023-04-01 16:02:12 -07:00
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
{
print ( " Failed to remove temporary directory. " , error )
}
}
2019-06-10 15:03:47 -07:00
}