diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 6b4fbd4c..65dc2c40 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -52,7 +52,10 @@ private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, Uns extension AppDelegate { - static let openPatreonSettingsDeepLinkNotification = Notification.Name("openPatreonSettingsDeepLinkNotification") + static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification") + static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification") + + static let importAppDeepLinkURLKey = "fileURL" } @UIApplicationMain @@ -115,14 +118,27 @@ private extension AppDelegate func open(_ url: URL) -> Bool { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } - guard let host = components.host, host.lowercased() == "patreon" else { return false } - - DispatchQueue.main.async { - NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) + if url.isFileURL + { + guard url.pathExtension.lowercased() == "ipa" else { return false } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url]) + } + + return true + } + else + { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } + guard let host = components.host, host.lowercased() == "patreon" else { return false } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) + } + + return true } - - return true } } diff --git a/AltStore/Info.plist b/AltStore/Info.plist index c53d860a..61de066e 100644 --- a/AltStore/Info.plist +++ b/AltStore/Info.plist @@ -8,6 +8,23 @@ 1AAAB6FD-E8CE-4B70-8F26-4073215C03B0 CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDocumentTypes + + + CFBundleTypeIconFiles + + CFBundleTypeName + iOS App + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + com.apple.itunes.ipa + + + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 813e3af8..605efa85 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -60,6 +60,7 @@ class MyAppsViewController: UICollectionViewController super.init(coder: aDecoder) NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.didFetchSource(_:)), name: AppManager.didFetchSourceNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil) } override func viewDidLoad() @@ -577,23 +578,83 @@ private extension MyAppsViewController @IBAction func sideloadApp(_ sender: UIBarButtonItem) { - func sideloadApp() - { + self.presentSideloadingAlert { (shouldContinue) in + guard shouldContinue else { return } + let iOSAppUTI = "com.apple.itunes.ipa" // Declared by the system. let documentPickerViewController = UIDocumentPickerViewController(documentTypes: [iOSAppUTI], in: .import) documentPickerViewController.delegate = self self.present(documentPickerViewController, animated: true, completion: nil) } - + } + + func presentSideloadingAlert(completion: @escaping (Bool) -> Void) + { let alertController = UIAlertController(title: NSLocalizedString("Sideload Apps (Beta)", comment: ""), message: NSLocalizedString("You may only install 10 apps + app extensions per week due to Apple's restrictions.\n\nIf you encounter an app that is not able to be sideloaded, please report the app to support@altstore.io.", comment: ""), preferredStyle: .alert) alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .default, handler: { (action) in - sideloadApp() + completion(true) + })) + alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in + completion(false) })) - alertController.addAction(.cancel) self.present(alertController, animated: true, completion: nil) } + func installApp(at fileURL: URL, completion: @escaping (Result) -> Void) + { + self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true + + DispatchQueue.global().async { + let temporaryDirectory = FileManager.default.uniqueTemporaryURL() + + do + { + try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) + + let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory) + + guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { return } + + self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in + try? FileManager.default.removeItem(at: temporaryDirectory) + + DispatchQueue.main.async { + if let error = result.error + { + let toastView = ToastView(text: error.localizedDescription, detailText: nil) + toastView.show(in: self.view, duration: 2.0) + } + else + { + print("Successfully installed app:", application.bundleIdentifier) + } + + self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false + self.sideloadingProgressView.observedProgress = nil + self.sideloadingProgressView.setHidden(true, animated: true) + + completion(.success(())) + } + } + + DispatchQueue.main.async { + self.sideloadingProgressView.progress = 0 + self.sideloadingProgressView.isHidden = false + self.sideloadingProgressView.observedProgress = self.sideloadingProgress + } + } + catch + { + try? FileManager.default.removeItem(at: temporaryDirectory) + + self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false + + completion(.failure(error)) + } + } + } + @objc func presentAlert(for installedApp: InstalledApp) { let alertController = UIAlertController(title: nil, message: NSLocalizedString("Removing a sideloaded app only removes it from AltStore. You must also delete it from the home screen to fully uninstall the app.", comment: ""), preferredStyle: .actionSheet) @@ -643,6 +704,41 @@ private extension MyAppsViewController self.presentAlert(for: installedApp) } + + @objc func importApp(_ notification: Notification) + { + #if BETA + + guard let fileURL = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return } + guard self.presentedViewController == nil else { return } + + func finish() + { + do + { + try FileManager.default.removeItem(at: fileURL) + } + catch + { + print("Unable to remove imported .ipa.", error) + } + } + + self.presentSideloadingAlert { (shouldContinue) in + if shouldContinue + { + self.installApp(at: fileURL) { (result) in + finish() + } + } + else + { + finish() + } + } + + #endif + } } extension MyAppsViewController @@ -834,51 +930,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate { guard let fileURL = urls.first else { return } - self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true - - DispatchQueue.global().async { - let temporaryDirectory = FileManager.default.uniqueTemporaryURL() - - do - { - try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) - - let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory) - - guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { return } - - self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in - try? FileManager.default.removeItem(at: temporaryDirectory) - - DispatchQueue.main.async { - if let error = result.error - { - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) - } - else - { - print("Successfully installed app:", application.bundleIdentifier) - } - - self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false - self.sideloadingProgressView.observedProgress = nil - self.sideloadingProgressView.setHidden(true, animated: true) - } - } - - DispatchQueue.main.async { - self.sideloadingProgressView.progress = 0 - self.sideloadingProgressView.isHidden = false - self.sideloadingProgressView.observedProgress = self.sideloadingProgress - } - } - catch - { - try? FileManager.default.removeItem(at: temporaryDirectory) - - self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false - } + self.installApp(at: fileURL) { (result) in + print("Sideloaded app at \(fileURL) with result:", result) } } } diff --git a/AltStore/TabBarController.swift b/AltStore/TabBarController.swift index 80d92f43..904c28a3 100644 --- a/AltStore/TabBarController.swift +++ b/AltStore/TabBarController.swift @@ -8,6 +8,17 @@ import UIKit +extension TabBarController +{ + private enum Tab: Int, CaseIterable + { + case news + case browse + case myApps + case settings + } +} + class TabBarController: UITabBarController { required init?(coder aDecoder: NSCoder) @@ -15,6 +26,7 @@ class TabBarController: UITabBarController super.init(coder: aDecoder) NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil) } } @@ -22,7 +34,11 @@ private extension TabBarController { @objc func openPatreonSettings(_ notification: Notification) { - guard let items = self.tabBar.items else { return } - self.selectedIndex = items.count - 1 + self.selectedIndex = Tab.settings.rawValue + } + + @objc func importApp(_ notification: Notification) + { + self.selectedIndex = Tab.myApps.rawValue } }