2019-05-31 18:24:08 -07:00
|
|
|
//
|
|
|
|
|
// AppManager.swift
|
|
|
|
|
// AltStore
|
|
|
|
|
//
|
|
|
|
|
// Created by Riley Testut on 5/29/19.
|
|
|
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import UIKit
|
2019-09-21 22:31:10 -07:00
|
|
|
import UserNotifications
|
2019-12-17 19:17:45 -08:00
|
|
|
import MobileCoreServices
|
2019-05-31 18:24:08 -07:00
|
|
|
|
|
|
|
|
import AltSign
|
|
|
|
|
import AltKit
|
|
|
|
|
|
|
|
|
|
import Roxas
|
|
|
|
|
|
2019-06-17 14:49:23 -07:00
|
|
|
extension AppManager
|
|
|
|
|
{
|
2019-07-30 17:00:04 -07:00
|
|
|
static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource")
|
2019-09-21 22:31:10 -07:00
|
|
|
|
|
|
|
|
static let expirationWarningNotificationID = "altstore-expiration-warning"
|
2019-06-17 14:49:23 -07:00
|
|
|
}
|
|
|
|
|
|
2019-05-31 18:24:08 -07:00
|
|
|
class AppManager
|
|
|
|
|
{
|
|
|
|
|
static let shared = AppManager()
|
|
|
|
|
|
2019-06-05 18:05:21 -07:00
|
|
|
private let operationQueue = OperationQueue()
|
2019-06-21 11:20:03 -07:00
|
|
|
private let processingQueue = DispatchQueue(label: "com.altstore.AppManager.processingQueue")
|
2019-05-31 18:24:08 -07:00
|
|
|
|
2019-07-19 16:10:30 -07:00
|
|
|
private var installationProgress = [String: Progress]()
|
|
|
|
|
private var refreshProgress = [String: Progress]()
|
2019-07-16 14:22:45 -07:00
|
|
|
|
2019-05-31 18:24:08 -07:00
|
|
|
private init()
|
|
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
self.operationQueue.name = "com.altstore.AppManager.operationQueue"
|
2019-05-31 18:24:08 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-04 13:53:21 -07:00
|
|
|
extension AppManager
|
|
|
|
|
{
|
2019-06-04 18:29:50 -07:00
|
|
|
func update()
|
2019-06-04 13:53:21 -07:00
|
|
|
{
|
2019-06-06 12:56:52 -07:00
|
|
|
#if targetEnvironment(simulator)
|
|
|
|
|
// Apps aren't ever actually installed to simulator, so just do nothing rather than delete them from database.
|
|
|
|
|
return
|
|
|
|
|
#else
|
|
|
|
|
|
2019-06-04 13:53:21 -07:00
|
|
|
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
|
|
|
|
|
|
|
|
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
2019-07-28 15:08:13 -07:00
|
|
|
fetchRequest.returnsObjectsAsFaults = false
|
2019-06-04 13:53:21 -07:00
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
let installedApps = try context.fetch(fetchRequest)
|
2019-12-17 19:17:45 -08:00
|
|
|
for app in installedApps
|
2019-06-04 13:53:21 -07:00
|
|
|
{
|
2019-12-17 19:17:45 -08:00
|
|
|
let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
|
|
|
|
|
2019-09-21 22:31:10 -07:00
|
|
|
if app.bundleIdentifier == StoreApp.altstoreAppID
|
2019-06-04 13:53:21 -07:00
|
|
|
{
|
2019-09-21 22:31:10 -07:00
|
|
|
self.scheduleExpirationWarningLocalNotification(for: app)
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-01-13 09:01:19 -08:00
|
|
|
if uti == nil && !UIApplication.shared.canOpenURL(app.openAppURL)
|
2019-09-21 22:31:10 -07:00
|
|
|
{
|
2019-12-17 19:17:45 -08:00
|
|
|
// This UTI is not declared by any apps, which means this app has been deleted by the user.
|
2020-01-13 09:01:19 -08:00
|
|
|
// We also check canOpenURL as a fallback for apps installed before we switched to using custom UTIs.
|
|
|
|
|
// canOpenURL always returns NO for apps not declared in our Info.plist,
|
|
|
|
|
// so it should not affect UTI-based installation checks.
|
2019-09-21 22:31:10 -07:00
|
|
|
context.delete(app)
|
|
|
|
|
}
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try context.save()
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
print("Error while fetching installed apps")
|
|
|
|
|
}
|
2019-06-06 12:56:52 -07:00
|
|
|
|
|
|
|
|
#endif
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|
2019-06-05 18:05:21 -07:00
|
|
|
|
2019-11-18 14:49:17 -08:00
|
|
|
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTSigner, ALTAppleAPISession), Error>) -> Void)
|
2019-06-05 18:05:21 -07:00
|
|
|
{
|
2019-11-18 14:49:17 -08:00
|
|
|
let group = OperationGroup()
|
|
|
|
|
|
|
|
|
|
let findServerOperation = FindServerOperation(group: group)
|
|
|
|
|
findServerOperation.resultHandler = { (result) in
|
|
|
|
|
switch result
|
|
|
|
|
{
|
|
|
|
|
case .failure(let error): group.error = error
|
|
|
|
|
case .success(let server): group.server = server
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-08 12:41:02 -08:00
|
|
|
self.operationQueue.addOperation(findServerOperation)
|
2019-11-18 14:49:17 -08:00
|
|
|
|
|
|
|
|
let authenticationOperation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
|
2019-06-05 18:05:21 -07:00
|
|
|
authenticationOperation.resultHandler = { (result) in
|
|
|
|
|
completionHandler(result)
|
|
|
|
|
}
|
2020-01-08 12:41:02 -08:00
|
|
|
authenticationOperation.addDependency(findServerOperation)
|
2019-06-05 18:05:21 -07:00
|
|
|
self.operationQueue.addOperation(authenticationOperation)
|
|
|
|
|
}
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|
|
|
|
|
|
2019-05-31 18:24:08 -07:00
|
|
|
extension AppManager
|
2019-06-17 14:49:23 -07:00
|
|
|
{
|
2019-07-30 17:00:04 -07:00
|
|
|
func fetchSource(completionHandler: @escaping (Result<Source, Error>) -> Void)
|
2019-06-17 14:49:23 -07:00
|
|
|
{
|
2019-07-30 17:00:04 -07:00
|
|
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
|
|
|
|
guard let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) else {
|
|
|
|
|
return completionHandler(.failure(OperationError.noSources))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let fetchSourceOperation = FetchSourceOperation(sourceURL: source.sourceURL)
|
|
|
|
|
fetchSourceOperation.resultHandler = { (result) in
|
|
|
|
|
switch result
|
|
|
|
|
{
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
completionHandler(.failure(error))
|
|
|
|
|
|
|
|
|
|
case .success(let source):
|
|
|
|
|
completionHandler(.success(source))
|
|
|
|
|
NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self)
|
|
|
|
|
}
|
2019-06-17 14:49:23 -07:00
|
|
|
}
|
2019-07-30 17:00:04 -07:00
|
|
|
self.operationQueue.addOperation(fetchSourceOperation)
|
2019-06-17 14:49:23 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension AppManager
|
2019-05-31 18:24:08 -07:00
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
func install(_ app: AppProtocol, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
2019-05-31 18:24:08 -07:00
|
|
|
{
|
2019-07-16 14:22:45 -07:00
|
|
|
if let progress = self.installationProgress(for: app)
|
|
|
|
|
{
|
|
|
|
|
return progress
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
let bundleIdentifier = app.bundleIdentifier
|
2019-07-19 16:10:30 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
let group = self.install([app], forceDownload: true, presentingViewController: presentingViewController)
|
2019-07-16 14:22:45 -07:00
|
|
|
group.completionHandler = { (result) in
|
2019-06-18 18:31:59 -07:00
|
|
|
do
|
2019-05-31 18:24:08 -07:00
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
self.installationProgress[bundleIdentifier] = nil
|
2019-07-16 14:22:45 -07:00
|
|
|
|
2019-06-18 18:31:59 -07:00
|
|
|
guard let (_, result) = try result.get().first else { throw OperationError.unknown }
|
|
|
|
|
completionHandler(result)
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
completionHandler(.failure(error))
|
2019-05-31 18:24:08 -07:00
|
|
|
}
|
|
|
|
|
}
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
self.installationProgress[bundleIdentifier] = group.progress
|
2019-07-16 14:22:45 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
return group.progress
|
2019-05-31 18:24:08 -07:00
|
|
|
}
|
2019-06-04 18:29:50 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
|
2019-06-04 18:29:50 -07:00
|
|
|
{
|
2019-10-28 13:16:55 -07:00
|
|
|
let apps = installedApps.filter { self.refreshProgress(for: $0) == nil || self.refreshProgress(for: $0)?.isCancelled == true }
|
2019-06-21 11:20:03 -07:00
|
|
|
|
|
|
|
|
let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group)
|
2019-07-19 16:10:30 -07:00
|
|
|
|
|
|
|
|
for app in apps
|
|
|
|
|
{
|
|
|
|
|
guard let progress = group.progress(for: app) else { continue }
|
2019-07-28 15:08:13 -07:00
|
|
|
self.refreshProgress[app.bundleIdentifier] = progress
|
2019-07-19 16:10:30 -07:00
|
|
|
}
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
return group
|
2019-06-18 18:31:59 -07:00
|
|
|
}
|
2019-07-16 14:22:45 -07:00
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
func installationProgress(for app: AppProtocol) -> Progress?
|
2019-07-16 14:22:45 -07:00
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
let progress = self.installationProgress[app.bundleIdentifier]
|
2019-07-19 16:10:30 -07:00
|
|
|
return progress
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
func refreshProgress(for app: AppProtocol) -> Progress?
|
2019-07-19 16:10:30 -07:00
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
let progress = self.refreshProgress[app.bundleIdentifier]
|
2019-07-16 14:22:45 -07:00
|
|
|
return progress
|
|
|
|
|
}
|
2019-06-18 18:31:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension AppManager
|
|
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
func install(_ apps: [AppProtocol], forceDownload: Bool, presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
|
2019-06-18 18:31:59 -07:00
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
// Authenticate -> Download (if necessary) -> Resign -> Send -> Install.
|
|
|
|
|
let group = group ?? OperationGroup()
|
|
|
|
|
var operations = [Operation]()
|
|
|
|
|
|
2019-09-10 12:19:46 -07:00
|
|
|
/* Find Server */
|
|
|
|
|
let findServerOperation = FindServerOperation(group: group)
|
|
|
|
|
findServerOperation.resultHandler = { (result) in
|
|
|
|
|
switch result
|
|
|
|
|
{
|
|
|
|
|
case .failure(let error): group.error = error
|
|
|
|
|
case .success(let server): group.server = server
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
operations.append(findServerOperation)
|
|
|
|
|
|
2019-11-18 14:49:17 -08:00
|
|
|
let authenticationOperation: AuthenticationOperation?
|
|
|
|
|
|
|
|
|
|
if group.signer == nil || group.session == nil
|
2019-10-28 13:16:55 -07:00
|
|
|
{
|
|
|
|
|
/* Authenticate */
|
2019-11-18 14:49:17 -08:00
|
|
|
let operation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
|
|
|
|
|
operation.resultHandler = { (result) in
|
2019-10-28 13:16:55 -07:00
|
|
|
switch result
|
|
|
|
|
{
|
|
|
|
|
case .failure(let error): group.error = error
|
2019-11-18 14:49:17 -08:00
|
|
|
case .success(let signer, let session):
|
|
|
|
|
group.signer = signer
|
|
|
|
|
group.session = session
|
2019-10-28 13:16:55 -07:00
|
|
|
}
|
|
|
|
|
}
|
2019-11-18 14:49:17 -08:00
|
|
|
operations.append(operation)
|
|
|
|
|
operation.addDependency(findServerOperation)
|
2019-10-28 13:16:55 -07:00
|
|
|
|
2019-11-18 14:49:17 -08:00
|
|
|
authenticationOperation = operation
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
authenticationOperation = nil
|
|
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
|
2020-01-08 12:41:02 -08:00
|
|
|
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(group: group)
|
|
|
|
|
refreshAnisetteDataOperation.resultHandler = { (result) in
|
|
|
|
|
switch result
|
|
|
|
|
{
|
|
|
|
|
case .failure(let error): group.error = error
|
|
|
|
|
case .success(let anisetteData): group.session?.anisetteData = anisetteData
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
refreshAnisetteDataOperation.addDependency(authenticationOperation ?? findServerOperation)
|
|
|
|
|
operations.append(refreshAnisetteDataOperation)
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
for app in apps
|
|
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, group: group)
|
2019-06-21 11:20:03 -07:00
|
|
|
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Resign */
|
|
|
|
|
let resignAppOperation = ResignAppOperation(context: context)
|
|
|
|
|
resignAppOperation.resultHandler = { (result) in
|
2019-07-28 15:08:13 -07:00
|
|
|
guard let resignedApp = self.process(result, context: context) else { return }
|
|
|
|
|
context.resignedApp = resignedApp
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
2020-01-08 12:41:02 -08:00
|
|
|
resignAppOperation.addDependency(refreshAnisetteDataOperation)
|
2019-06-21 11:20:03 -07:00
|
|
|
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
|
|
|
|
|
operations.append(resignAppOperation)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Download */
|
|
|
|
|
let fileURL = InstalledApp.fileURL(for: app)
|
2019-07-28 15:08:13 -07:00
|
|
|
|
|
|
|
|
var localApp: ALTApplication?
|
|
|
|
|
|
|
|
|
|
let managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
|
|
|
|
managedObjectContext.performAndWait {
|
|
|
|
|
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), context.bundleIdentifier)
|
|
|
|
|
|
|
|
|
|
if let installedApp = InstalledApp.first(satisfying: predicate, in: managedObjectContext), FileManager.default.fileExists(atPath: fileURL.path), !forceDownload
|
|
|
|
|
{
|
|
|
|
|
localApp = ALTApplication(fileURL: installedApp.fileURL)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let localApp = localApp
|
2019-06-21 11:20:03 -07:00
|
|
|
{
|
|
|
|
|
// Already installed, don't need to download.
|
2019-06-05 18:05:21 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
// If we don't need to download the app, reduce the total unit count by 40.
|
|
|
|
|
progress.totalUnitCount -= 40
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
context.app = localApp
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// App is not yet installed (or we're forcing it to download a new version), so download it before resigning it.
|
|
|
|
|
|
2019-09-10 12:19:46 -07:00
|
|
|
let downloadOperation = DownloadAppOperation(app: app, context: context)
|
2019-06-21 11:20:03 -07:00
|
|
|
downloadOperation.resultHandler = { (result) in
|
2019-07-28 15:08:13 -07:00
|
|
|
guard let app = self.process(result, context: context) else { return }
|
|
|
|
|
context.app = app
|
2019-06-05 18:05:21 -07:00
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
progress.addChild(downloadOperation.progress, withPendingUnitCount: 40)
|
2019-09-10 12:19:46 -07:00
|
|
|
downloadOperation.addDependency(findServerOperation)
|
2019-06-21 11:20:03 -07:00
|
|
|
resignAppOperation.addDependency(downloadOperation)
|
|
|
|
|
operations.append(downloadOperation)
|
2019-06-04 18:29:50 -07:00
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
|
|
|
|
|
/* Send */
|
|
|
|
|
let sendAppOperation = SendAppOperation(context: context)
|
|
|
|
|
sendAppOperation.resultHandler = { (result) in
|
2020-01-08 12:41:02 -08:00
|
|
|
guard let installationConnection = self.process(result, context: context) else { return }
|
|
|
|
|
context.installationConnection = installationConnection
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
|
|
|
|
progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10)
|
|
|
|
|
sendAppOperation.addDependency(resignAppOperation)
|
|
|
|
|
operations.append(sendAppOperation)
|
|
|
|
|
|
|
|
|
|
|
2019-09-21 22:31:10 -07:00
|
|
|
let beginInstallationHandler = group.beginInstallationHandler
|
|
|
|
|
group.beginInstallationHandler = { (installedApp) in
|
|
|
|
|
if installedApp.bundleIdentifier == StoreApp.altstoreAppID
|
|
|
|
|
{
|
|
|
|
|
self.scheduleExpirationWarningLocalNotification(for: installedApp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
beginInstallationHandler?(installedApp)
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
/* Install */
|
|
|
|
|
let installOperation = InstallAppOperation(context: context)
|
|
|
|
|
installOperation.resultHandler = { (result) in
|
2019-07-19 16:09:45 -07:00
|
|
|
if let error = result.error
|
|
|
|
|
{
|
|
|
|
|
context.error = error
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
if let installedApp = result.value
|
|
|
|
|
{
|
2019-07-31 14:07:00 -07:00
|
|
|
if let app = app as? StoreApp, let storeApp = installedApp.managedObjectContext?.object(with: app.objectID) as? StoreApp
|
2019-07-28 15:08:13 -07:00
|
|
|
{
|
|
|
|
|
installedApp.storeApp = storeApp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
context.installedApp = installedApp
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-19 16:09:45 -07:00
|
|
|
self.finishAppOperation(context) // Finish operation no matter what.
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
|
|
|
|
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
|
|
|
|
installOperation.addDependency(sendAppOperation)
|
|
|
|
|
operations.append(installOperation)
|
2019-07-19 16:10:30 -07:00
|
|
|
|
|
|
|
|
group.set(progress, for: app)
|
2019-06-04 18:29:50 -07:00
|
|
|
}
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2020-01-08 12:41:02 -08:00
|
|
|
// Refresh anisette data after downloading all apps to prevent session from expiring.
|
|
|
|
|
for case let downloadOperation as DownloadAppOperation in operations
|
|
|
|
|
{
|
|
|
|
|
refreshAnisetteDataOperation.addDependency(downloadOperation)
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
group.addOperations(operations)
|
2019-06-10 15:03:47 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
return group
|
2019-06-04 18:29:50 -07:00
|
|
|
}
|
2019-06-18 18:31:59 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
@discardableResult func process<T>(_ result: Result<T, Error>, context: AppOperationContext) -> T?
|
2019-05-31 18:24:08 -07:00
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
do
|
2019-07-16 14:22:45 -07:00
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
let value = try result.get()
|
|
|
|
|
return value
|
|
|
|
|
}
|
2019-07-16 14:22:45 -07:00
|
|
|
catch OperationError.cancelled
|
|
|
|
|
{
|
|
|
|
|
context.error = OperationError.cancelled
|
|
|
|
|
self.finishAppOperation(context)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
context.error = error
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func finishAppOperation(_ context: AppOperationContext)
|
|
|
|
|
{
|
|
|
|
|
self.processingQueue.sync {
|
2019-07-16 14:22:45 -07:00
|
|
|
guard !context.isFinished else { return }
|
|
|
|
|
context.isFinished = true
|
|
|
|
|
|
2019-10-28 13:16:55 -07:00
|
|
|
if let progress = self.refreshProgress[context.bundleIdentifier], progress == context.group.progress(forAppWithBundleIdentifier: context.bundleIdentifier)
|
|
|
|
|
{
|
|
|
|
|
// Only remove progress if it hasn't been replaced by another one.
|
|
|
|
|
self.refreshProgress[context.bundleIdentifier] = nil
|
|
|
|
|
}
|
2019-07-28 15:08:13 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
if let error = context.error
|
2019-06-04 18:29:50 -07:00
|
|
|
{
|
2019-09-03 23:47:47 -07:00
|
|
|
switch error
|
|
|
|
|
{
|
|
|
|
|
case let error as ALTServerError where error.code == .deviceNotFound || error.code == .lostConnection:
|
|
|
|
|
if let server = context.group.server, server.isPreferred
|
|
|
|
|
{
|
|
|
|
|
// Preferred server, so report errors normally.
|
|
|
|
|
context.group.results[context.bundleIdentifier] = .failure(error)
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Not preferred server, so ignore these specific errors and throw serverNotFound instead.
|
|
|
|
|
context.group.results[context.bundleIdentifier] = .failure(ConnectionError.serverNotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case let error:
|
|
|
|
|
context.group.results[context.bundleIdentifier] = .failure(error)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-31 18:24:08 -07:00
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
else if let installedApp = context.installedApp
|
2019-06-04 18:29:50 -07:00
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
context.group.results[context.bundleIdentifier] = .success(installedApp)
|
2019-06-21 11:20:03 -07:00
|
|
|
|
|
|
|
|
// Save after each installation.
|
2019-07-24 13:52:58 -07:00
|
|
|
installedApp.managedObjectContext?.performAndWait {
|
2019-06-21 11:20:03 -07:00
|
|
|
do { try installedApp.managedObjectContext?.save() }
|
|
|
|
|
catch { print("Error saving installed app.", error) }
|
2019-06-04 18:29:50 -07:00
|
|
|
}
|
2019-12-11 12:26:48 -08:00
|
|
|
}
|
2019-07-19 16:10:30 -07:00
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
print("Finished operation!", context.bundleIdentifier)
|
2019-06-21 11:20:03 -07:00
|
|
|
|
|
|
|
|
if context.group.results.count == context.group.progress.totalUnitCount
|
|
|
|
|
{
|
|
|
|
|
context.group.completionHandler?(.success(context.group.results))
|
2019-09-21 22:31:10 -07:00
|
|
|
|
|
|
|
|
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
|
|
|
|
backgroundContext.performAndWait {
|
|
|
|
|
guard let altstore = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: backgroundContext) else { return }
|
|
|
|
|
self.scheduleExpirationWarningLocalNotification(for: altstore)
|
|
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
2019-06-04 18:29:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
2019-09-21 22:31:10 -07:00
|
|
|
|
|
|
|
|
func scheduleExpirationWarningLocalNotification(for app: InstalledApp)
|
|
|
|
|
{
|
|
|
|
|
let notificationDate = app.expirationDate.addingTimeInterval(-1 * 60 * 60 * 24) // 24 hours before expiration.
|
|
|
|
|
|
|
|
|
|
let timeIntervalUntilNotification = notificationDate.timeIntervalSinceNow
|
|
|
|
|
guard timeIntervalUntilNotification > 0 else {
|
|
|
|
|
// Crashes if we pass negative value to UNTimeIntervalNotificationTrigger initializer.
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeIntervalUntilNotification, repeats: false)
|
|
|
|
|
|
|
|
|
|
let content = UNMutableNotificationContent()
|
|
|
|
|
content.title = NSLocalizedString("AltStore Expiring Soon", comment: "")
|
|
|
|
|
content.body = NSLocalizedString("AltStore will expire in 24 hours. Open the app and refresh it to prevent it from expiring.", comment: "")
|
|
|
|
|
content.sound = .default
|
|
|
|
|
|
|
|
|
|
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
|
|
|
|
|
UNUserNotificationCenter.current().add(request)
|
|
|
|
|
}
|
2019-05-31 18:24:08 -07:00
|
|
|
}
|