Adds option to explicitly back up installed apps

This commit is contained in:
Riley Testut
2020-05-19 11:47:43 -07:00
parent da2370d9ac
commit fff128e1ce
2 changed files with 162 additions and 8 deletions

View File

@@ -389,6 +389,31 @@ extension AppManager
} }
} }
func backup(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
{
let group = RefreshGroup()
group.completionHandler = { (results) in
do
{
guard let result = results.values.first else { throw OperationError.unknown }
let installedApp = try result.get()
assert(installedApp.managedObjectContext != nil)
installedApp.managedObjectContext?.perform {
completionHandler(.success(installedApp))
}
}
catch
{
completionHandler(.failure(error))
}
}
let operation = AppOperation.backup(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
}
func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
{ {
let group = RefreshGroup() let group = RefreshGroup()
@@ -479,14 +504,15 @@ private extension AppManager
case refresh(InstalledApp) case refresh(InstalledApp)
case activate(InstalledApp) case activate(InstalledApp)
case deactivate(InstalledApp) case deactivate(InstalledApp)
case backup(InstalledApp)
case restore(InstalledApp) case restore(InstalledApp)
var app: AppProtocol { var app: AppProtocol {
switch self switch self
{ {
case .install(let app), .update(let app), case .install(let app), .update(let app), .refresh(let app as AppProtocol),
.refresh(let app as AppProtocol), .activate(let app as AppProtocol), .activate(let app as AppProtocol), .deactivate(let app as AppProtocol),
.deactivate(let app as AppProtocol), .restore(let app as AppProtocol): .backup(let app as AppProtocol), .restore(let app as AppProtocol):
return app return app
} }
} }
@@ -606,6 +632,12 @@ private extension AppManager
} }
progress?.addChild(deactivateProgress, withPendingUnitCount: 80) progress?.addChild(deactivateProgress, withPendingUnitCount: 80)
case .backup(let app):
let backupProgress = self._backup(app, operation: operation, group: group) { (result) in
self.finish(operation, result: result, group: group, progress: progress)
}
progress?.addChild(backupProgress, withPendingUnitCount: 80)
case .restore(let app): case .restore(let app):
// Restoring, which is effectively just activating an app. // Restoring, which is effectively just activating an app.
@@ -1017,6 +1049,68 @@ private extension AppManager
return progress return progress
} }
private func _backup(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 100)
let restoreContext = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
let appContext = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
let installBackupAppProgress = Progress.discreteProgress(totalUnitCount: 100)
let installBackupAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
app.managedObjectContext?.perform {
guard let self = self else { return }
let progress = self._installBackupApp(for: app, operation: appOperation, group: group, context: restoreContext) { (result) in
switch result
{
case .success(let installedApp): restoreContext.installedApp = installedApp
case .failure(let error):
restoreContext.error = error
appContext.error = error
}
operation.finish()
}
installBackupAppProgress.addChild(progress, withPendingUnitCount: 100)
}
}
progress.addChild(installBackupAppProgress, withPendingUnitCount: 30)
let backupAppOperation = BackupAppOperation(action: .backup, context: restoreContext)
backupAppOperation.resultHandler = { (result) in
switch result
{
case .success: break
case .failure(let error):
restoreContext.error = error
appContext.error = error
}
}
backupAppOperation.addDependency(installBackupAppOperation)
progress.addChild(backupAppOperation.progress, withPendingUnitCount: 15)
let installAppProgress = Progress.discreteProgress(totalUnitCount: 100)
let installAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
app.managedObjectContext?.perform {
guard let self = self else { return }
let progress = self._install(app, operation: appOperation, group: group, context: appContext) { (result) in
completionHandler(result)
operation.finish()
}
installAppProgress.addChild(progress, withPendingUnitCount: 100)
}
}
installAppOperation.addDependency(backupAppOperation)
progress.addChild(installAppProgress, withPendingUnitCount: 55)
group.add([installBackupAppOperation, backupAppOperation, installAppOperation])
self.run([installBackupAppOperation, installAppOperation, backupAppOperation], context: group.context)
return progress
}
private func _installBackupApp(for app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress private func _installBackupApp(for app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
let progress = Progress.discreteProgress(totalUnitCount: 100) let progress = Progress.discreteProgress(totalUnitCount: 100)
@@ -1176,7 +1270,7 @@ private extension AppManager
event = nil event = nil
case .update: event = .updatedApp(installedApp) case .update: event = .updatedApp(installedApp)
case .activate, .deactivate, .restore: event = nil case .activate, .deactivate, .backup, .restore: event = nil
} }
if let event = event if let event = event
@@ -1234,7 +1328,7 @@ private extension AppManager
switch operation switch operation
{ {
case .install, .update: return self.installationProgress[operation.bundleIdentifier] case .install, .update: return self.installationProgress[operation.bundleIdentifier]
case .refresh, .activate, .deactivate, .restore: return self.refreshProgress[operation.bundleIdentifier] case .refresh, .activate, .deactivate, .backup, .restore: return self.refreshProgress[operation.bundleIdentifier]
} }
} }
@@ -1243,7 +1337,7 @@ private extension AppManager
switch operation switch operation
{ {
case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress
case .refresh, .activate, .deactivate, .restore: self.refreshProgress[operation.bundleIdentifier] = progress case .refresh, .activate, .deactivate, .backup, .restore: self.refreshProgress[operation.bundleIdentifier] = progress
} }
} }
} }

View File

@@ -1174,6 +1174,45 @@ private extension MyAppsViewController
self.present(alertController, animated: true, completion: nil) self.present(alertController, animated: true, completion: nil)
} }
func backup(_ installedApp: InstalledApp)
{
let title = NSLocalizedString("Start Backup?", comment: "")
let message = NSLocalizedString("This will replace any previous backups. Please leave AltStore open until the backup is complete.", comment: "")
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
alertController.addAction(.cancel)
let actionTitle = String(format: NSLocalizedString("Back Up %@", comment: ""), installedApp.name)
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { (action) in
AppManager.shared.backup(installedApp, presentingViewController: self) { (result) in
do
{
let app = try result.get()
try? app.managedObjectContext?.save()
print("Finished backing up app:", app.bundleIdentifier)
}
catch
{
print("Failed to back up app:", error)
DispatchQueue.main.async {
let toastView = ToastView(error: error)
toastView.show(in: self)
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
}
}
DispatchQueue.main.async {
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
}
}))
self.present(alertController, animated: true, completion: nil)
}
func restore(_ installedApp: InstalledApp) func restore(_ installedApp: InstalledApp)
{ {
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name) let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
@@ -1404,6 +1443,10 @@ extension MyAppsViewController
self.remove(installedApp) self.remove(installedApp)
} }
let backupAction = UIAction(title: NSLocalizedString("Back Up", comment: ""), image: UIImage(systemName: "doc.on.doc")) { (action) in
self.backup(installedApp)
}
let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc")) { (action) in let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc")) { (action) in
self.exportBackup(for: installedApp) self.exportBackup(for: installedApp)
} }
@@ -1419,14 +1462,26 @@ extension MyAppsViewController
if installedApp.isActive if installedApp.isActive
{ {
actions.append(refreshAction) actions.append(refreshAction)
actions.append(deactivateAction)
} }
else else
{ {
actions.append(activateAction) actions.append(activateAction)
} }
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp), !UserDefaults.standard.isLegacyDeactivationSupported if installedApp.isActive
{
actions.append(backupAction)
}
else if let _ = UTTypeCopyDeclaration(installedApp.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?, !UserDefaults.standard.isLegacyDeactivationSupported
{
// Allow backing up inactive apps if they are still installed,
// but on an iOS version that no longer supports legacy deactivation.
// This handles edge case where you can't install more apps until you
// delete some, but can't activate inactive apps again to back them up first.
actions.append(backupAction)
}
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
{ {
var backupExists = false var backupExists = false
var outError: NSError? = nil var outError: NSError? = nil
@@ -1454,6 +1509,11 @@ extension MyAppsViewController
} }
} }
if installedApp.isActive
{
actions.append(deactivateAction)
}
#if DEBUG #if DEBUG
if installedApp.bundleIdentifier != StoreApp.altstoreAppID if installedApp.bundleIdentifier != StoreApp.altstoreAppID