mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Tries to refresh apps that are about to expire first, and then always refreshes AltStore itself last, since refreshing AltStore means that the app will quit.
297 lines
10 KiB
Swift
297 lines
10 KiB
Swift
//
|
|
// AppManager.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 5/29/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
import AltSign
|
|
import AltKit
|
|
|
|
import Roxas
|
|
|
|
extension AppManager
|
|
{
|
|
static let didFetchAppsNotification = Notification.Name("com.altstore.AppManager.didFetchApps")
|
|
}
|
|
|
|
class AppManager
|
|
{
|
|
static let shared = AppManager()
|
|
|
|
private let operationQueue = OperationQueue()
|
|
private let processingQueue = DispatchQueue(label: "com.altstore.AppManager.processingQueue")
|
|
|
|
private init()
|
|
{
|
|
self.operationQueue.name = "com.altstore.AppManager.operationQueue"
|
|
}
|
|
}
|
|
|
|
extension AppManager
|
|
{
|
|
func update()
|
|
{
|
|
#if targetEnvironment(simulator)
|
|
// Apps aren't ever actually installed to simulator, so just do nothing rather than delete them from database.
|
|
return
|
|
#else
|
|
|
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
|
|
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
|
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
|
|
|
|
do
|
|
{
|
|
let installedApps = try context.fetch(fetchRequest)
|
|
for app in installedApps
|
|
{
|
|
if UIApplication.shared.canOpenURL(app.openAppURL)
|
|
{
|
|
// App is still installed, good!
|
|
}
|
|
else
|
|
{
|
|
context.delete(app)
|
|
}
|
|
}
|
|
|
|
try context.save()
|
|
}
|
|
catch
|
|
{
|
|
print("Error while fetching installed apps")
|
|
}
|
|
|
|
#endif
|
|
}
|
|
|
|
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTSigner, Error>) -> Void)
|
|
{
|
|
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
|
authenticationOperation.resultHandler = { (result) in
|
|
completionHandler(result)
|
|
}
|
|
self.operationQueue.addOperation(authenticationOperation)
|
|
}
|
|
}
|
|
|
|
extension AppManager
|
|
{
|
|
func fetchApps(completionHandler: @escaping (Result<[App], Error>) -> Void)
|
|
{
|
|
let fetchAppsOperation = FetchAppsOperation()
|
|
fetchAppsOperation.resultHandler = { (result) in
|
|
switch result
|
|
{
|
|
case .failure(let error):
|
|
completionHandler(.failure(error))
|
|
|
|
case .success(let apps):
|
|
completionHandler(.success(apps))
|
|
NotificationCenter.default.post(name: AppManager.didFetchAppsNotification, object: self)
|
|
}
|
|
}
|
|
self.operationQueue.addOperation(fetchAppsOperation)
|
|
}
|
|
}
|
|
|
|
extension AppManager
|
|
{
|
|
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
|
{
|
|
let group = self.install([app], forceDownload: true, presentingViewController: presentingViewController)
|
|
group.completionHandler = { (result) in
|
|
do
|
|
{
|
|
guard let (_, result) = try result.get().first else { throw OperationError.unknown }
|
|
completionHandler(result)
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
|
|
return group.progress
|
|
}
|
|
|
|
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
|
|
{
|
|
let apps = installedApps.compactMap { $0.app }
|
|
|
|
let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group)
|
|
return group
|
|
}
|
|
}
|
|
|
|
private extension AppManager
|
|
{
|
|
func install(_ apps: [App], forceDownload: Bool, presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
|
|
{
|
|
// Authenticate -> Download (if necessary) -> Resign -> Send -> Install.
|
|
let group = group ?? OperationGroup()
|
|
|
|
guard let server = ServerManager.shared.discoveredServers.first else {
|
|
DispatchQueue.main.async {
|
|
group.completionHandler?(.failure(ConnectionError.serverNotFound))
|
|
}
|
|
|
|
return group
|
|
}
|
|
|
|
group.server = server
|
|
|
|
var operations = [Operation]()
|
|
|
|
|
|
/* Authenticate */
|
|
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
|
authenticationOperation.resultHandler = { (result) in
|
|
switch result
|
|
{
|
|
case .failure(let error): group.error = error
|
|
case .success(let signer): group.signer = signer
|
|
}
|
|
}
|
|
operations.append(authenticationOperation)
|
|
|
|
|
|
for app in apps
|
|
{
|
|
let context = AppOperationContext(appIdentifier: app.identifier, group: group)
|
|
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
|
|
|
|
|
/* Resign */
|
|
let resignAppOperation = ResignAppOperation(context: context)
|
|
resignAppOperation.resultHandler = { (result) in
|
|
switch result
|
|
{
|
|
case .failure(let error): context.error = error
|
|
case .success(let fileURL): context.resignedFileURL = fileURL
|
|
}
|
|
}
|
|
resignAppOperation.addDependency(authenticationOperation)
|
|
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
|
operations.append(resignAppOperation)
|
|
|
|
|
|
/* Download */
|
|
let fileURL = InstalledApp.fileURL(for: app)
|
|
if let installedApp = app.installedApp, FileManager.default.fileExists(atPath: fileURL.path), !forceDownload
|
|
{
|
|
// Already installed, don't need to download.
|
|
|
|
// If we don't need to download the app, reduce the total unit count by 40.
|
|
progress.totalUnitCount -= 40
|
|
|
|
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
|
backgroundContext.performAndWait {
|
|
let installedApp = backgroundContext.object(with: installedApp.objectID) as! InstalledApp
|
|
context.installedApp = installedApp
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// App is not yet installed (or we're forcing it to download a new version), so download it before resigning it.
|
|
|
|
let downloadOperation = DownloadAppOperation(app: app)
|
|
downloadOperation.resultHandler = { (result) in
|
|
switch result
|
|
{
|
|
case .failure(let error): context.error = error
|
|
case .success(let installedApp): context.installedApp = installedApp
|
|
}
|
|
}
|
|
progress.addChild(downloadOperation.progress, withPendingUnitCount: 40)
|
|
resignAppOperation.addDependency(downloadOperation)
|
|
operations.append(downloadOperation)
|
|
}
|
|
|
|
|
|
/* Send */
|
|
let sendAppOperation = SendAppOperation(context: context)
|
|
sendAppOperation.resultHandler = { (result) in
|
|
switch result
|
|
{
|
|
case .failure(let error): context.error = error
|
|
case .success(let connection): context.connection = connection
|
|
}
|
|
}
|
|
progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10)
|
|
sendAppOperation.addDependency(resignAppOperation)
|
|
operations.append(sendAppOperation)
|
|
|
|
|
|
/* Install */
|
|
let installOperation = InstallAppOperation(context: context)
|
|
installOperation.resultHandler = { (result) in
|
|
switch result
|
|
{
|
|
case .failure(let error): context.error = error
|
|
case .success: break
|
|
}
|
|
|
|
self.finishAppOperation(context)
|
|
}
|
|
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
|
installOperation.addDependency(sendAppOperation)
|
|
operations.append(installOperation)
|
|
|
|
group.progress.totalUnitCount += 1
|
|
group.progress.addChild(progress, withPendingUnitCount: 1)
|
|
}
|
|
|
|
group.addOperations(operations)
|
|
|
|
return group
|
|
}
|
|
|
|
@discardableResult func process<T>(_ result: Result<T, Error>, context: AppOperationContext) -> T?
|
|
{
|
|
do
|
|
{
|
|
let value = try result.get()
|
|
return value
|
|
}
|
|
catch
|
|
{
|
|
context.error = error
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func finishAppOperation(_ context: AppOperationContext)
|
|
{
|
|
self.processingQueue.sync {
|
|
if let error = context.error
|
|
{
|
|
context.group.results[context.appIdentifier] = .failure(error)
|
|
}
|
|
else if let installedApp = context.installedApp
|
|
{
|
|
context.group.results[context.appIdentifier] = .success(installedApp)
|
|
|
|
// Save after each installation.
|
|
installedApp.managedObjectContext?.perform {
|
|
do { try installedApp.managedObjectContext?.save() }
|
|
catch { print("Error saving installed app.", error) }
|
|
}
|
|
}
|
|
|
|
print("Finished operation!", context.appIdentifier)
|
|
|
|
if context.group.results.count == context.group.progress.totalUnitCount
|
|
{
|
|
context.group.completionHandler?(.success(context.group.results))
|
|
}
|
|
}
|
|
}
|
|
}
|