2019-05-24 11:29:27 -07:00
//
// A p p D e l e g a t e . s w i f t
// A l t S e r v e r
//
// C r e a t e d b y R i l e y T e s t u t o n 5 / 2 4 / 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 Cocoa
2019-07-01 15:19:22 -07:00
import UserNotifications
import AltSign
2019-05-24 11:29:27 -07:00
2019-09-04 11:58:28 -07:00
import LaunchAtLogin
2022-02-09 13:46:03 -08:00
import Sparkle
2019-11-18 14:42:38 -08:00
2021-06-04 11:53:26 -07:00
extension ALTDevice : MenuDisplayable { }
2019-05-24 11:29:27 -07:00
@NSApplicationMain
class AppDelegate : NSObject , NSApplicationDelegate {
2020-10-06 18:11:03 -07:00
private let pluginManager = PluginManager ( )
2019-07-01 15:19:22 -07:00
private var statusItem : NSStatusItem ?
private var connectedDevices = [ ALTDevice ] ( )
private weak var authenticationAlert : NSAlert ?
@IBOutlet private var appMenu : NSMenu !
@IBOutlet private var connectedDevicesMenu : NSMenu !
2020-11-11 17:25:16 -08:00
@IBOutlet private var sideloadIPAConnectedDevicesMenu : NSMenu !
2021-06-04 12:35:01 -07:00
@IBOutlet private var enableJITMenu : NSMenu !
2019-09-04 11:58:28 -07:00
@IBOutlet private var launchAtLoginMenuItem : NSMenuItem !
2019-11-18 14:42:38 -08:00
@IBOutlet private var installMailPluginMenuItem : NSMenuItem !
2021-11-10 11:42:26 -08:00
@IBOutlet private var installAltStoreMenuItem : NSMenuItem !
@IBOutlet private var sideloadAppMenuItem : NSMenuItem !
2019-07-01 15:19:22 -07:00
private weak var authenticationAppleIDTextField : NSTextField ?
private weak var authenticationPasswordTextField : NSSecureTextField ?
2019-11-18 14:42:38 -08:00
2021-06-04 11:53:26 -07:00
private var connectedDevicesMenuController : MenuController < ALTDevice > !
private var sideloadIPAConnectedDevicesMenuController : MenuController < ALTDevice > !
2021-06-04 12:35:01 -07:00
private var enableJITMenuController : MenuController < ALTDevice > !
private var _jitAppListMenuControllers = [ AnyObject ] ( )
2021-06-04 11:53:26 -07:00
2022-02-15 14:44:11 -06:00
private var isAltPluginUpdateAvailable = false
2023-01-24 13:56:41 -06:00
private var popoverController : NSPopover ?
private var popoverError : NSError ?
private var errorAlert : NSAlert ?
2019-07-01 15:19:22 -07:00
func applicationDidFinishLaunching ( _ aNotification : Notification )
{
2019-08-01 10:45:54 -07:00
UserDefaults . standard . registerDefaults ( )
2019-07-01 15:19:22 -07:00
UNUserNotificationCenter . current ( ) . delegate = self
2020-01-13 10:17:30 -08:00
2020-08-31 13:58:44 -07:00
ServerConnectionManager . shared . start ( )
2020-01-13 10:17:30 -08:00
ALTDeviceManager . shared . start ( )
2019-07-01 15:19:22 -07:00
2022-02-09 13:46:03 -08:00
#if STAGING
SUUpdater . shared ( ) . feedURL = URL ( string : " https://altstore.io/altserver/sparkle-macos-staging.xml " )
2022-05-10 15:02:58 -07:00
#else
SUUpdater . shared ( ) . feedURL = URL ( string : " https://altstore.io/altserver/sparkle-macos.xml " )
2022-02-09 13:46:03 -08:00
#endif
2019-07-01 15:19:22 -07:00
let item = NSStatusBar . system . statusItem ( withLength : - 1 )
2020-09-04 13:22:26 -07:00
item . menu = self . appMenu
item . button ? . image = NSImage ( named : " MenuBarIcon " )
2019-07-01 15:19:22 -07:00
self . statusItem = item
2020-09-04 13:22:26 -07:00
self . appMenu . delegate = self
2021-06-04 11:53:26 -07:00
2021-11-10 11:42:26 -08:00
self . sideloadAppMenuItem . keyEquivalentModifierMask = . option
self . sideloadAppMenuItem . isAlternate = true
2021-06-04 11:53:26 -07:00
let placeholder = NSLocalizedString ( " No Connected Devices " , comment : " " )
self . connectedDevicesMenuController = MenuController < ALTDevice > ( menu : self . connectedDevicesMenu , items : [ ] )
self . connectedDevicesMenuController . placeholder = placeholder
self . connectedDevicesMenuController . action = { [ weak self ] device in
self ? . installAltStore ( to : device )
}
self . sideloadIPAConnectedDevicesMenuController = MenuController < ALTDevice > ( menu : self . sideloadIPAConnectedDevicesMenu , items : [ ] )
self . sideloadIPAConnectedDevicesMenuController . placeholder = placeholder
self . sideloadIPAConnectedDevicesMenuController . action = { [ weak self ] device in
self ? . sideloadIPA ( to : device )
}
2019-09-25 01:23:23 -07:00
2021-06-04 12:35:01 -07:00
self . enableJITMenuController = MenuController < ALTDevice > ( menu : self . enableJITMenu , items : [ ] )
self . enableJITMenuController . placeholder = placeholder
2019-11-04 15:07:51 -08:00
UNUserNotificationCenter . current ( ) . requestAuthorization ( options : [ . alert ] ) { ( success , error ) in
guard success else { return }
2019-09-25 01:23:23 -07:00
2019-11-04 15:07:51 -08:00
if ! UserDefaults . standard . didPresentInitialNotification
{
let content = UNMutableNotificationContent ( )
content . title = NSLocalizedString ( " AltServer Running " , comment : " " )
content . body = NSLocalizedString ( " AltServer runs in the background as a menu bar app listening for AltStore. " , comment : " " )
let request = UNNotificationRequest ( identifier : UUID ( ) . uuidString , content : content , trigger : nil )
UNUserNotificationCenter . current ( ) . add ( request )
UserDefaults . standard . didPresentInitialNotification = true
}
2019-09-25 01:23:23 -07:00
}
2019-05-24 11:29:27 -07:00
}
2019-07-01 15:19:22 -07:00
func applicationWillTerminate ( _ aNotification : Notification )
{
2019-05-24 11:29:27 -07:00
// I n s e r t c o d e h e r e t o t e a r d o w n y o u r a p p l i c a t i o n
}
2019-07-01 15:19:22 -07:00
}
private extension AppDelegate
{
2021-06-04 11:53:26 -07:00
@objc func installAltStore ( to device : ALTDevice )
2019-07-01 15:19:22 -07:00
{
2022-11-22 13:12:10 -06:00
self . installApplication ( at : nil , to : device )
2020-11-11 17:25:16 -08:00
}
2021-06-04 11:53:26 -07:00
@objc func sideloadIPA ( to device : ALTDevice )
2020-11-11 17:25:16 -08:00
{
2021-11-10 11:44:08 -08:00
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
2020-11-11 17:25:16 -08:00
let openPanel = NSOpenPanel ( )
openPanel . canChooseDirectories = false
openPanel . allowsMultipleSelection = false
openPanel . allowedFileTypes = [ " ipa " ]
openPanel . begin { ( response ) in
guard let fileURL = openPanel . url , response = = . OK else { return }
self . installApplication ( at : fileURL , to : device )
}
}
2021-06-04 12:35:01 -07:00
func enableJIT ( for app : InstalledApp , on device : ALTDevice )
{
2023-09-08 14:15:55 -05:00
Task < Void , Never > {
do
{
try await JITManager . shared . enableUnsignedCodeExecution ( process : . name ( app . executableName ) , device : device )
await MainActor . run {
2021-06-04 12:35:01 -07:00
let alert = NSAlert ( )
alert . messageText = String ( format : NSLocalizedString ( " Successfully enabled JIT for %@. " , comment : " " ) , app . name )
alert . informativeText = String ( format : NSLocalizedString ( " JIT will remain enabled until you quit the app. You can now disconnect %@ from your computer. " , comment : " " ) , device . name )
2023-09-08 14:15:55 -05:00
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
2021-06-04 12:35:01 -07:00
alert . runModal ( )
}
}
2023-09-08 14:15:55 -05:00
catch let error as JITError where error . code = = . dependencyNotFound
2021-06-04 12:35:01 -07:00
{
2023-09-08 14:15:55 -05:00
var errorMessage = error . localizedDescription
if let recoverySuggestion = error . recoverySuggestion
{
errorMessage += " \n \n " + recoverySuggestion
}
await MainActor . run { [ errorMessage ] in
let alert = NSAlert ( )
alert . alertStyle = . critical
alert . messageText = NSLocalizedString ( " Missing AltJIT Dependencies " , comment : " " )
alert . informativeText = errorMessage
alert . addButton ( withTitle : NSLocalizedString ( " View Instructions " , comment : " " ) )
alert . addButton ( withTitle : NSLocalizedString ( " Cancel " , comment : " " ) )
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
2021-06-04 12:35:01 -07:00
2023-09-08 14:15:55 -05:00
let response = alert . runModal ( )
if response = = . alertFirstButtonReturn
{
let faqURL = URL ( string : " https://faq.altstore.io/how-to-use-altstore/altjit " ) !
NSWorkspace . shared . open ( faqURL )
2021-06-04 12:35:01 -07:00
}
}
}
2023-09-08 14:15:55 -05:00
catch let error as NSError
{
await MainActor . run {
let localizedTitle = String ( format : NSLocalizedString ( " JIT could not be enabled for %@. " , comment : " " ) , app . name )
self . showErrorAlert ( error : error . withLocalizedTitle ( localizedTitle ) )
}
}
2021-06-04 12:35:01 -07:00
}
}
2022-11-22 13:12:10 -06:00
func installApplication ( at fileURL : URL ? , to device : ALTDevice )
2020-11-11 17:25:16 -08:00
{
2019-07-01 15:19:22 -07:00
let alert = NSAlert ( )
alert . messageText = NSLocalizedString ( " Please enter your Apple ID and password. " , comment : " " )
2019-11-18 14:17:57 -08:00
alert . informativeText = NSLocalizedString ( " Your Apple ID and password are not saved and are only sent to Apple for authentication. " , comment : " " )
2019-07-01 15:19:22 -07:00
let textFieldSize = NSSize ( width : 300 , height : 22 )
let appleIDTextField = NSTextField ( frame : NSRect ( x : 0 , y : 0 , width : textFieldSize . width , height : textFieldSize . height ) )
appleIDTextField . delegate = self
appleIDTextField . translatesAutoresizingMaskIntoConstraints = false
appleIDTextField . placeholderString = NSLocalizedString ( " Apple ID " , comment : " " )
alert . window . initialFirstResponder = appleIDTextField
self . authenticationAppleIDTextField = appleIDTextField
let passwordTextField = NSSecureTextField ( frame : NSRect ( x : 0 , y : 0 , width : textFieldSize . width , height : textFieldSize . height ) )
passwordTextField . delegate = self
passwordTextField . translatesAutoresizingMaskIntoConstraints = false
passwordTextField . placeholderString = NSLocalizedString ( " Password " , comment : " " )
self . authenticationPasswordTextField = passwordTextField
appleIDTextField . nextKeyView = passwordTextField
let stackView = NSStackView ( frame : NSRect ( x : 0 , y : 0 , width : textFieldSize . width , height : textFieldSize . height * 2 ) )
stackView . orientation = . vertical
stackView . distribution = . equalSpacing
stackView . spacing = 0
stackView . addArrangedSubview ( appleIDTextField )
stackView . addArrangedSubview ( passwordTextField )
alert . accessoryView = stackView
alert . addButton ( withTitle : NSLocalizedString ( " Install " , comment : " " ) )
alert . addButton ( withTitle : NSLocalizedString ( " Cancel " , comment : " " ) )
self . authenticationAlert = alert
self . validate ( )
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
let response = alert . runModal ( )
guard response = = . alertFirstButtonReturn else { return }
let username = appleIDTextField . stringValue
let password = passwordTextField . stringValue
2022-02-15 14:44:11 -06:00
2023-09-13 15:24:29 -05:00
ALTDeviceManager . shared . installApplication ( at : fileURL , to : device , appleID : username , password : password ) { ( result ) in
2022-02-15 14:44:11 -06:00
switch result
{
case . success ( let application ) :
let content = UNMutableNotificationContent ( )
content . title = NSLocalizedString ( " Installation Succeeded " , comment : " " )
content . body = String ( format : NSLocalizedString ( " %@ was successfully installed on %@. " , comment : " " ) , application . name , device . name )
let request = UNNotificationRequest ( identifier : UUID ( ) . uuidString , content : content , trigger : nil )
UNUserNotificationCenter . current ( ) . add ( request )
2022-11-21 17:50:42 -06:00
case . failure ( OperationError . cancelled ) , . failure ( ALTAppleAPIError . requiresTwoFactorAuthentication ) :
2022-02-15 14:44:11 -06:00
// I g n o r e
break
case . failure ( let error ) :
DispatchQueue . main . async {
2023-01-24 13:56:41 -06:00
self . showErrorAlert ( error : error )
2019-09-13 14:25:26 -07:00
}
2020-02-13 21:41:31 -08:00
}
}
2019-07-01 15:19:22 -07:00
}
2019-09-04 11:58:28 -07:00
2023-01-24 13:56:41 -06:00
func showErrorAlert ( error : Error )
2021-06-04 13:57:40 -07:00
{
2023-01-24 13:56:41 -06:00
self . popoverError = error as NSError
2021-06-04 13:57:40 -07:00
2023-01-24 13:56:41 -06:00
let nsError = error as NSError
2021-06-04 13:57:40 -07:00
2023-01-24 13:56:41 -06:00
var messageComponents = [ error . localizedDescription ]
if let recoverySuggestion = nsError . localizedRecoverySuggestion
2021-10-04 15:21:57 -07:00
{
2023-01-24 13:56:41 -06:00
messageComponents . append ( recoverySuggestion )
2021-10-04 15:21:57 -07:00
}
2023-01-24 13:56:41 -06:00
let title = nsError . localizedTitle ? ? NSLocalizedString ( " Operation Failed " , comment : " " )
let message = messageComponents . joined ( separator : " \n \n " )
2021-06-04 13:57:40 -07:00
2023-01-24 13:56:41 -06:00
let alert = NSAlert ( )
alert . alertStyle = . critical
alert . messageText = title
alert . informativeText = message
alert . addButton ( withTitle : NSLocalizedString ( " OK " , comment : " " ) )
alert . addButton ( withTitle : NSLocalizedString ( " View More Details " , comment : " " ) )
if let viewMoreButton = alert . buttons . last
2021-06-04 13:57:40 -07:00
{
2023-01-24 13:56:41 -06:00
viewMoreButton . target = self
viewMoreButton . action = #selector ( AppDelegate . showDetailedErrorDescription )
self . errorAlert = alert
2021-06-04 13:57:40 -07:00
}
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
2023-01-24 13:56:41 -06:00
2021-06-04 13:57:40 -07:00
alert . runModal ( )
2023-01-24 13:56:41 -06:00
self . popoverController = nil
self . errorAlert = nil
self . popoverError = nil
}
@objc func showDetailedErrorDescription ( )
{
guard let errorAlert , let contentView = errorAlert . window . contentView else { return }
let errorDetailsViewController = NSStoryboard ( name : " Main " , bundle : . main ) . instantiateController ( withIdentifier : " errorDetailsViewController " ) as ! ErrorDetailsViewController
errorDetailsViewController . error = self . popoverError
let fittingSize = errorDetailsViewController . view . fittingSize
errorDetailsViewController . view . frame . size = fittingSize
let popoverController = NSPopover ( )
popoverController . contentViewController = errorDetailsViewController
popoverController . contentSize = fittingSize
popoverController . behavior = . transient
popoverController . show ( relativeTo : contentView . bounds , of : contentView , preferredEdge : . maxX )
self . popoverController = popoverController
2021-06-04 13:57:40 -07:00
}
2019-09-04 11:58:28 -07:00
@objc func toggleLaunchAtLogin ( _ item : NSMenuItem )
{
LaunchAtLogin . isEnabled . toggle ( )
}
2019-11-18 14:42:38 -08:00
2023-09-13 15:51:50 -05:00
@IBAction private func uninstallMailPlugin ( _ sender : NSMenuItem )
2020-02-13 21:41:31 -08:00
{
2020-10-06 18:11:03 -07:00
self . pluginManager . uninstallMailPlugin { ( result ) in
DispatchQueue . main . async {
switch result
2020-02-13 21:41:31 -08:00
{
2020-10-06 18:11:03 -07:00
case . failure ( PluginError . cancelled ) : break
case . failure ( let error ) :
let alert = NSAlert ( )
alert . messageText = NSLocalizedString ( " Failed to Uninstall Mail Plug-in " , comment : " " )
alert . informativeText = error . localizedDescription
alert . runModal ( )
case . success :
let alert = NSAlert ( )
alert . messageText = NSLocalizedString ( " Mail Plug-in Uninstalled " , comment : " " )
2023-09-13 15:51:50 -05:00
alert . informativeText = NSLocalizedString ( " Please restart Mail for changes to take effect. " , comment : " " )
2020-10-06 18:11:03 -07:00
alert . runModal ( )
2020-02-13 21:41:31 -08:00
}
}
}
}
2023-09-13 17:19:19 -05:00
@IBAction private func showAboutPanel ( _ sender : NSMenuItem )
{
NSApplication . shared . orderFrontStandardAboutPanel ( sender )
NSRunningApplication . current . activate ( options : . activateIgnoringOtherApps )
}
2020-02-13 21:41:31 -08:00
}
2019-07-01 15:19:22 -07:00
extension AppDelegate : NSMenuDelegate
{
2020-09-04 13:22:26 -07:00
func menuWillOpen ( _ menu : NSMenu )
{
guard menu = = self . appMenu else { return }
2021-06-04 12:35:01 -07:00
// C l e a r a n y c a c h e d _ j i t A p p L i s t M e n u C o n t r o l l e r s .
self . _jitAppListMenuControllers . removeAll ( )
2020-09-04 13:22:26 -07:00
2020-11-11 16:38:45 -08:00
self . connectedDevices = ALTDeviceManager . shared . availableDevices
2021-06-04 11:53:26 -07:00
self . connectedDevicesMenuController . items = self . connectedDevices
self . sideloadIPAConnectedDevicesMenuController . items = self . connectedDevices
2021-06-04 12:35:01 -07:00
self . enableJITMenuController . items = self . connectedDevices
2020-09-04 13:22:26 -07:00
self . launchAtLoginMenuItem . target = self
self . launchAtLoginMenuItem . action = #selector ( AppDelegate . toggleLaunchAtLogin ( _ : ) )
self . launchAtLoginMenuItem . state = LaunchAtLogin . isEnabled ? . on : . off
2023-09-13 15:51:50 -05:00
if ! self . pluginManager . isMailPluginInstalled
2020-09-04 13:22:26 -07:00
{
2023-09-13 15:51:50 -05:00
// H i d e " I n s t a l l M a i l P l u g - I n " o p t i o n n o w t h a t i t ' s n o t r e q u i r e d .
self . installMailPluginMenuItem . isHidden = true
2020-09-04 13:22:26 -07:00
}
2021-06-04 12:35:01 -07:00
// N e e d t o r e - s e t t h i s e v e r y t i m e m e n u a p p e a r s s o w e c a n r e f r e s h d e v i c e a p p l i s t .
self . enableJITMenuController . submenuHandler = { [ weak self ] device in
let submenu = NSMenu ( title : NSLocalizedString ( " Sideloaded Apps " , comment : " " ) )
guard let ` self ` = self else { return submenu }
let submenuController = MenuController < InstalledApp > ( menu : submenu , items : [ ] )
submenuController . placeholder = NSLocalizedString ( " Loading... " , comment : " " )
submenuController . action = { [ weak self ] ( appInfo ) in
self ? . enableJIT ( for : appInfo , on : device )
}
// K e e p s t r o n g r e f e r e n c e
self . _jitAppListMenuControllers . append ( submenuController )
ALTDeviceManager . shared . fetchInstalledApps ( on : device ) { ( installedApps , error ) in
DispatchQueue . main . async {
guard let installedApps = installedApps else {
print ( " Failed to fetch installed apps from \( device ) . " , error ! )
submenuController . placeholder = error ? . localizedDescription
return
}
print ( " Fetched \( installedApps . count ) apps for \( device ) . " )
let sortedApps = installedApps . sorted { ( app1 , app2 ) in
if app1 . name = = app2 . name
{
return app1 . bundleIdentifier < app2 . bundleIdentifier
}
else
{
return app1 . name < app2 . name
}
}
submenuController . items = sortedApps
if submenuController . items . isEmpty
{
submenuController . placeholder = NSLocalizedString ( " No Sideloaded Apps " , comment : " " )
}
}
}
return submenu
}
}
func menuDidClose ( _ menu : NSMenu )
{
2022-03-02 15:58:01 -08:00
guard menu = = self . appMenu else { return }
2021-06-04 12:35:01 -07:00
// C l e a r i n g _ j i t A p p L i s t M e n u C o n t r o l l e r s n o w p r e v e n t s a c t i o n h a n d l e r f r o m b e i n g c a l l e d .
// s e l f . _ j i t A p p L i s t M e n u C o n t r o l l e r s = [ ]
2022-03-02 15:58:01 -08:00
// S e t ` s u b m e n u H a n d l e r ` t o n i l t o p r e v e n t p r e m a t u r e l y f e t c h i n g i n s t a l l e d a p p s i n m e n u W i l l O p e n ( _ : )
// w h e n a s s i g n i n g s e l f . c o n n e c t e d D e v i c e s t o ` i t e m s ` ( w h i c h i m p l i c i t l y c a l l s ` s u b m e n u H a n d l e r ` )
self . enableJITMenuController . submenuHandler = nil
2020-09-04 13:22:26 -07:00
}
2021-11-10 11:42:26 -08:00
func menu ( _ menu : NSMenu , willHighlight item : NSMenuItem ? )
{
guard menu = = self . appMenu else { return }
// T h e s u b m e n u w o n ' t u p d a t e c o r r e c t l y i f t h e u s e r h o l d s / r e l e a s e s
// t h e O p t i o n k e y w h i l e t h e s u b m e n u i s v i s i b l e .
// W o r k a r o u n d : t e m p o r a r i l y s e t s u b m e n u t o n i l t o d i s m i s s i t ,
// w h i c h w i l l t h e n c a u s e t h e c o r r e c t s u b m e n u t o a p p e a r .
let previousItem : NSMenuItem
switch item
{
case self . sideloadAppMenuItem : previousItem = self . installAltStoreMenuItem
case self . installAltStoreMenuItem : previousItem = self . sideloadAppMenuItem
default : return
}
let submenu = previousItem . submenu
previousItem . submenu = nil
previousItem . submenu = submenu
}
2019-07-01 15:19:22 -07:00
}
2019-05-24 11:29:27 -07:00
2019-07-01 15:19:22 -07:00
extension AppDelegate : NSTextFieldDelegate
{
func controlTextDidChange ( _ obj : Notification )
{
self . validate ( )
}
func controlTextDidEndEditing ( _ obj : Notification )
{
self . validate ( )
}
private func validate ( )
{
guard
let appleID = self . authenticationAppleIDTextField ? . stringValue . trimmingCharacters ( in : . whitespacesAndNewlines ) ,
let password = self . authenticationPasswordTextField ? . stringValue . trimmingCharacters ( in : . whitespacesAndNewlines )
else { return }
if appleID . isEmpty || password . isEmpty
{
self . authenticationAlert ? . buttons . first ? . isEnabled = false
}
else
{
self . authenticationAlert ? . buttons . first ? . isEnabled = true
}
self . authenticationAlert ? . layout ( )
}
2019-05-24 11:29:27 -07:00
}
2019-07-01 15:19:22 -07:00
extension AppDelegate : UNUserNotificationCenterDelegate
{
func userNotificationCenter ( _ center : UNUserNotificationCenter , willPresent notification : UNNotification , withCompletionHandler completionHandler : @ escaping ( UNNotificationPresentationOptions ) -> Void )
{
completionHandler ( [ . alert , . sound , . badge ] )
}
}