Files
SideStore/SideStoreApp/Sources/SideStoreAppKit/Operations/BackupAppOperation.swift

159 lines
6.4 KiB
Swift
Raw Normal View History

//
// BackupAppOperation.swift
// AltStore
//
// Created by Riley Testut on 5/12/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
2023-03-01 00:48:36 -05:00
import SideStoreCore
2023-03-01 00:48:36 -05:00
extension BackupAppOperation {
enum Action: String {
case backup
case restore
}
}
@objc(BackupAppOperation)
2023-03-01 00:48:36 -05:00
class BackupAppOperation: ResultOperation<Void> {
let action: Action
let context: InstallAppOperationContext
2023-03-01 00:48:36 -05:00
private var appName: String?
private var timeoutTimer: Timer?
2023-03-01 00:48:36 -05:00
init(action: Action, context: InstallAppOperationContext) {
self.action = action
self.context = context
2023-03-01 00:48:36 -05:00
super.init()
}
2023-03-01 00:48:36 -05:00
override func main() {
super.main()
2023-03-01 00:48:36 -05:00
do {
if let error = self.context.error {
throw error
}
2023-03-01 00:48:36 -05:00
guard let installedApp = context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
context.perform {
2023-03-01 00:48:36 -05:00
do {
let appName = installedApp.name
self.appName = appName
2023-03-01 00:48:36 -05:00
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
let altstoreOpenURL = altstoreApp.openAppURL
2023-03-01 00:48:36 -05:00
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
returnURLComponents?.host = "appBackupResponse"
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
2023-03-01 00:48:36 -05:00
var openURLComponents = URLComponents()
openURLComponents.scheme = installedApp.openAppURL.scheme
openURLComponents.host = self.action.rawValue
openURLComponents.queryItems = [URLQueryItem(name: "returnURL", value: returnURL.absoluteString)]
2023-03-01 00:48:36 -05:00
guard let openURL = openURLComponents.url else { throw OperationError.openAppFailed(name: appName) }
2023-03-01 00:48:36 -05:00
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let currentTime = CFAbsoluteTimeGetCurrent()
2023-03-01 00:48:36 -05:00
UIApplication.shared.open(openURL, options: [:]) { success in
let elapsedTime = CFAbsoluteTimeGetCurrent() - currentTime
2023-03-01 00:48:36 -05:00
if success {
self.registerObservers()
2023-03-01 00:48:36 -05:00
} else if elapsedTime < 0.5 {
// Failed too quickly for human to respond to alert, possibly still finalizing installation.
// Try again in a couple seconds.
2023-03-01 00:48:36 -05:00
print("Failed too quickly, retrying after a few seconds...")
2023-03-01 00:48:36 -05:00
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
2023-03-01 00:48:36 -05:00
UIApplication.shared.open(openURL, options: [:]) { success in
if success {
self.registerObservers()
2023-03-01 00:48:36 -05:00
} else {
self.finish(.failure(OperationError.openAppFailed(name: appName)))
}
}
}
2023-03-01 00:48:36 -05:00
} else {
self.finish(.failure(OperationError.openAppFailed(name: appName)))
}
}
}
2023-03-01 00:48:36 -05:00
} catch {
self.finish(.failure(error))
}
}
2023-03-01 00:48:36 -05:00
} catch {
finish(.failure(error))
}
}
2023-03-01 00:48:36 -05:00
override func finish(_ result: Result<Void, Error>) {
let result = result.mapError { error -> Error in
let appName = self.appName ?? self.context.bundleIdentifier
2023-03-01 00:48:36 -05:00
switch (error, self.action) {
case let (error as NSError, _) where (self.context.error as NSError?) == error: fallthrough
case (OperationError.cancelled, _):
return error
2023-03-01 00:48:36 -05:00
case let (error as NSError, .backup):
let localizedFailure = String(format: NSLocalizedString("Could not back up “%@”.", comment: ""), appName)
return error.withLocalizedFailure(localizedFailure)
2023-03-01 00:48:36 -05:00
case let (error as NSError, .restore):
let localizedFailure = String(format: NSLocalizedString("Could not restore “%@”.", comment: ""), appName)
return error.withLocalizedFailure(localizedFailure)
}
}
2023-03-01 00:48:36 -05:00
switch result {
case .success: progress.completedUnitCount += 1
case .failure: break
}
2023-03-01 00:48:36 -05:00
super.finish(result)
}
}
2023-03-01 00:48:36 -05:00
private extension BackupAppOperation {
func registerObservers() {
var applicationWillReturnObserver: NSObjectProtocol!
2023-03-01 00:48:36 -05:00
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
guard let self = self, !self.isFinished else { return }
2023-03-01 00:48:36 -05:00
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in
// Final delay to ensure we don't prematurely return failure
// in case timer expired while we were in background, but
// are now returning to app with success response.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
guard let self = self, !self.isFinished else { return }
self.finish(.failure(OperationError.timedOut))
}
}
2023-03-01 00:48:36 -05:00
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
}
2023-03-01 00:48:36 -05:00
var backupResponseObserver: NSObjectProtocol!
2023-03-01 19:09:33 -05:00
backupResponseObserver = NotificationCenter.default.addObserver(forName: SideStoreAppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] notification in
self?.timeoutTimer?.invalidate()
2023-03-01 00:48:36 -05:00
2023-03-01 19:09:33 -05:00
let result = notification.userInfo?[SideStoreAppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
self?.finish(result)
2023-03-01 00:48:36 -05:00
NotificationCenter.default.removeObserver(backupResponseObserver!)
}
}
}