Adds support for sideloading .ipa’s via “Open in…”

This commit is contained in:
Riley Testut
2019-09-27 17:39:36 -07:00
parent 7fc822948c
commit 9e610ddb73
4 changed files with 162 additions and 60 deletions

View File

@@ -52,7 +52,10 @@ private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, Uns
extension AppDelegate 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 @UIApplicationMain
@@ -115,14 +118,27 @@ private extension AppDelegate
func open(_ url: URL) -> Bool func open(_ url: URL) -> Bool
{ {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } if url.isFileURL
guard let host = components.host, host.lowercased() == "patreon" else { return false } {
guard url.pathExtension.lowercased() == "ipa" else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) 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
} }
} }

View File

@@ -8,6 +8,23 @@
<string>1AAAB6FD-E8CE-4B70-8F26-4073215C03B0</string> <string>1AAAB6FD-E8CE-4B70-8F26-4073215C03B0</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>iOS App</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.itunes.ipa</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>

View File

@@ -60,6 +60,7 @@ class MyAppsViewController: UICollectionViewController
super.init(coder: aDecoder) super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.didFetchSource(_:)), name: AppManager.didFetchSourceNotification, object: nil) 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() override func viewDidLoad()
@@ -577,23 +578,83 @@ private extension MyAppsViewController
@IBAction func sideloadApp(_ sender: UIBarButtonItem) @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 iOSAppUTI = "com.apple.itunes.ipa" // Declared by the system.
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: [iOSAppUTI], in: .import) let documentPickerViewController = UIDocumentPickerViewController(documentTypes: [iOSAppUTI], in: .import)
documentPickerViewController.delegate = self documentPickerViewController.delegate = self
self.present(documentPickerViewController, animated: true, completion: nil) 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) 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 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) self.present(alertController, animated: true, completion: nil)
} }
func installApp(at fileURL: URL, completion: @escaping (Result<Void, Error>) -> 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) @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) 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) 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 extension MyAppsViewController
@@ -834,51 +930,8 @@ extension MyAppsViewController: UIDocumentPickerDelegate
{ {
guard let fileURL = urls.first else { return } guard let fileURL = urls.first else { return }
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true self.installApp(at: fileURL) { (result) in
print("Sideloaded app at \(fileURL) with result:", result)
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
}
} }
} }
} }

View File

@@ -8,6 +8,17 @@
import UIKit import UIKit
extension TabBarController
{
private enum Tab: Int, CaseIterable
{
case news
case browse
case myApps
case settings
}
}
class TabBarController: UITabBarController class TabBarController: UITabBarController
{ {
required init?(coder aDecoder: NSCoder) required init?(coder aDecoder: NSCoder)
@@ -15,6 +26,7 @@ class TabBarController: UITabBarController
super.init(coder: aDecoder) super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) 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) @objc func openPatreonSettings(_ notification: Notification)
{ {
guard let items = self.tabBar.items else { return } self.selectedIndex = Tab.settings.rawValue
self.selectedIndex = items.count - 1 }
@objc func importApp(_ notification: Notification)
{
self.selectedIndex = Tab.myApps.rawValue
} }
} }