mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Deactivates apps by backing up + deleting them on iOS 13.5+
Deactivating apps by removing their profiles no longer works on iOS 13.5. Instead, AltStore will now back up the app by temporarily replacing it with AltBackup, then remove the app from the phone.
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
BF44CC6C232AEB90004DA9C3 /* LaunchAtLogin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */; };
|
||||
BF44CC6D232AEB90004DA9C3 /* LaunchAtLogin.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF44EEEF246B08BA002A52F2 /* BackupController.swift */; };
|
||||
BF44EEF3246B3A17002A52F2 /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */; };
|
||||
BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */; };
|
||||
BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF45868F229872EA00BD7491 /* AppDelegate.swift */; };
|
||||
BF458694229872EA00BD7491 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF458693229872EA00BD7491 /* Assets.xcassets */; };
|
||||
@@ -375,6 +376,7 @@
|
||||
BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AltStore.swift"; sourceTree = "<group>"; };
|
||||
BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LaunchAtLogin.framework; path = Carthage/Build/Mac/LaunchAtLogin.framework; sourceTree = "<group>"; };
|
||||
BF44EEEF246B08BA002A52F2 /* BackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupController.swift; sourceTree = "<group>"; };
|
||||
BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = "<group>"; };
|
||||
BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppOperation.swift; sourceTree = "<group>"; };
|
||||
BF45868D229872EA00BD7491 /* AltServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltServer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF45868F229872EA00BD7491 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -1171,6 +1173,7 @@
|
||||
BFD247962284D7C100981D42 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */,
|
||||
BFB1169C22932DB100BB457C /* apps.json */,
|
||||
BFD247762284B9A700981D42 /* Assets.xcassets */,
|
||||
BF770E6822BD57DD002A40FE /* Silence.m4a */,
|
||||
@@ -1588,6 +1591,7 @@
|
||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
||||
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
|
||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||
BF44EEF3246B3A17002A52F2 /* AltBackup.ipa in Resources */,
|
||||
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
|
||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
|
||||
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */,
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
<string>altstore</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltStore Backup</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altstore-com.rileytestut.AltStore</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
||||
@@ -74,6 +74,15 @@ extension AppManager
|
||||
|
||||
guard !self.isActivelyManagingApp(withBundleID: app.bundleIdentifier) else { continue }
|
||||
|
||||
if !UserDefaults.standard.isLegacyDeactivationSupported
|
||||
{
|
||||
// We can't (ab)use provisioning profiles to deactivate apps,
|
||||
// which means we must delete apps to free up active slots.
|
||||
// So, only check if active apps are installed to prevent
|
||||
// false positives when checking inactive apps.
|
||||
guard app.isActive else { continue }
|
||||
}
|
||||
|
||||
let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||
if uti == nil && !legacySideloadedApps.contains(app.bundleIdentifier)
|
||||
{
|
||||
@@ -329,19 +338,49 @@ extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
func deactivate(_ installedApp: InstalledApp, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
func deactivate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
{
|
||||
let context = OperationContext()
|
||||
|
||||
let findServerOperation = self.findServer(context: context) { _ in }
|
||||
|
||||
let deactivateAppOperation = DeactivateAppOperation(app: installedApp, context: context)
|
||||
deactivateAppOperation.resultHandler = { (result) in
|
||||
completionHandler(result)
|
||||
if UserDefaults.standard.isLegacyDeactivationSupported
|
||||
{
|
||||
// Normally we pipe everything down into perform(),
|
||||
// but the pre-iOS 13.5 deactivation method doesn't require
|
||||
// authentication, so we keep it separate.
|
||||
let context = OperationContext()
|
||||
|
||||
let findServerOperation = self.findServer(context: context) { _ in }
|
||||
|
||||
let deactivateAppOperation = DeactivateAppOperation(app: installedApp, context: context)
|
||||
deactivateAppOperation.resultHandler = { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
deactivateAppOperation.addDependency(findServerOperation)
|
||||
|
||||
self.run([deactivateAppOperation], context: context, requiresSerialQueue: true)
|
||||
}
|
||||
else
|
||||
{
|
||||
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.deactivate(installedApp)
|
||||
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||
}
|
||||
deactivateAppOperation.addDependency(findServerOperation)
|
||||
|
||||
self.run([deactivateAppOperation], context: context, requiresSerialQueue: true)
|
||||
}
|
||||
|
||||
func remove(_ installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
@@ -405,12 +444,15 @@ private extension AppManager
|
||||
{
|
||||
case install(AppProtocol)
|
||||
case update(AppProtocol)
|
||||
case refresh(AppProtocol)
|
||||
case refresh(InstalledApp)
|
||||
case deactivate(InstalledApp)
|
||||
|
||||
var app: AppProtocol {
|
||||
switch self
|
||||
{
|
||||
case .install(let app), .update(let app), .refresh(let app): return app
|
||||
case .install(let app), .update(let app),
|
||||
.refresh(let app as AppProtocol), .deactivate(let app as AppProtocol):
|
||||
return app
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,22 +527,43 @@ private extension AppManager
|
||||
|
||||
switch operation
|
||||
{
|
||||
case .refresh(let installedApp as InstalledApp) where installedApp.certificateSerialNumber == group.context.certificate?.serialNumber:
|
||||
// Refreshing apps, but using same certificate as last time, so we can just refresh provisioning profiles.
|
||||
|
||||
let refreshProgress = self._refresh(installedApp, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
|
||||
|
||||
case .refresh(let app), .install(let app), .update(let app):
|
||||
// Either installing for first time, or refreshing with a different signing certificate,
|
||||
// so we need to resign the app then install it.
|
||||
|
||||
case .install(let app), .update(let app):
|
||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(installProgress, withPendingUnitCount: 80)
|
||||
|
||||
case .activate(let app): fallthrough
|
||||
case .refresh(let app):
|
||||
// Check if backup app is installed in place of real app.
|
||||
let uti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||
|
||||
if app.certificateSerialNumber == group.context.certificate?.serialNumber && uti == nil
|
||||
{
|
||||
// Refreshing with same certificate as last time, and backup app isn't still installed,
|
||||
// so we can just refresh provisioning profiles.
|
||||
|
||||
let refreshProgress = self._refresh(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Refreshing using different certificate or backup app is still installed,
|
||||
// so we need to resign + install.
|
||||
|
||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(installProgress, withPendingUnitCount: 80)
|
||||
}
|
||||
|
||||
case .deactivate(let app):
|
||||
let deactivateProgress = self._deactivate(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(deactivateProgress, withPendingUnitCount: 80)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -528,11 +591,13 @@ private extension AppManager
|
||||
return group
|
||||
}
|
||||
|
||||
private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let context = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
let context = context ?? InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
assert(context.authenticatedContext === group.context)
|
||||
|
||||
context.beginInstallationHandler = { (installedApp) in
|
||||
switch operation
|
||||
{
|
||||
@@ -578,6 +643,7 @@ private extension AppManager
|
||||
}
|
||||
progress.addChild(downloadOperation.progress, withPendingUnitCount: 25)
|
||||
|
||||
|
||||
/* Verify App */
|
||||
let verifyOperation = VerifyAppOperation(context: context)
|
||||
verifyOperation.resultHandler = { (result) in
|
||||
@@ -589,6 +655,7 @@ private extension AppManager
|
||||
}
|
||||
verifyOperation.addDependency(downloadOperation)
|
||||
|
||||
|
||||
/* Refresh Anisette Data */
|
||||
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
||||
refreshAnisetteDataOperation.resultHandler = { (result) in
|
||||
@@ -648,6 +715,8 @@ private extension AppManager
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let installedApp):
|
||||
context.installedApp = installedApp
|
||||
|
||||
if let app = app as? StoreApp, let storeApp = installedApp.managedObjectContext?.object(with: app.objectID) as? StoreApp
|
||||
{
|
||||
installedApp.storeApp = storeApp
|
||||
@@ -678,7 +747,7 @@ private extension AppManager
|
||||
|
||||
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
context.app = ALTApplication(fileURL: app.url)
|
||||
|
||||
|
||||
/* Fetch Provisioning Profiles */
|
||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
||||
@@ -689,7 +758,7 @@ private extension AppManager
|
||||
}
|
||||
}
|
||||
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
|
||||
|
||||
|
||||
/* Refresh */
|
||||
let refreshAppOperation = RefreshAppOperation(context: context)
|
||||
refreshAppOperation.resultHandler = { (result) in
|
||||
@@ -718,6 +787,140 @@ private extension AppManager
|
||||
let operations = [fetchProvisioningProfilesOperation, refreshAppOperation]
|
||||
group.add(operations)
|
||||
self.run(operations, context: group.context)
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
private func _deactivate(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let context = 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: context) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): context.installedApp = installedApp
|
||||
case .failure(let error): context.error = error
|
||||
}
|
||||
|
||||
operation.finish()
|
||||
}
|
||||
installBackupAppProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
}
|
||||
progress.addChild(installBackupAppProgress, withPendingUnitCount: 70)
|
||||
|
||||
let backupAppOperation = BackupAppOperation(action: .backup, context: context)
|
||||
backupAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): context.error = error
|
||||
case .success: break
|
||||
}
|
||||
}
|
||||
backupAppOperation.addDependency(installBackupAppOperation)
|
||||
progress.addChild(backupAppOperation.progress, withPendingUnitCount: 15)
|
||||
|
||||
let removeAppOperation = RemoveAppOperation(context: context)
|
||||
removeAppOperation.resultHandler = { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
removeAppOperation.addDependency(backupAppOperation)
|
||||
progress.addChild(removeAppOperation.progress, withPendingUnitCount: 15)
|
||||
|
||||
group.add([installBackupAppOperation, backupAppOperation, removeAppOperation])
|
||||
self.run([installBackupAppOperation, backupAppOperation, removeAppOperation], 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
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
guard let application = ALTApplication(fileURL: app.fileURL) else {
|
||||
completionHandler(.failure(OperationError.appNotFound))
|
||||
return progress
|
||||
}
|
||||
|
||||
let prepareProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
let prepareOperation = RSTAsyncBlockOperation { (operation) in
|
||||
app.managedObjectContext?.perform {
|
||||
do
|
||||
{
|
||||
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound }
|
||||
|
||||
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
|
||||
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
|
||||
|
||||
if var infoDictionary = unzippedAppBundle.infoDictionary
|
||||
{
|
||||
// Replace name + bundle identifier so AltStore treats it as the same app.
|
||||
infoDictionary["CFBundleDisplayName"] = app.name
|
||||
infoDictionary[kCFBundleIdentifierKey as String] = app.bundleIdentifier
|
||||
|
||||
// Add app-specific exported UTI so we can check later if this temporary backup app is still installed or not.
|
||||
let installedAppUTI = ["UTTypeConformsTo": [],
|
||||
"UTTypeDescription": "AltStore Backup App",
|
||||
"UTTypeIconFiles": [],
|
||||
"UTTypeIdentifier": app.installedBackupAppUTI,
|
||||
"UTTypeTagSpecification": [:]] as [String : Any]
|
||||
|
||||
var exportedUTIs = infoDictionary[Bundle.Info.exportedUTIs] as? [[String: Any]] ?? []
|
||||
exportedUTIs.append(installedAppUTI)
|
||||
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: unzippedAppBundle.infoPlistURL)
|
||||
}
|
||||
|
||||
guard let backupApp = ALTApplication(fileURL: unzippedAppBundleURL) else { throw OperationError.invalidApp }
|
||||
context.app = backupApp
|
||||
|
||||
prepareProgress.completedUnitCount += 1
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
progress.addChild(prepareProgress, withPendingUnitCount: 20)
|
||||
|
||||
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
guard let self = self else { return }
|
||||
|
||||
guard let backupApp = context.app else {
|
||||
context.error = OperationError.invalidApp
|
||||
operation.finish()
|
||||
return
|
||||
}
|
||||
|
||||
var appGroups = application.entitlements[.appGroups] as? [String] ?? []
|
||||
appGroups.append(Bundle.baseAltStoreAppGroupID)
|
||||
|
||||
let additionalEntitlements: [ALTEntitlement: Any] = [.appGroups: appGroups]
|
||||
let progress = self._install(backupApp, operation: appOperation, group: group, context: context, additionalEntitlements: additionalEntitlements, cacheApp: false) { (result) in
|
||||
completionHandler(result)
|
||||
operation.finish()
|
||||
}
|
||||
installProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
installOperation.addDependency(prepareOperation)
|
||||
progress.addChild(installProgress, withPendingUnitCount: 80)
|
||||
|
||||
group.add([prepareOperation, installOperation])
|
||||
self.run([prepareOperation, installOperation], context: group.context)
|
||||
|
||||
return progress
|
||||
}
|
||||
@@ -775,6 +978,7 @@ private extension AppManager
|
||||
event = nil
|
||||
|
||||
case .update: event = .updatedApp(installedApp)
|
||||
case .deactivate: event = nil
|
||||
}
|
||||
|
||||
if let event = event
|
||||
@@ -819,17 +1023,8 @@ private extension AppManager
|
||||
switch operation
|
||||
{
|
||||
case _ where requiresSerialQueue: fallthrough
|
||||
case is InstallAppOperation, is RefreshAppOperation:
|
||||
if let context = context, let previousOperation = self.serialOperationQueue.operations.last(where: { context.operations.contains($0) })
|
||||
{
|
||||
// Ensure operations execute in the order they're added (in same context), since they may become ready at different points.
|
||||
operation.addDependency(previousOperation)
|
||||
}
|
||||
|
||||
self.serialOperationQueue.addOperation(operation)
|
||||
|
||||
default:
|
||||
self.operationQueue.addOperation(operation)
|
||||
case is InstallAppOperation, is RefreshAppOperation, is BackupAppOperation: self.serialOperationQueue.addOperation(operation)
|
||||
default: self.operationQueue.addOperation(operation)
|
||||
}
|
||||
|
||||
context?.operations.add(operation)
|
||||
@@ -841,7 +1036,7 @@ private extension AppManager
|
||||
switch operation
|
||||
{
|
||||
case .install, .update: return self.installationProgress[operation.bundleIdentifier]
|
||||
case .refresh: return self.refreshProgress[operation.bundleIdentifier]
|
||||
case .refresh, .deactivate: return self.refreshProgress[operation.bundleIdentifier]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,7 +1045,7 @@ private extension AppManager
|
||||
switch operation
|
||||
{
|
||||
case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress
|
||||
case .refresh: self.refreshProgress[operation.bundleIdentifier] = progress
|
||||
case .refresh, .deactivate: self.refreshProgress[operation.bundleIdentifier] = progress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,12 @@ extension InstalledApp
|
||||
return installedAppUTI
|
||||
}
|
||||
|
||||
class func installedBackupAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
||||
{
|
||||
let installedBackupAppUTI = InstalledApp.installedAppUTI(forBundleIdentifier: bundleIdentifier) + ".backup"
|
||||
return installedBackupAppUTI
|
||||
}
|
||||
|
||||
var directoryURL: URL {
|
||||
return InstalledApp.directoryURL(for: self)
|
||||
}
|
||||
@@ -262,4 +268,8 @@ extension InstalledApp
|
||||
var installedAppUTI: String {
|
||||
return InstalledApp.installedAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||
}
|
||||
|
||||
var installedBackupAppUTI: String {
|
||||
return InstalledApp.installedBackupAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,6 +404,15 @@ private extension MyAppsViewController
|
||||
|
||||
// Ensure no leftover progress from active apps cell reuse.
|
||||
cell.bannerView.button.progress = nil
|
||||
|
||||
if let progress = AppManager.shared.refreshProgress(for: installedApp), progress.fractionCompleted < 1.0
|
||||
{
|
||||
cell.bannerView.button.progress = progress
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.bannerView.button.progress = nil
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||
let fileURL = item.fileURL
|
||||
@@ -887,7 +896,7 @@ private extension MyAppsViewController
|
||||
guard installedApp.isActive else { return }
|
||||
installedApp.isActive = false
|
||||
|
||||
AppManager.shared.deactivate(installedApp) { (result) in
|
||||
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
|
||||
@@ -63,7 +63,7 @@ class AuthenticatedOperationContext: OperationContext
|
||||
class AppOperationContext
|
||||
{
|
||||
let bundleIdentifier: String
|
||||
private let authenticatedContext: AuthenticatedOperationContext
|
||||
let authenticatedContext: AuthenticatedOperationContext
|
||||
|
||||
var app: ALTApplication?
|
||||
var provisioningProfiles: [String: ALTProvisioningProfile]?
|
||||
|
||||
@@ -21,6 +21,10 @@ class RefreshGroup: NSObject
|
||||
|
||||
private(set) var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
// Keep strong references to managed object contexts
|
||||
// so they don't die out from under us.
|
||||
private(set) var _contexts = Set<NSManagedObjectContext>()
|
||||
|
||||
private var isFinished = false
|
||||
|
||||
private let dispatchGroup = DispatchGroup()
|
||||
@@ -33,6 +37,8 @@ class RefreshGroup: NSObject
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// Used to keep track of which operations belong to this group.
|
||||
/// This does _not_ add them to any operation queue.
|
||||
func add(_ operations: [Foundation.Operation])
|
||||
{
|
||||
for operation in operations
|
||||
@@ -57,6 +63,14 @@ class RefreshGroup: NSObject
|
||||
func set(_ result: Result<InstalledApp, Error>, forAppWithBundleIdentifier bundleIdentifier: String)
|
||||
{
|
||||
self.results[bundleIdentifier] = result
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure: break
|
||||
case .success(let installedApp):
|
||||
guard let context = installedApp.managedObjectContext else { break }
|
||||
self._contexts.insert(context)
|
||||
}
|
||||
}
|
||||
|
||||
func cancel()
|
||||
|
||||
@@ -89,8 +89,13 @@ private extension SendAppOperation
|
||||
connection.send(appData, prependSize: false) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success: completionHandler(.success(()))
|
||||
case .failure(let error):
|
||||
print("Failed to send app data (\(appData.count) bytes)")
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Successfully sent app data (\(appData.count) bytes)")
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
AltStore/Resources/AltBackup.ipa
Normal file
BIN
AltStore/Resources/AltBackup.ipa
Normal file
Binary file not shown.
Reference in New Issue
Block a user