From f5fc64be4456bbba6464e6c145b7ceb7edde7e24 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 14 May 2020 10:29:06 -0700 Subject: [PATCH 01/37] =?UTF-8?q?[AltServer]=20Supports=20=E2=80=9Cremove?= =?UTF-8?q?=20app=E2=80=9D=20requests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves support for removing apps --- AltKit/NSError+ALTServerError.h | 4 +- AltKit/NSError+ALTServerError.m | 3 + AltKit/ServerProtocol.swift | 39 ++++++ AltServer/Connections/ConnectionManager.swift | 29 +++- AltServer/Devices/ALTDeviceManager.h | 1 + AltServer/Devices/ALTDeviceManager.mm | 124 +++++++++++++++++- 6 files changed, 197 insertions(+), 3 deletions(-) diff --git a/AltKit/NSError+ALTServerError.h b/AltKit/NSError+ALTServerError.h index a63c6905..0fcf48d3 100644 --- a/AltKit/NSError+ALTServerError.h +++ b/AltKit/NSError+ALTServerError.h @@ -39,7 +39,9 @@ typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError) ALTServerErrorInvalidAnisetteData = 13, ALTServerErrorPluginNotFound = 14, - ALTServerErrorProfileNotFound = 15 + ALTServerErrorProfileNotFound = 15, + + ALTServerErrorAppDeletionFailed = 16, }; NS_ASSUME_NONNULL_BEGIN diff --git a/AltKit/NSError+ALTServerError.m b/AltKit/NSError+ALTServerError.m index 03cdd072..c82da672 100644 --- a/AltKit/NSError+ALTServerError.m +++ b/AltKit/NSError+ALTServerError.m @@ -95,6 +95,9 @@ NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdenti case ALTServerErrorProfileNotFound: return [self profileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not find profile", "")]; + + case ALTServerErrorAppDeletionFailed: + return NSLocalizedString(@"An error occured while removing the app.", @""); } } diff --git a/AltKit/ServerProtocol.swift b/AltKit/ServerProtocol.swift index 8ff666d3..a386eb42 100644 --- a/AltKit/ServerProtocol.swift +++ b/AltKit/ServerProtocol.swift @@ -24,6 +24,7 @@ public enum ServerRequest: Decodable case beginInstallation(BeginInstallationRequest) case installProvisioningProfiles(InstallProvisioningProfilesRequest) case removeProvisioningProfiles(RemoveProvisioningProfilesRequest) + case removeApp(RemoveAppRequest) case unknown(identifier: String, version: Int) var identifier: String { @@ -34,6 +35,7 @@ public enum ServerRequest: Decodable case .beginInstallation(let request): return request.identifier case .installProvisioningProfiles(let request): return request.identifier case .removeProvisioningProfiles(let request): return request.identifier + case .removeApp(let request): return request.identifier case .unknown(let identifier, _): return identifier } } @@ -46,6 +48,7 @@ public enum ServerRequest: Decodable case .beginInstallation(let request): return request.version case .installProvisioningProfiles(let request): return request.version case .removeProvisioningProfiles(let request): return request.version + case .removeApp(let request): return request.version case .unknown(_, let version): return version } } @@ -85,6 +88,10 @@ public enum ServerRequest: Decodable let request = try RemoveProvisioningProfilesRequest(from: decoder) self = .removeProvisioningProfiles(request) + case "RemoveAppRequest": + let request = try RemoveAppRequest(from: decoder) + self = .removeApp(request) + default: self = .unknown(identifier: identifier, version: version) } @@ -97,6 +104,7 @@ public enum ServerResponse: Decodable case installationProgress(InstallationProgressResponse) case installProvisioningProfiles(InstallProvisioningProfilesResponse) case removeProvisioningProfiles(RemoveProvisioningProfilesResponse) + case removeApp(RemoveAppResponse) case error(ErrorResponse) case unknown(identifier: String, version: Int) @@ -107,6 +115,7 @@ public enum ServerResponse: Decodable case .installationProgress(let response): return response.identifier case .installProvisioningProfiles(let response): return response.identifier case .removeProvisioningProfiles(let response): return response.identifier + case .removeApp(let response): return response.identifier case .error(let response): return response.identifier case .unknown(let identifier, _): return identifier } @@ -119,6 +128,7 @@ public enum ServerResponse: Decodable case .installationProgress(let response): return response.version case .installProvisioningProfiles(let response): return response.version case .removeProvisioningProfiles(let response): return response.version + case .removeApp(let response): return response.version case .error(let response): return response.version case .unknown(_, let version): return version } @@ -155,6 +165,10 @@ public enum ServerResponse: Decodable let response = try RemoveProvisioningProfilesResponse(from: decoder) self = .removeProvisioningProfiles(response) + case "RemoveAppResponse": + let response = try RemoveAppResponse(from: decoder) + self = .removeApp(response) + case "ErrorResponse": let response = try ErrorResponse(from: decoder) self = .error(response) @@ -379,3 +393,28 @@ public struct RemoveProvisioningProfilesResponse: ServerMessageProtocol { } } + +public struct RemoveAppRequest: ServerMessageProtocol +{ + public var version = 1 + public var identifier = "RemoveAppRequest" + + public var udid: String + public var bundleIdentifier: String + + public init(udid: String, bundleIdentifier: String) + { + self.udid = udid + self.bundleIdentifier = bundleIdentifier + } +} + +public struct RemoveAppResponse: ServerMessageProtocol +{ + public var version = 1 + public var identifier = "RemoveAppResponse" + + public init() + { + } +} diff --git a/AltServer/Connections/ConnectionManager.swift b/AltServer/Connections/ConnectionManager.swift index 6c5203a9..fa665e8d 100644 --- a/AltServer/Connections/ConnectionManager.swift +++ b/AltServer/Connections/ConnectionManager.swift @@ -273,6 +273,9 @@ private extension ConnectionManager case .success(.removeProvisioningProfiles(let request)): self.handleRemoveProvisioningProfilesRequest(request, for: connection) + case .success(.removeApp(let request)): + self.handleRemoveAppRequest(request, for: connection) + case .success(.unknown): let response = ErrorResponse(error: ALTServerError(.unknownRequest)) connection.send(response, shouldDisconnect: true) { (result) in @@ -485,7 +488,31 @@ private extension ConnectionManager let response = RemoveProvisioningProfilesResponse() connection.send(response, shouldDisconnect: true) { (result) in - print("Sent remove profiles error response to \(connection) with result:", result) + print("Sent remove profiles success response to \(connection) with result:", result) + } + } + } + } + + func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: ClientConnection) + { + ALTDeviceManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier, fromDeviceWithUDID: request.udid) { (success, error) in + if let error = error, !success + { + print("Failed to remove app \(request.bundleIdentifier):", error) + + let errorResponse = ErrorResponse(error: ALTServerError(error)) + connection.send(errorResponse, shouldDisconnect: true) { (result) in + print("Sent remove a[[ error response with result:", result) + } + } + else + { + print("Removed app:", request.bundleIdentifier) + + let response = RemoveAppResponse() + connection.send(response, shouldDisconnect: true) { (result) in + print("Sent remove app success response to \(connection) with result:", result) } } } diff --git a/AltServer/Devices/ALTDeviceManager.h b/AltServer/Devices/ALTDeviceManager.h index 8d27d9f3..296e57df 100644 --- a/AltServer/Devices/ALTDeviceManager.h +++ b/AltServer/Devices/ALTDeviceManager.h @@ -28,6 +28,7 @@ extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification /* App Installation */ - (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; +- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; - (void)installProvisioningProfiles:(NSSet *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; - (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; diff --git a/AltServer/Devices/ALTDeviceManager.mm b/AltServer/Devices/ALTDeviceManager.mm index 3711fb11..8055b594 100644 --- a/AltServer/Devices/ALTDeviceManager.mm +++ b/AltServer/Devices/ALTDeviceManager.mm @@ -20,6 +20,7 @@ #include void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid); +void ALTDeviceManagerUpdateAppDeletionStatus(plist_t command, plist_t status, void *uuid); void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data); NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification = @"ALTDeviceManagerDeviceDidConnectNotification"; @@ -28,6 +29,8 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT @interface ALTDeviceManager () @property (nonatomic, readonly) NSMutableDictionary *installationCompletionHandlers; +@property (nonatomic, readonly) NSMutableDictionary *deletionCompletionHandlers; + @property (nonatomic, readonly) NSMutableDictionary *installationProgress; @property (nonatomic, readonly) dispatch_queue_t installationQueue; @@ -54,8 +57,9 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT if (self) { _installationCompletionHandlers = [NSMutableDictionary dictionary]; - _installationProgress = [NSMutableDictionary dictionary]; + _deletionCompletionHandlers = [NSMutableDictionary dictionary]; + _installationProgress = [NSMutableDictionary dictionary]; _installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL); _cachedDevices = [NSMutableSet set]; @@ -498,6 +502,87 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT return success; } +- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler +{ + __block idevice_t device = NULL; + __block lockdownd_client_t client = NULL; + __block instproxy_client_t ipc = NULL; + __block lockdownd_service_descriptor_t service = NULL; + + void (^finish)(NSError *error) = ^(NSError *e) { + __block NSError *error = e; + + lockdownd_service_descriptor_free(service); + instproxy_client_free(ipc); + lockdownd_client_free(client); + idevice_free(device); + + if (error != nil) + { + completionHandler(NO, error); + } + else + { + completionHandler(YES, nil); + } + }; + + /* Find Device */ + if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS) + { + return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]); + } + + /* Connect to Device */ + if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS) + { + return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + /* Connect to Installation Proxy */ + if ((lockdownd_start_service(client, "com.apple.mobile.installation_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL) + { + return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS) + { + return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]); + } + + if (service) + { + lockdownd_service_descriptor_free(service); + service = NULL; + } + + NSUUID *UUID = [NSUUID UUID]; + __block char *uuidString = (char *)malloc(UUID.UUIDString.length + 1); + strncpy(uuidString, (const char *)UUID.UUIDString.UTF8String, UUID.UUIDString.length); + uuidString[UUID.UUIDString.length] = '\0'; + + self.deletionCompletionHandlers[UUID] = ^(NSError *error) { + if (error != nil) + { + NSString *localizedFailure = [NSString stringWithFormat:NSLocalizedString(@"Could not remove “%@”.", @""), bundleIdentifier]; + + NSMutableDictionary *userInfo = [error.userInfo mutableCopy]; + userInfo[NSLocalizedFailureErrorKey] = localizedFailure; + + NSError *localizedError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]; + finish(localizedError); + } + else + { + finish(nil); + } + + free(uuidString); + }; + + instproxy_uninstall(ipc, bundleIdentifier.UTF8String, NULL, ALTDeviceManagerUpdateAppDeletionStatus, uuidString); +} + #pragma mark - Provisioning Profiles - - (void)installProvisioningProfiles:(NSSet *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *error))completionHandler @@ -1117,6 +1202,43 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid) } } +void ALTDeviceManagerUpdateAppDeletionStatus(plist_t command, plist_t status, void *uuid) +{ + NSUUID *UUID = [[NSUUID alloc] initWithUUIDString:[NSString stringWithUTF8String:(const char *)uuid]]; + + char *statusName = NULL; + instproxy_status_get_name(status, &statusName); + + char *errorName = NULL; + char *errorDescription = NULL; + uint64_t code = 0; + instproxy_status_get_error(status, &errorName, &errorDescription, &code); + + if ([@(statusName) isEqualToString:@"Complete"] || code != 0 || errorName != NULL) + { + void (^completionHandler)(NSError *) = ALTDeviceManager.sharedManager.deletionCompletionHandlers[UUID]; + if (completionHandler != nil) + { + if (code != 0 || errorName != NULL) + { + NSLog(@"Error removing app. %@ (%@). %@", @(code), @(errorName ?: ""), @(errorDescription ?: "")); + + NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(errorDescription ?: "")}]; + NSError *error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorAppDeletionFailed userInfo:@{NSUnderlyingErrorKey: underlyingError}]; + + completionHandler(error); + } + else + { + NSLog(@"Finished removing app!"); + completionHandler(nil); + } + + ALTDeviceManager.sharedManager.deletionCompletionHandlers[UUID] = nil; + } + } +} + void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data) { ALTDevice * (^deviceForUDID)(NSString *, NSArray *) = ^ALTDevice *(NSString *udid, NSArray *devices) { From 2fc19f674182026a12a3ec54c53569178bee9729 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 14 May 2020 10:31:00 -0700 Subject: [PATCH 02/37] Fixes RefreshGroup strong reference cycle --- AltStore/Operations/RefreshGroup.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AltStore/Operations/RefreshGroup.swift b/AltStore/Operations/RefreshGroup.swift index c4f3dd72..9d487b6b 100644 --- a/AltStore/Operations/RefreshGroup.swift +++ b/AltStore/Operations/RefreshGroup.swift @@ -46,8 +46,8 @@ class RefreshGroup: NSObject if self.operations.isEmpty && !operations.isEmpty { - self.dispatchGroup.notify(queue: .global()) { - self.finish() + self.dispatchGroup.notify(queue: .global()) { [weak self] in + self?.finish() } } From 484742885f4feb368fe886d3e633533ca5fe5e1b Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 14:54:43 -0700 Subject: [PATCH 03/37] Supports custom entitlements when fetching provisioning profiles --- AltStore/Managing Apps/AppManager.swift | 3 ++- .../FetchProvisioningProfilesOperation.swift | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 614c8ce3..4eb342ea 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -478,7 +478,7 @@ private extension AppManager return group } - private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result) -> Void) -> Progress + private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, additionalEntitlements: [ALTEntitlement: Any]? = nil, completionHandler: @escaping (Result) -> Void) -> Progress { let progress = Progress.discreteProgress(totalUnitCount: 100) @@ -542,6 +542,7 @@ private extension AppManager /* Fetch Provisioning Profiles */ let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context) + fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements fetchProvisioningProfilesOperation.resultHandler = { (result) in switch result { diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift index dfffd645..2110e0a4 100644 --- a/AltStore/Operations/FetchProvisioningProfilesOperation.swift +++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift @@ -16,6 +16,8 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni { let context: AppOperationContext + var additionalEntitlements: [ALTEntitlement: Any]? + private let appGroupsLock = NSLock() init(context: AppOperationContext) @@ -300,14 +302,20 @@ extension FetchProvisioningProfilesOperation func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { - let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in + var entitlements = app.entitlements + for (key, value) in additionalEntitlements ?? [:] + { + entitlements[key] = value + } + + let requiredFeatures = entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in guard let feature = ALTFeature(entitlement: entitlement) else { return nil } return (feature, value) } var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 } - if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty + if let applicationGroups = entitlements[.appGroups] as? [String], !applicationGroups.isEmpty { features[.appGroups] = true } @@ -348,8 +356,14 @@ extension FetchProvisioningProfilesOperation func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { + var entitlements = app.entitlements + for (key, value) in additionalEntitlements ?? [:] + { + entitlements[key] = value + } + // TODO: Handle apps belonging to more than one app group. - guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { + guard let applicationGroups = entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { return completionHandler(.success(appID)) } From a0b5d6d8aeca504e84de8df36224c49753dff95a Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 14 May 2020 11:02:40 -0700 Subject: [PATCH 04/37] Adds additional checks before considering apps deleted --- AltStore/Managing Apps/AppManager.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 4eb342ea..547948cc 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -72,6 +72,8 @@ extension AppManager continue } + guard !self.isActivelyManagingApp(withBundleID: app.bundleIdentifier) else { continue } + let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary? if uti == nil && !legacySideloadedApps.contains(app.bundleIdentifier) { @@ -103,7 +105,7 @@ extension AppManager let resourceValues = try appDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey]) guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue } - if isDirectory && !installedAppBundleIDs.contains(bundleID) && !self.installationProgress.keys.contains(bundleID) + if isDirectory && !installedAppBundleIDs.contains(bundleID) && !self.isActivelyManagingApp(withBundleID: bundleID) { print("DELETING CACHED APP:", bundleID) try FileManager.default.removeItem(at: appDirectory) @@ -386,6 +388,12 @@ private extension AppManager } } + func isActivelyManagingApp(withBundleID bundleID: String) -> Bool + { + let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID) + return isActivelyManaging + } + @discardableResult private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) -> RefreshGroup { From ea6861b1eb32735330bcee060866721d5fab4467 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 14 May 2020 16:31:15 -0700 Subject: [PATCH 05/37] [AltServer] Uses empty strings in place of nil error messages --- AltServer/Devices/ALTDeviceManager.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AltServer/Devices/ALTDeviceManager.mm b/AltServer/Devices/ALTDeviceManager.mm index 8055b594..38f1fefd 100644 --- a/AltServer/Devices/ALTDeviceManager.mm +++ b/AltServer/Devices/ALTDeviceManager.mm @@ -1160,7 +1160,7 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid) { if (code != 0 || name != NULL) { - NSLog(@"Error installing app. %@ (%@). %@", @(code), @(name), @(description)); + NSLog(@"Error installing app. %@ (%@). %@", @(code), @(name ?: ""), @(description ?: "")); NSError *error = nil; @@ -1170,14 +1170,14 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid) } else { - NSString *errorName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding]; + NSString *errorName = [NSString stringWithCString:name ?: "" encoding:NSUTF8StringEncoding]; if ([errorName isEqualToString:@"DeviceOSVersionTooLow"]) { error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnsupportediOSVersion userInfo:nil]; } else { - NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(description)}]; + NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(description ?: "")}]; error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorInstallationFailed userInfo:@{NSUnderlyingErrorKey: underlyingError}]; } } From b9b2afa2003d56c6aaf8e648ff636a1bac8e5dc3 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 10:55:18 -0700 Subject: [PATCH 06/37] Replaces ConnectionError.errorDescription with .failureReason Improves error messages where ConnectionError was the underlying failure, but not the main error. --- AltStore/Server/Server.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AltStore/Server/Server.swift b/AltStore/Server/Server.swift index 568d091d..76af2427 100644 --- a/AltStore/Server/Server.swift +++ b/AltStore/Server/Server.swift @@ -32,7 +32,7 @@ enum ConnectionError: LocalizedError case connectionFailed case connectionDropped - var errorDescription: String? { + var failureReason: String? { switch self { case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "") From 47cf59a1ad814e400e175f84f0cdf8f0b0342832 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 11:35:44 -0700 Subject: [PATCH 07/37] Adds initial AltBackup app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When deactivating an app, AltStore will first install AltBackup in its place. This allows AltBackup to access the (soon to be) inactive app’s sandbox, and backup all files to a shared app group with AltStore. Later when activating, AltStore will again install AltBackup and use it to restore files before installing the actual app again. --- AltBackup/AltBackup.entitlements | 10 + AltBackup/AppDelegate.swift | 121 ++++++++ .../AppIcon.appiconset/Contents.json | 98 ++++++ .../Background.colorset/Contents.json | 38 +++ AltBackup/Assets.xcassets/Contents.json | 6 + .../Text.colorset/Contents.json | 20 ++ AltBackup/BackupController.swift | 293 ++++++++++++++++++ AltBackup/Base.lproj/LaunchScreen.storyboard | 32 ++ AltBackup/Info.plist | 64 ++++ AltBackup/UIColor+AltBackup.swift | 15 + AltBackup/ViewController.swift | 131 ++++++++ AltKit/Bundle+AltStore.swift | 4 + AltStore.xcodeproj/project.pbxproj | 154 ++++++++- 13 files changed, 983 insertions(+), 3 deletions(-) create mode 100644 AltBackup/AltBackup.entitlements create mode 100644 AltBackup/AppDelegate.swift create mode 100644 AltBackup/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 AltBackup/Assets.xcassets/Background.colorset/Contents.json create mode 100644 AltBackup/Assets.xcassets/Contents.json create mode 100644 AltBackup/Assets.xcassets/Text.colorset/Contents.json create mode 100644 AltBackup/BackupController.swift create mode 100644 AltBackup/Base.lproj/LaunchScreen.storyboard create mode 100644 AltBackup/Info.plist create mode 100644 AltBackup/UIColor+AltBackup.swift create mode 100644 AltBackup/ViewController.swift diff --git a/AltBackup/AltBackup.entitlements b/AltBackup/AltBackup.entitlements new file mode 100644 index 00000000..099f1e39 --- /dev/null +++ b/AltBackup/AltBackup.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.rileytestut.AltStore + + + diff --git a/AltBackup/AppDelegate.swift b/AltBackup/AppDelegate.swift new file mode 100644 index 00000000..5d8a21a7 --- /dev/null +++ b/AltBackup/AppDelegate.swift @@ -0,0 +1,121 @@ +// +// AppDelegate.swift +// AltBackup +// +// Created by Riley Testut on 5/11/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import UIKit + +extension AppDelegate +{ + static let startBackupNotification = Notification.Name("io.altstore.StartBackup") + static let startRestoreNotification = Notification.Name("io.altstore.StartRestore") + + static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished") + + static let operationResultKey = "result" +} + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + private var currentBackupReturnURL: URL? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool + { + // Override point for customization after application launch. + + NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil) + + let viewController = ViewController() + + self.window = UIWindow(frame: UIScreen.main.bounds) + self.window?.rootViewController = viewController + self.window?.makeKeyAndVisible() + + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool + { + return self.open(url) + } +} + +private extension AppDelegate +{ + func open(_ url: URL) -> Bool + { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } + guard let command = components.host?.lowercased() else { return false } + + switch command + { + case "backup": + guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false } + self.currentBackupReturnURL = returnURL + NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil) + + return true + + case "restore": + guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false } + self.currentBackupReturnURL = returnURL + NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil) + + return true + + default: return false + } + } + + @objc func operationDidFinish(_ notification: Notification) + { + defer { self.currentBackupReturnURL = nil } + + guard + let returnURL = self.currentBackupReturnURL, + let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result + else { return } + + guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return } + + switch result + { + case .success: + components.path = "/success" + + case .failure(let error as NSError): + components.path = "/failure" + components.queryItems = ["errorDomain": error.domain, + "errorCode": String(error.code), + "errorDescription": error.localizedDescription].map { URLQueryItem(name: $0, value: $1) } + } + + guard let responseURL = components.url else { return } + + DispatchQueue.main.async { + UIApplication.shared.open(responseURL, options: [:]) { (success) in + print("Sent response to app with success:", success) + } + } + } +} + diff --git a/AltBackup/Assets.xcassets/AppIcon.appiconset/Contents.json b/AltBackup/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/AltBackup/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltBackup/Assets.xcassets/Background.colorset/Contents.json b/AltBackup/Assets.xcassets/Background.colorset/Contents.json new file mode 100644 index 00000000..8251d696 --- /dev/null +++ b/AltBackup/Assets.xcassets/Background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.518", + "green" : "0.502", + "red" : "0.004" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.404", + "green" : "0.322", + "red" : "0.008" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltBackup/Assets.xcassets/Contents.json b/AltBackup/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/AltBackup/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltBackup/Assets.xcassets/Text.colorset/Contents.json b/AltBackup/Assets.xcassets/Text.colorset/Contents.json new file mode 100644 index 00000000..a004a7f8 --- /dev/null +++ b/AltBackup/Assets.xcassets/Text.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.750", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AltBackup/BackupController.swift b/AltBackup/BackupController.swift new file mode 100644 index 00000000..e835e335 --- /dev/null +++ b/AltBackup/BackupController.swift @@ -0,0 +1,293 @@ +// +// BackupController.swift +// AltBackup +// +// Created by Riley Testut on 5/12/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +extension ErrorUserInfoKey +{ + static let sourceFile: String = "alt_sourceFile" + static let sourceFileLine: String = "alt_sourceFileLine" +} + +extension Error +{ + var sourceDescription: String? { + guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else { + return nil + } + return "(\((sourceFile as NSString).lastPathComponent), Line \(sourceFileLine))" + } +} + +struct BackupError: ALTLocalizedError +{ + enum Code + { + case invalidBundleID + case appGroupNotFound(String?) + case randomError // Used for debugging. + } + + let code: Code + + let sourceFile: String + let sourceFileLine: Int + + var errorFailure: String? + + var failureReason: String? { + switch self.code + { + case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "") + case .appGroupNotFound(let appGroup): + if let appGroup = appGroup + { + return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup) + } + else + { + return NSLocalizedString("The AltStore app group could not be found.", comment: "") + } + case .randomError: return NSLocalizedString("A random error occured.", comment: "") + } + } + + var errorUserInfo: [String : Any] { + let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription, + NSLocalizedFailureReasonErrorKey: self.failureReason, + NSLocalizedFailureErrorKey: self.errorFailure, + ErrorUserInfoKey.sourceFile: self.sourceFile, + ErrorUserInfoKey.sourceFileLine: self.sourceFileLine] + return userInfo.compactMapValues { $0 } + } + + init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line) + { + self.code = code + self.errorFailure = description + self.sourceFile = file + self.sourceFileLine = line + } +} + +class BackupController: NSObject +{ + private var altstoreAppGroup: String? { + return Bundle.main.appGroups.first + } + + private let fileCoordinator = NSFileCoordinator(filePresenter: nil) + private let operationQueue = OperationQueue() + + override init() + { + self.operationQueue.name = "AltBackup-BackupQueue" + } + + func performBackup(completionHandler: @escaping (Result) -> Void) + { + do + { + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: "")) } + + guard + let altstoreAppGroup = self.altstoreAppGroup, + let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup) + else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) } + + let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups") + + // Use temporary directory to prevent messing up successful backup with incomplete one. + let temporaryAppBackupDirectory = backupsDirectory.appendingPathComponent("Temp", isDirectory: true).appendingPathComponent(UUID().uuidString) + let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier) + + let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: []) + let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing]) + self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in + do + { + if let error = error + { + throw error + } + + do + { + let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App") + try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil) + + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent) + + if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path) + { + try FileManager.default.removeItem(at: backupDocumentsDirectory) + } + + if FileManager.default.fileExists(atPath: documentsDirectory.path) + { + try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory) + } + + print("Copied Documents directory from \(documentsDirectory) to \(backupDocumentsDirectory)") + + let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] + let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent) + + if FileManager.default.fileExists(atPath: backupLibraryDirectory.path) + { + try FileManager.default.removeItem(at: backupLibraryDirectory) + } + + if FileManager.default.fileExists(atPath: libraryDirectory.path) + { + try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory) + } + + print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)") + } + + for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup + { + guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: "")) + } + + let backupAppGroupURL = temporaryAppBackupDirectory.appendingPathComponent(appGroup) + + // There are several system hidden files that we don't have permission to read, so we just skip all hidden files in app group directories. + try self.copyDirectoryContents(at: appGroupURL, to: backupAppGroupURL, options: [.skipsHiddenFiles]) + } + + // Replace previous backup with new backup. + _ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory) + + print("Replaced previous backup with new backup:", temporaryAppBackupDirectory) + + completionHandler(.success(())) + } + catch + { + do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) } + catch { print("Failed to remove temporary directory.", error) } + + completionHandler(.failure(error)) + } + } + } + catch + { + completionHandler(.failure(error)) + } + } + + func restoreBackup(completionHandler: @escaping (Result) -> Void) + { + do + { + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: "")) } + + guard + let altstoreAppGroup = self.altstoreAppGroup, + let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup) + else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) } + + let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups") + let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier) + + let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: []) + self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in + do + { + if let error = error + { + throw error + } + + let mainGroupBackupDirectory = appBackupDirectory.appendingPathComponent("App") + + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent) + + let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] + let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent) + + try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory) + try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory) + + for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup + { + guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: "")) + } + + let backupAppGroupURL = appBackupDirectory.appendingPathComponent(appGroup) + try self.copyDirectoryContents(at: backupAppGroupURL, to: appGroupURL) + } + + completionHandler(.success(())) + } + catch + { + completionHandler(.failure(error)) + } + } + } + catch + { + completionHandler(.failure(error)) + } + } +} + +private extension BackupController +{ + func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws + { + guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return } + + if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path) + { + try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil) + } + + for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options) + { + let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false + let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent) + + if FileManager.default.fileExists(atPath: destinationURL.path) + { + do { + try FileManager.default.removeItem(at: destinationURL) + } + catch CocoaError.fileWriteNoPermission where isDirectory { + try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options) + continue + } + catch { + print(error) + throw error + } + } + + do { + try FileManager.default.copyItem(at: fileURL, to: destinationURL) + print("Copied item from \(fileURL) to \(destinationURL)") + } + catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" { + // Ignore errors for /Documents/Inbox + print("Failed to copy Inbox directory:", error) + } + catch { + print(error) + throw error + } + } + } +} diff --git a/AltBackup/Base.lproj/LaunchScreen.storyboard b/AltBackup/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..61e8dd4c --- /dev/null +++ b/AltBackup/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AltBackup/Info.plist b/AltBackup/Info.plist new file mode 100644 index 00000000..25e69f4e --- /dev/null +++ b/AltBackup/Info.plist @@ -0,0 +1,64 @@ + + + + + ALTAppGroups + + group.com.rileytestut.AltStore + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + AltBackup General + CFBundleURLSchemes + + altbackup + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportsDocumentBrowser + + + diff --git a/AltBackup/UIColor+AltBackup.swift b/AltBackup/UIColor+AltBackup.swift new file mode 100644 index 00000000..73c7ceeb --- /dev/null +++ b/AltBackup/UIColor+AltBackup.swift @@ -0,0 +1,15 @@ +// +// UIColor+AltBackup.swift +// AltBackup +// +// Created by Riley Testut on 5/11/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import UIKit + +extension UIColor +{ + static let altstoreBackground = UIColor(named: "Background")! + static let altstoreText = UIColor(named: "Text")! +} diff --git a/AltBackup/ViewController.swift b/AltBackup/ViewController.swift new file mode 100644 index 00000000..8f1adeda --- /dev/null +++ b/AltBackup/ViewController.swift @@ -0,0 +1,131 @@ +// +// ViewController.swift +// AltBackup +// +// Created by Riley Testut on 5/11/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController +{ + private let backupController = BackupController() + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) + { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + + NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil) + } + + required init?(coder: NSCoder) { + fatalError() + } + + override func viewDidLoad() + { + super.viewDidLoad() + + self.view.backgroundColor = .altstoreBackground + + let textLabel = UILabel(frame: .zero) + textLabel.font = UIFont.preferredFont(forTextStyle: .title2) + textLabel.textColor = .altstoreText + textLabel.text = NSLocalizedString("Backing up app data…", comment: "") + + let activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge) + activityIndicatorView.color = .altstoreText + activityIndicatorView.startAnimating() + + #if DEBUG + let button1 = UIButton(type: .system) + button1.setTitle("Backup", for: .normal) + button1.setTitleColor(.white, for: .normal) + button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered) + + let button2 = UIButton(type: .system) + button2.setTitle("Restore", for: .normal) + button2.setTitleColor(.white, for: .normal) + button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered) + + let arrangedSubviews = [textLabel, activityIndicatorView, button1, button2] + #else + let arrangedSubviews = [textLabel, activityIndicatorView] + #endif + + let stackView = UIStackView(arrangedSubviews: arrangedSubviews) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.spacing = 22 + stackView.axis = .vertical + stackView.alignment = .center + self.view.addSubview(stackView) + + NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)]) + } +} + +private extension ViewController +{ + @objc func backup() + { + self.backupController.performBackup { (result) in + let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? + Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String ?? + NSLocalizedString("App", comment: "") + + let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName) + self.process(result, errorTitle: title) + } + } + + @objc func restore() + { + self.backupController.restoreBackup { (result) in + let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? + Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String ?? + NSLocalizedString("App", comment: "") + + let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName) + self.process(result, errorTitle: title) + } + } +} + +private extension ViewController +{ + func process(_ result: Result, errorTitle: String) + { + DispatchQueue.main.async { + switch result + { + case .success: break + case .failure(let error as NSError): + let message: String + + if let sourceDescription = error.sourceDescription + { + message = error.localizedDescription + "\n\n" + sourceDescription + } + else + { + message = error.localizedDescription + } + + let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + self.present(alertController, animated: true, completion: nil) + } + + NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result]) + } + } +} diff --git a/AltKit/Bundle+AltStore.swift b/AltKit/Bundle+AltStore.swift index 110b20f5..280b547d 100644 --- a/AltKit/Bundle+AltStore.swift +++ b/AltKit/Bundle+AltStore.swift @@ -38,4 +38,8 @@ public extension Bundle let infoPlistURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12") return infoPlistURL } + + var appGroups: [String] { + return self.infoDictionary?[Bundle.Info.appGroups] as? [String] ?? [] + } } diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 37d53abb..383c2bcb 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3129229F474900370A3C /* ConnectionManager.swift */; }; BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; }; BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAC8852295C90300587369 /* Result+Conveniences.swift */; }; - BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; }; BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; }; BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; }; BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; }; @@ -50,6 +49,7 @@ BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */; }; 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 */; }; BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF45868F229872EA00BD7491 /* AppDelegate.swift */; }; BF458694229872EA00BD7491 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF458693229872EA00BD7491 /* Assets.xcassets */; }; BF458697229872EA00BD7491 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF458695229872EA00BD7491 /* Main.storyboard */; }; @@ -118,10 +118,19 @@ BF45884B2298D55000BD7491 /* thread.h in Headers */ = {isa = PBXBuildFile; fileRef = BF4588492298D55000BD7491 /* thread.h */; }; BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; }; BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; }; + BF4E8456246F16D700ECCBD4 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; }; BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; }; BF56D2AA23DF88310006506D /* AppID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2A923DF88310006506D /* AppID.swift */; }; BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */; }; BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */; }; + BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF58047D246A28F7008AE704 /* AppDelegate.swift */; }; + BF580482246A28F7008AE704 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580481246A28F7008AE704 /* ViewController.swift */; }; + BF580487246A28F9008AE704 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF580486246A28F9008AE704 /* Assets.xcassets */; }; + BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; }; + BF580492246A2C5C008AE704 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; }; + BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; }; + BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF580497246A3D19008AE704 /* UIKit.framework */; }; + BF58049B246A432D008AE704 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; }; BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; }; BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */; }; BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; }; @@ -360,6 +369,7 @@ BF43002D22A714AF0051E2BC /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AltStore.swift"; sourceTree = ""; }; BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LaunchAtLogin.framework; path = Carthage/Build/Mac/LaunchAtLogin.framework; sourceTree = ""; }; + BF44EEEF246B08BA002A52F2 /* BackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupController.swift; sourceTree = ""; }; 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 = ""; }; BF458693229872EA00BD7491 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -441,6 +451,15 @@ BF56D2A923DF88310006506D /* AppID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppID.swift; sourceTree = ""; }; BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppIDsOperation.swift; sourceTree = ""; }; BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDsViewController.swift; sourceTree = ""; }; + BF58047B246A28F7008AE704 /* AltBackup.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltBackup.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BF58047D246A28F7008AE704 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BF580481246A28F7008AE704 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + BF580486246A28F9008AE704 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BF580489246A28F9008AE704 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + BF58048B246A28F9008AE704 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltBackup.swift"; sourceTree = ""; }; + BF580497246A3D19008AE704 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + BF580499246A4153008AE704 /* AltBackup.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltBackup.entitlements; sourceTree = ""; }; BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AltPlugin.mailbundle; sourceTree = BUILT_PRODUCTS_DIR; }; BF5C5FC7237DF5AE00EDD0C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -622,6 +641,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BF580478246A28F7008AE704 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; BF5C5FC2237DF5AE00EDD0C6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -902,6 +929,21 @@ path = "App IDs"; sourceTree = ""; }; + BF58047C246A28F7008AE704 /* AltBackup */ = { + isa = PBXGroup; + children = ( + BF580499246A4153008AE704 /* AltBackup.entitlements */, + BF58047D246A28F7008AE704 /* AppDelegate.swift */, + BF580481246A28F7008AE704 /* ViewController.swift */, + BF44EEEF246B08BA002A52F2 /* BackupController.swift */, + BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */, + BF580486246A28F9008AE704 /* Assets.xcassets */, + BF580488246A28F9008AE704 /* LaunchScreen.storyboard */, + BF58048B246A28F9008AE704 /* Info.plist */, + ); + path = AltBackup; + sourceTree = ""; + }; BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */ = { isa = PBXGroup; children = ( @@ -1019,6 +1061,7 @@ BF1E315122A0616100370A3C /* AltKit */, BF45872C2298D31600BD7491 /* libimobiledevice */, BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */, + BF58047C246A28F7008AE704 /* AltBackup */, BFD247852284BB3300981D42 /* Frameworks */, BFD2476B2284B9A500981D42 /* Products */, 4460E048E3AC1C9708C4FA33 /* Pods */, @@ -1033,6 +1076,7 @@ BF45872B2298D31600BD7491 /* libimobiledevice.a */, BF1E315022A0616100370A3C /* libAltKit.a */, BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */, + BF58047B246A28F7008AE704 /* AltBackup.app */, ); name = Products; sourceTree = ""; @@ -1073,6 +1117,7 @@ BFD247852284BB3300981D42 /* Frameworks */ = { isa = PBXGroup; children = ( + BF580497246A3D19008AE704 /* UIKit.framework */, BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */, BF9B63C5229DD44D002F0A62 /* AltSign.framework */, BF4588962298DE6E00BD7491 /* libzip.framework */, @@ -1370,6 +1415,23 @@ productReference = BF45872B2298D31600BD7491 /* libimobiledevice.a */; productType = "com.apple.product-type.library.static"; }; + BF58047A246A28F7008AE704 /* AltBackup */ = { + isa = PBXNativeTarget; + buildConfigurationList = BF58048E246A28F9008AE704 /* Build configuration list for PBXNativeTarget "AltBackup" */; + buildPhases = ( + BF580477246A28F7008AE704 /* Sources */, + BF580478246A28F7008AE704 /* Frameworks */, + BF580479246A28F7008AE704 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AltBackup; + productName = AltBackup; + productReference = BF58047B246A28F7008AE704 /* AltBackup.app */; + productType = "com.apple.product-type.application"; + }; BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */ = { isa = PBXNativeTarget; buildConfigurationList = BF5C5FC8237DF5AE00EDD0C6 /* Build configuration list for PBXNativeTarget "AltPlugin" */; @@ -1414,7 +1476,7 @@ BFD247622284B9A500981D42 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1120; + LastSwiftUpdateCheck = 1140; LastUpgradeCheck = 1020; ORGANIZATIONNAME = "Riley Testut"; TargetAttributes = { @@ -1436,6 +1498,9 @@ BF45872A2298D31600BD7491 = { CreatedOnToolsVersion = 10.2.1; }; + BF58047A246A28F7008AE704 = { + CreatedOnToolsVersion = 11.4.1; + }; BF5C5FC4237DF5AE00EDD0C6 = { CreatedOnToolsVersion = 11.2; LastSwiftMigration = 1120; @@ -1472,6 +1537,7 @@ BF1E314F22A0616100370A3C /* AltKit */, BF45872A2298D31600BD7491 /* libimobiledevice */, BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */, + BF58047A246A28F7008AE704 /* AltBackup */, ); }; /* End PBXProject section */ @@ -1486,6 +1552,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BF580479246A28F7008AE704 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */, + BF580487246A28F9008AE704 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; BF5C5FC3237DF5AE00EDD0C6 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1639,11 +1714,11 @@ files = ( BF718BD823C93DB700A89F2D /* AltKit.m in Sources */, BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */, - BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */, BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */, BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */, BFD44606241188C400EAB90A /* CodableServerError.swift in Sources */, BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */, + BF4E8456246F16D700ECCBD4 /* Bundle+AltStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1724,6 +1799,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BF580477246A28F7008AE704 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BF580492246A2C5C008AE704 /* Bundle+AltStore.swift in Sources */, + BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */, + BF580482246A28F7008AE704 /* ViewController.swift in Sources */, + BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */, + BF58049B246A432D008AE704 /* NSError+LocalizedFailure.swift in Sources */, + BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; BF5C5FC1237DF5AE00EDD0C6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1877,6 +1965,14 @@ name = Main.storyboard; sourceTree = ""; }; + BF580488246A28F9008AE704 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BF580489246A28F9008AE704 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; BFD247732284B9A500981D42 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2109,6 +2205,49 @@ }; name = Release; }; + BF58048C246A28F9008AE704 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CODE_SIGN_ENTITLEMENTS = AltBackup/AltBackup.entitlements; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6XVY5G3U44; + INFOPLIST_FILE = AltBackup/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltBackup; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BF58048D246A28F9008AE704 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CODE_SIGN_ENTITLEMENTS = AltBackup/AltBackup.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 6XVY5G3U44; + INFOPLIST_FILE = AltBackup/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltBackup; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; BF5C5FC9237DF5AE00EDD0C6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2369,6 +2508,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + BF58048E246A28F9008AE704 /* Build configuration list for PBXNativeTarget "AltBackup" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF58048C246A28F9008AE704 /* Debug */, + BF58048D246A28F9008AE704 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; BF5C5FC8237DF5AE00EDD0C6 /* Build configuration list for PBXNativeTarget "AltPlugin" */ = { isa = XCConfigurationList; buildConfigurations = ( From a4d9188bc772e6aec914b61d52e0b0d97ce79029 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 11:39:03 -0700 Subject: [PATCH 08/37] Fixes missing error descriptions when using NSError.withLocalizedFailure() --- AltStore/Extensions/NSError+LocalizedFailure.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AltStore/Extensions/NSError+LocalizedFailure.swift b/AltStore/Extensions/NSError+LocalizedFailure.swift index f7cf10a2..adf9d23b 100644 --- a/AltStore/Extensions/NSError+LocalizedFailure.swift +++ b/AltStore/Extensions/NSError+LocalizedFailure.swift @@ -20,6 +20,9 @@ extension NSError { var userInfo = self.userInfo userInfo[NSLocalizedFailureErrorKey] = failure + userInfo[NSLocalizedDescriptionKey] = self.localizedDescription + userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason + userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo) return error From 1b8b043290505e76bf21cd6d3346d4b59b8b1366 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 17 May 2020 23:47:24 -0700 Subject: [PATCH 09/37] Supports resigning apps with multiple app groups --- AltServer/Devices/ALTDeviceManager.mm | 2 +- .../FetchProvisioningProfilesOperation.swift | 85 +++++++++++-------- Dependencies/AltSign | 2 +- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/AltServer/Devices/ALTDeviceManager.mm b/AltServer/Devices/ALTDeviceManager.mm index 38f1fefd..89b3b2dd 100644 --- a/AltServer/Devices/ALTDeviceManager.mm +++ b/AltServer/Devices/ALTDeviceManager.mm @@ -1117,7 +1117,7 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT NSString *name = [NSString stringWithCString:device_name encoding:NSUTF8StringEncoding]; NSString *identifier = [NSString stringWithCString:udid encoding:NSUTF8StringEncoding]; - ALTDevice *altDevice = [[ALTDevice alloc] initWithName:name identifier:identifier]; + ALTDevice *altDevice = [[ALTDevice alloc] initWithName:name identifier:identifier type:ALTDeviceTypeiPhone]; [connectedDevices addObject:altDevice]; if (device_name != NULL) diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift index 2110e0a4..239faf2d 100644 --- a/AltStore/Operations/FetchProvisioningProfilesOperation.swift +++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift @@ -361,57 +361,70 @@ extension FetchProvisioningProfilesOperation { entitlements[key] = value } - - // TODO: Handle apps belonging to more than one app group. - guard let applicationGroups = entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else { - return completionHandler(.success(appID)) - } - - func finish(_ result: Result) - { - switch result - { - case .failure(let error): completionHandler(.failure(error)) - case .success(let group): - // Assign App Group - // TODO: Determine whether app already belongs to app group. - ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in - let result = result.map { _ in appID } - completionHandler(result) - } - } - } + guard let applicationGroups = entitlements[.appGroups] as? [String] else { return completionHandler(.success(appID)) } // Dispatch onto global queue to prevent appGroupsLock deadlock. DispatchQueue.global().async { - let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier // Ensure we're not concurrently fetching and updating app groups, // which can lead to race conditions such as adding an app group twice. self.appGroupsLock.lock() + func finish(_ result: Result) + { + self.appGroupsLock.unlock() + completionHandler(result) + } + ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in switch Result(groups, error) { - case .failure(let error): - self.appGroupsLock.unlock() - completionHandler(.failure(error)) + case .failure(let error): finish(.failure(error)) + case .success(let fetchedGroups): + let dispatchGroup = DispatchGroup() - case .success(let groups): - if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier }) + var groups = [ALTAppGroup]() + var errors = [Error]() + + for groupIdentifier in applicationGroups { - self.appGroupsLock.unlock() - finish(.success(group)) - } - else - { - // Not all characters are allowed in group names, so we replace periods with spaces (like Apple does). - let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ") + let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier - ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in - self.appGroupsLock.unlock() - finish(Result(group, error)) + if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier }) + { + groups.append(group) + } + else + { + dispatchGroup.enter() + + // Not all characters are allowed in group names, so we replace periods with spaces (like Apple does). + let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ") + + ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in + switch Result(group, error) + { + case .success(let group): groups.append(group) + case .failure(let error): errors.append(error) + } + + dispatchGroup.leave() + } + } + } + + dispatchGroup.notify(queue: .global()) { + if let error = errors.first + { + finish(.failure(error)) + } + else + { + ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in + let result = Result(success, error) + finish(result.map { _ in appID }) + } } } } diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 59050382..db6cadf0 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 5905038272c4b2ccee4390a0b830bfa84001f273 +Subproject commit db6cadf0210e150594f1b50a89fe15c64d9ffeee From b25a0e46cb5aba4b907adc1c1f8189b71a01a3fc Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 14:27:53 -0700 Subject: [PATCH 10/37] [AltBackup] No longer assumes AltStore app group is first in ALTAppGroups --- AltBackup/BackupController.swift | 8 ++------ AltKit/Bundle+AltStore.swift | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/AltBackup/BackupController.swift b/AltBackup/BackupController.swift index e835e335..60d99dea 100644 --- a/AltBackup/BackupController.swift +++ b/AltBackup/BackupController.swift @@ -77,10 +77,6 @@ struct BackupError: ALTLocalizedError class BackupController: NSObject { - private var altstoreAppGroup: String? { - return Bundle.main.appGroups.first - } - private let fileCoordinator = NSFileCoordinator(filePresenter: nil) private let operationQueue = OperationQueue() @@ -96,7 +92,7 @@ class BackupController: NSObject guard let bundleIdentifier = Bundle.main.bundleIdentifier else { throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: "")) } guard - let altstoreAppGroup = self.altstoreAppGroup, + let altstoreAppGroup = Bundle.main.altstoreAppGroup, let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup) else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) } @@ -193,7 +189,7 @@ class BackupController: NSObject guard let bundleIdentifier = Bundle.main.bundleIdentifier else { throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: "")) } guard - let altstoreAppGroup = self.altstoreAppGroup, + let altstoreAppGroup = Bundle.main.altstoreAppGroup, let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup) else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) } diff --git a/AltKit/Bundle+AltStore.swift b/AltKit/Bundle+AltStore.swift index 280b547d..e5bb4e39 100644 --- a/AltKit/Bundle+AltStore.swift +++ b/AltKit/Bundle+AltStore.swift @@ -24,6 +24,8 @@ public extension Bundle public extension Bundle { + static var baseAltStoreAppGroupID = "group.com.rileytestut.AltStore" + var infoPlistURL: URL { let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist") return infoPlistURL @@ -42,4 +44,9 @@ public extension Bundle var appGroups: [String] { return self.infoDictionary?[Bundle.Info.appGroups] as? [String] ?? [] } + + var altstoreAppGroup: String? { + let appGroup = self.appGroups.first { $0.contains(Bundle.baseAltStoreAppGroupID) } + return appGroup + } } From 8354794c2446e3faebe945404332c3da4fb8e268 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 14:44:06 -0700 Subject: [PATCH 11/37] Embeds original bundle ID under ALTBundleIdentifier Info.plist key --- AltKit/Bundle+AltStore.swift | 1 + AltStore/Operations/ResignAppOperation.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/AltKit/Bundle+AltStore.swift b/AltKit/Bundle+AltStore.swift index e5bb4e39..37fe4374 100644 --- a/AltKit/Bundle+AltStore.swift +++ b/AltKit/Bundle+AltStore.swift @@ -16,6 +16,7 @@ public extension Bundle public static let serverID = "ALTServerID" public static let certificateID = "ALTCertificateID" public static let appGroups = "ALTAppGroups" + public static let altBundleID = "ALTBundleIdentifier" public static let urlTypes = "CFBundleURLTypes" public static let exportedUTIs = "UTExportedTypeDeclarations" diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index 3288d79e..314a59a1 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -112,6 +112,7 @@ private extension ResignAppOperation guard var infoDictionary = bundle.infoDictionary else { throw ALTError(.missingInfoPlist) } infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier + infoDictionary[Bundle.Info.altBundleID] = identifier for (key, value) in additionalInfoDictionaryValues { From 7cbe921020bb4c0a71a1a3be9079fe5cc4e7379b Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 14:44:29 -0700 Subject: [PATCH 12/37] [AltBackup] Derives backup location from original bundle ID, not resigned one Allows the backup to be used even if the app is later installed with a different developer team. --- AltBackup/BackupController.swift | 8 ++++++-- AltBackup/Info.plist | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/AltBackup/BackupController.swift b/AltBackup/BackupController.swift index 60d99dea..4b2379ac 100644 --- a/AltBackup/BackupController.swift +++ b/AltBackup/BackupController.swift @@ -89,7 +89,9 @@ class BackupController: NSObject { do { - guard let bundleIdentifier = Bundle.main.bundleIdentifier else { throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: "")) } + guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else { + throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: "")) + } guard let altstoreAppGroup = Bundle.main.altstoreAppGroup, @@ -186,7 +188,9 @@ class BackupController: NSObject { do { - guard let bundleIdentifier = Bundle.main.bundleIdentifier else { throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: "")) } + guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else { + throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: "")) + } guard let altstoreAppGroup = Bundle.main.altstoreAppGroup, diff --git a/AltBackup/Info.plist b/AltBackup/Info.plist index 25e69f4e..63d01c06 100644 --- a/AltBackup/Info.plist +++ b/AltBackup/Info.plist @@ -6,6 +6,8 @@ group.com.rileytestut.AltStore + ALTBundleIdentifier + com.rileytestut.AltBackup CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable From 7c9d8bd90d45c91ad70c54f464169abb91a41913 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 14:55:15 -0700 Subject: [PATCH 13/37] Adds option to not cache downloaded app during installation --- AltStore/Managing Apps/AppManager.swift | 21 ++++++++++++++----- .../Operations/DownloadAppOperation.swift | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 547948cc..239de97c 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -486,7 +486,7 @@ private extension AppManager return group } - private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, additionalEntitlements: [ALTEntitlement: Any]? = nil, completionHandler: @escaping (Result) -> Void) -> Progress + private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result) -> Void) -> Progress { let progress = Progress.discreteProgress(totalUnitCount: 100) @@ -514,13 +514,24 @@ private extension AppManager downloadingApp = storeApp } + let downloadedAppURL = context.temporaryDirectory.appendingPathComponent("Cached.app") + /* Download */ - let downloadOperation = DownloadAppOperation(app: downloadingApp, context: context) + let downloadOperation = DownloadAppOperation(app: downloadingApp, destinationURL: downloadedAppURL, context: context) downloadOperation.resultHandler = { (result) in - switch result + do { - case .failure(let error): context.error = error - case .success(let app): context.app = app + let app = try result.get() + context.app = app + + if cacheApp + { + try FileManager.default.copyItem(at: app.fileURL, to: InstalledApp.fileURL(for: app), shouldReplace: true) + } + } + catch + { + context.error = error } } progress.addChild(downloadOperation.progress, withPendingUnitCount: 25) diff --git a/AltStore/Operations/DownloadAppOperation.swift b/AltStore/Operations/DownloadAppOperation.swift index ce13bfc3..1af194d9 100644 --- a/AltStore/Operations/DownloadAppOperation.swift +++ b/AltStore/Operations/DownloadAppOperation.swift @@ -23,14 +23,14 @@ class DownloadAppOperation: ResultOperation private let session = URLSession(configuration: .default) - init(app: AppProtocol, context: AppOperationContext) + init(app: AppProtocol, destinationURL: URL, context: AppOperationContext) { self.app = app self.context = context self.bundleIdentifier = app.bundleIdentifier self.sourceURL = app.url - self.destinationURL = InstalledApp.fileURL(for: app) + self.destinationURL = destinationURL super.init() From c403d7c788a55e39e349db12b9eb21a8d804b1fd Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 15 May 2020 15:11:17 -0700 Subject: [PATCH 14/37] Adds BackupAppOperation to backup and restore app data --- AltStore.xcodeproj/project.pbxproj | 4 + AltStore/AppDelegate.swift | 43 ++++- AltStore/Operations/BackupAppOperation.swift | 183 +++++++++++++++++++ AltStore/Operations/OperationContexts.swift | 6 + AltStore/Operations/OperationError.swift | 7 + 5 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 AltStore/Operations/BackupAppOperation.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 383c2bcb..b0d3c9c5 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; }; BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; }; BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; }; + BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */; }; BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */; }; BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */; }; BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; }; @@ -354,6 +355,7 @@ BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = ""; }; BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = ""; }; BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = ""; }; + BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAppOperation.swift; sourceTree = ""; }; BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchProvisioningProfilesOperation.swift; sourceTree = ""; }; BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAppOperation.swift; sourceTree = ""; }; BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = ""; }; @@ -1290,6 +1292,7 @@ BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */, BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */, BFCCB519245E3401001853EA /* VerifyAppOperation.swift */, + BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */, ); path = Operations; sourceTree = ""; @@ -1912,6 +1915,7 @@ BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */, BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */, + BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */, BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */, diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 65106524..f31273b7 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -55,7 +55,10 @@ extension AppDelegate static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification") static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification") + static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish") + static let importAppDeepLinkURLKey = "fileURL" + static let appBackupResultKey = "result" } @UIApplicationMain @@ -134,13 +137,43 @@ private extension AppDelegate else { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false } - guard let host = components.host, host.lowercased() == "patreon" else { return false } + guard let host = components.host?.lowercased() else { return false } - DispatchQueue.main.async { - NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) + switch host + { + case "patreon": + DispatchQueue.main.async { + NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil) + } + + return true + + case "appbackupresponse": + let result: Result + + switch url.path.lowercased() + { + case "/success": result = .success(()) + case "/failure": + let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:] + guard + let errorDomain = queryItems["errorDomain"], + let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString), + let errorDescription = queryItems["errorDescription"] + else { return false } + + let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription]) + result = .failure(error) + + default: return false + } + + NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result]) + + return true + + default: return false } - - return true } } } diff --git a/AltStore/Operations/BackupAppOperation.swift b/AltStore/Operations/BackupAppOperation.swift new file mode 100644 index 00000000..000e44db --- /dev/null +++ b/AltStore/Operations/BackupAppOperation.swift @@ -0,0 +1,183 @@ +// +// BackupAppOperation.swift +// AltStore +// +// Created by Riley Testut on 5/12/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltKit +import AltSign + +extension BackupAppOperation +{ + enum Action: String + { + case backup + case restore + } +} + +@objc(BackupAppOperation) +class BackupAppOperation: ResultOperation +{ + let action: Action + let context: InstallAppOperationContext + + private var appName: String? + private var timeoutTimer: Timer? + + init(action: Action, context: InstallAppOperationContext) + { + self.action = action + self.context = context + + super.init() + } + + override func main() + { + super.main() + + do + { + if let error = self.context.error + { + throw error + } + + guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters } + context.perform { + do + { + let appName = installedApp.name + self.appName = appName + + guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound } + let altstoreOpenURL = altstoreApp.openAppURL + + var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false) + returnURLComponents?.host = "appBackupResponse" + guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) } + + var openURLComponents = URLComponents() + openURLComponents.scheme = installedApp.openAppURL.scheme + openURLComponents.host = self.action.rawValue + openURLComponents.queryItems = [URLQueryItem(name: "returnURL", value: returnURL.absoluteString)] + + guard let openURL = openURLComponents.url else { throw OperationError.openAppFailed(name: appName) } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let currentTime = CFAbsoluteTimeGetCurrent() + + UIApplication.shared.open(openURL, options: [:]) { (success) in + let elapsedTime = CFAbsoluteTimeGetCurrent() - currentTime + + if success + { + self.registerObservers() + } + else if elapsedTime < 0.5 + { + // Failed too quickly for human to respond to alert, possibly still finalizing installation. + // Try again in a couple seconds. + + print("Failed too quickly, retrying after a few seconds...") + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + UIApplication.shared.open(openURL, options: [:]) { (success) in + if success + { + self.registerObservers() + } + else + { + self.finish(.failure(OperationError.openAppFailed(name: appName))) + } + } + } + } + else + { + self.finish(.failure(OperationError.openAppFailed(name: appName))) + } + } + } + } + catch + { + self.finish(.failure(error)) + } + } + } + catch + { + self.finish(.failure(error)) + } + } + + override func finish(_ result: Result) + { + let result = result.mapError { (error) -> Error in + let appName = self.appName ?? self.context.bundleIdentifier + + switch (error, self.action) + { + case (let error as NSError, _) where (self.context.error as NSError?) == error: fallthrough + case (OperationError.cancelled, _): + return error + + case (let error as NSError, .backup): + let localizedFailure = String(format: NSLocalizedString("Could not back up “%@”.", comment: ""), appName) + return error.withLocalizedFailure(localizedFailure) + + case (let error as NSError, .restore): + let localizedFailure = String(format: NSLocalizedString("Could not restore “%@”.", comment: ""), appName) + return error.withLocalizedFailure(localizedFailure) + } + } + + switch result + { + case .success: self.progress.completedUnitCount += 1 + case .failure: break + } + + super.finish(result) + } +} + +private extension BackupAppOperation +{ + func registerObservers() + { + var applicationWillReturnObserver: NSObjectProtocol! + applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in + guard let self = self, !self.isFinished else { return } + + self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) 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)) + } + } + + NotificationCenter.default.removeObserver(applicationWillReturnObserver!) + } + + var backupResponseObserver: NSObjectProtocol! + backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in + self?.timeoutTimer?.invalidate() + + let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result ?? .failure(OperationError.unknownResult) + self?.finish(result) + + NotificationCenter.default.removeObserver(backupResponseObserver!) + } + } +} diff --git a/AltStore/Operations/OperationContexts.swift b/AltStore/Operations/OperationContexts.swift index 1b666785..97bc0dfb 100644 --- a/AltStore/Operations/OperationContexts.swift +++ b/AltStore/Operations/OperationContexts.swift @@ -105,6 +105,12 @@ class InstallAppOperationContext: AppOperationContext var resignedApp: ALTApplication? var installationConnection: ServerConnection? + var installedApp: InstalledApp? { + didSet { + self.installedAppContext = self.installedApp?.managedObjectContext + } + } + private var installedAppContext: NSManagedObjectContext? var beginInstallationHandler: ((InstalledApp) -> Void)? } diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift index f509a46b..82ed2a51 100644 --- a/AltStore/Operations/OperationError.swift +++ b/AltStore/Operations/OperationError.swift @@ -14,6 +14,7 @@ enum OperationError: LocalizedError case unknown case unknownResult case cancelled + case timedOut case notAuthenticated case appNotFound @@ -28,17 +29,23 @@ enum OperationError: LocalizedError case noSources + case openAppFailed(name: String) + case missingAppGroup + var failureReason: String? { switch self { case .unknown: return NSLocalizedString("An unknown error occured.", comment: "") case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "") case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") + case .timedOut: return NSLocalizedString("The operation timed out.", comment: "") case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "") case .appNotFound: return NSLocalizedString("App not found.", comment: "") case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "") case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "") case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "") case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "") + case .openAppFailed(let name): return String(format: NSLocalizedString("AltStore was denied permission to launch %@.", comment: ""), name) + case .missingAppGroup: return NSLocalizedString("AltStore's shared app group could not be found.", comment: "") case .iOSVersionNotSupported(let app): let name = app.name From 1582d1b143f9400f9ab3ef50c6b962dd239e57dd Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sat, 16 May 2020 11:42:31 -0700 Subject: [PATCH 15/37] Fixes updating App IDs with no app groups --- .../FetchProvisioningProfilesOperation.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift index 239faf2d..a7bdb49d 100644 --- a/AltStore/Operations/FetchProvisioningProfilesOperation.swift +++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift @@ -362,7 +362,17 @@ extension FetchProvisioningProfilesOperation entitlements[key] = value } - guard let applicationGroups = entitlements[.appGroups] as? [String] else { return completionHandler(.success(appID)) } + let applicationGroups = entitlements[.appGroups] as? [String] ?? [] + if applicationGroups.isEmpty + { + guard let isAppGroupsEnabled = appID.features[.appGroups] as? Bool, isAppGroupsEnabled else { + // No app groups, and we also haven't enabled the feature, so don't continue. + // For apps with no app groups but have had the feature enabled already + // we'll continue and assign the app ID to an empty array + // in case we need to explicitly remove them. + return completionHandler(.success(appID)) + } + } // Dispatch onto global queue to prevent appGroupsLock deadlock. DispatchQueue.global().async { From 753fb740fed4c8d4c2e7adb6d4facec8a882d24c Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sat, 16 May 2020 15:13:12 -0700 Subject: [PATCH 16/37] Adds RemoveAppOperation for removing inactive apps --- AltStore.xcodeproj/project.pbxproj | 4 + AltStore/Operations/RemoveAppOperation.swift | 83 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 AltStore/Operations/RemoveAppOperation.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index b0d3c9c5..7223e682 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -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 */; }; + 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 */; }; BF458697229872EA00BD7491 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF458695229872EA00BD7491 /* Main.storyboard */; }; @@ -372,6 +373,7 @@ BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AltStore.swift"; sourceTree = ""; }; BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LaunchAtLogin.framework; path = Carthage/Build/Mac/LaunchAtLogin.framework; sourceTree = ""; }; BF44EEEF246B08BA002A52F2 /* BackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupController.swift; sourceTree = ""; }; + BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppOperation.swift; sourceTree = ""; }; 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 = ""; }; BF458693229872EA00BD7491 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -1292,6 +1294,7 @@ BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */, BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */, BFCCB519245E3401001853EA /* VerifyAppOperation.swift */, + BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */, BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */, ); path = Operations; @@ -1871,6 +1874,7 @@ BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */, BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */, + BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */, BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */, BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */, BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */, diff --git a/AltStore/Operations/RemoveAppOperation.swift b/AltStore/Operations/RemoveAppOperation.swift new file mode 100644 index 00000000..9bb3a050 --- /dev/null +++ b/AltStore/Operations/RemoveAppOperation.swift @@ -0,0 +1,83 @@ +// +// RemoveAppOperation.swift +// AltStore +// +// Created by Riley Testut on 5/12/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltKit + +@objc(RemoveAppOperation) +class RemoveAppOperation: ResultOperation +{ + let context: InstallAppOperationContext + + init(context: InstallAppOperationContext) + { + self.context = context + + super.init() + } + + override func main() + { + super.main() + + if let error = self.context.error + { + self.finish(.failure(error)) + return + } + + guard let server = self.context.server, let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) } + guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) } + + installedApp.managedObjectContext?.perform { + let resignedBundleIdentifier = installedApp.resignedBundleIdentifier + + ServerManager.shared.connect(to: server) { (result) in + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success(let connection): + print("Sending remove app request...") + + let request = RemoveAppRequest(udid: udid, bundleIdentifier: resignedBundleIdentifier) + connection.send(request) { (result) in + print("Sent remove app request!") + + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success: + print("Waiting for remove app response...") + connection.receiveResponse() { (result) in + print("Receiving remove app response:", result) + + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success(.error(let response)): self.finish(.failure(response.error)) + case .success(.removeApp): + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + self.progress.completedUnitCount += 1 + + let installedApp = context.object(with: installedApp.objectID) as! InstalledApp + installedApp.isActive = false + self.finish(.success(installedApp)) + } + + case .success: self.finish(.failure(ALTServerError(.unknownResponse))) + } + } + } + } + } + } + } + } +} + From d8f1dcb032a74c3f528fc077e9d03bd79ae04f3c Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sat, 16 May 2020 15:28:22 -0700 Subject: [PATCH 17/37] Adds RemoveAppBackupOperation to remove backed up app data --- AltStore.xcodeproj/project.pbxproj | 8 ++ AltStore/AltStore.entitlements | 4 + .../FileManager+SharedDirectories.swift | 32 ++++++++ AltStore/Info.plist | 4 + .../Operations/RemoveAppBackupOperation.swift | 79 +++++++++++++++++++ 5 files changed, 127 insertions(+) create mode 100644 AltStore/Extensions/FileManager+SharedDirectories.swift create mode 100644 AltStore/Operations/RemoveAppBackupOperation.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 7223e682..d33aeef1 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -135,6 +135,7 @@ BF58049B246A432D008AE704 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; }; BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; }; BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */; }; + BF6A5320246DC1B0004F59C8 /* FileManager+SharedDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */; }; BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; }; BF6C33652419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */; }; BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; }; @@ -239,6 +240,7 @@ BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; }; BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */; }; BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */; }; + BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */; }; BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DC22F0E7F3002E24B9 /* Source.swift */; }; BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */; }; BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338E722F10E56002E24B9 /* LaunchViewController.swift */; }; @@ -470,6 +472,7 @@ BF5C5FCD237DF69100EDD0C6 /* ALTPluginService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPluginService.h; sourceTree = ""; }; BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPluginService.m; sourceTree = ""; }; BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DirectorySize.swift"; sourceTree = ""; }; + BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SharedDirectories.swift"; sourceTree = ""; }; BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+LocalizedFailure.swift"; sourceTree = ""; }; BF6C33632419ADEB0034FD24 /* AltStore 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 5.xcdatamodel"; sourceTree = ""; }; BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore4ToAltStore5.xcmappingmodel; sourceTree = ""; }; @@ -591,6 +594,7 @@ BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = ""; }; BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAppOperation.swift; sourceTree = ""; }; + BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppBackupOperation.swift; sourceTree = ""; }; BFE338DC22F0E7F3002E24B9 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = ""; }; BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSourceOperation.swift; sourceTree = ""; }; BFE338E722F10E56002E24B9 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; @@ -1217,6 +1221,7 @@ BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */, BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */, BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */, + BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */, ); path = Extensions; sourceTree = ""; @@ -1296,6 +1301,7 @@ BFCCB519245E3401001853EA /* VerifyAppOperation.swift */, BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */, BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */, + BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */, ); path = Operations; sourceTree = ""; @@ -1918,6 +1924,7 @@ BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */, BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */, BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */, + BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */, BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, @@ -1935,6 +1942,7 @@ BF770E5622BC3C03002A40FE /* Server.swift in Sources */, BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */, BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */, + BF6A5320246DC1B0004F59C8 /* FileManager+SharedDirectories.swift in Sources */, BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/AltStore/AltStore.entitlements b/AltStore/AltStore.entitlements index 903def2a..11842e8b 100644 --- a/AltStore/AltStore.entitlements +++ b/AltStore/AltStore.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.security.application-groups + + group.com.rileytestut.AltStore + diff --git a/AltStore/Extensions/FileManager+SharedDirectories.swift b/AltStore/Extensions/FileManager+SharedDirectories.swift new file mode 100644 index 00000000..d4de2eea --- /dev/null +++ b/AltStore/Extensions/FileManager+SharedDirectories.swift @@ -0,0 +1,32 @@ +// +// FileManager+SharedDirectories.swift +// AltStore +// +// Created by Riley Testut on 5/14/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltKit + +extension FileManager +{ + var altstoreSharedDirectory: URL? { + guard let appGroup = Bundle.main.appGroups.first else { return nil } + + let sharedDirectoryURL = self.containerURL(forSecurityApplicationGroupIdentifier: appGroup) + return sharedDirectoryURL + } + + var appBackupsDirectory: URL? { + let appBackupsDirectory = self.altstoreSharedDirectory?.appendingPathComponent("Backups", isDirectory: true) + return appBackupsDirectory + } + + func backupDirectoryURL(for app: InstalledApp) -> URL? + { + let backupDirectoryURL = self.appBackupsDirectory?.appendingPathComponent(app.bundleIdentifier, isDirectory: true) + return backupDirectoryURL + } +} diff --git a/AltStore/Info.plist b/AltStore/Info.plist index 4df53e85..1f9ca88e 100644 --- a/AltStore/Info.plist +++ b/AltStore/Info.plist @@ -2,6 +2,10 @@ + ALTAppGroups + + group.com.rileytestut.AltStore + ALTDeviceID 00008030-001948590202802E ALTServerID diff --git a/AltStore/Operations/RemoveAppBackupOperation.swift b/AltStore/Operations/RemoveAppBackupOperation.swift new file mode 100644 index 00000000..142ebd84 --- /dev/null +++ b/AltStore/Operations/RemoveAppBackupOperation.swift @@ -0,0 +1,79 @@ +// +// RemoveAppBackupOperation.swift +// AltStore +// +// Created by Riley Testut on 5/13/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltKit + +@objc(RemoveAppBackupOperation) +class RemoveAppBackupOperation: ResultOperation +{ + let context: InstallAppOperationContext + + private let coordinator = NSFileCoordinator() + private let coordinatorQueue = OperationQueue() + + init(context: InstallAppOperationContext) + { + self.context = context + + super.init() + + self.coordinatorQueue.name = "AltStore - RemoveAppBackupOperation Queue" + } + + override func main() + { + super.main() + + if let error = self.context.error + { + self.finish(.failure(error)) + return + } + + guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) } + installedApp.managedObjectContext?.perform { + guard let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return self.finish(.failure(OperationError.missingAppGroup)) } + + let intent = NSFileAccessIntent.writingIntent(with: backupDirectoryURL, options: [.forDeleting]) + self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in + do + { + if let error = error + { + throw error + } + + try FileManager.default.removeItem(at: intent.url) + + self.finish(.success(())) + } + catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile + { + #if DEBUG + + // When debugging, it's expected that app groups don't match, so ignore. + self.finish(.success(())) + + #else + + print("Failed to remove app backup directory:", error) + self.finish(.failure(error)) + + #endif + } + catch + { + print("Failed to remove app backup directory:", error) + self.finish(.failure(error)) + } + } + } + } +} From 19bf19350e1c0f4908cadda06a3e4d67543cb682 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sat, 16 May 2020 15:34:10 -0700 Subject: [PATCH 18/37] Supports removing inactive apps from My Apps --- .../Extensions/UserDefaults+AltStore.swift | 7 +- AltStore/Managing Apps/AppManager.swift | 42 +++++++++ AltStore/My Apps/MyAppsViewController.swift | 88 ++++++++++++------- 3 files changed, 102 insertions(+), 35 deletions(-) diff --git a/AltStore/Extensions/UserDefaults+AltStore.swift b/AltStore/Extensions/UserDefaults+AltStore.swift index 1756348b..2e04e5d5 100644 --- a/AltStore/Extensions/UserDefaults+AltStore.swift +++ b/AltStore/Extensions/UserDefaults+AltStore.swift @@ -22,6 +22,8 @@ extension UserDefaults @NSManaged var legacySideloadedApps: [String]? + @NSManaged var isLegacyDeactivationSupported: Bool + var activeAppsLimit: Int? { get { return self._activeAppsLimit?.intValue @@ -41,6 +43,9 @@ extension UserDefaults func registerDefaults() { - self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true]) + self.register(defaults: [ + #keyPath(UserDefaults.isBackgroundRefreshEnabled): true, + #keyPath(UserDefaults.isLegacyDeactivationSupported): false + ]) } } diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 239de97c..c8688380 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -344,6 +344,48 @@ extension AppManager self.run([deactivateAppOperation], context: context, requiresSerialQueue: true) } + func remove(_ installedApp: InstalledApp, completionHandler: @escaping (Result) -> Void) + { + let authenticationContext = AuthenticatedOperationContext() + let appContext = InstallAppOperationContext(bundleIdentifier: installedApp.bundleIdentifier, authenticatedContext: authenticationContext) + appContext.installedApp = installedApp + + let removeAppOperation = RSTAsyncBlockOperation { (operation) in + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + let installedApp = context.object(with: installedApp.objectID) as! InstalledApp + context.delete(installedApp) + + do { try context.save() } + catch { appContext.error = error } + + operation.finish() + } + } + + let removeAppBackupOperation = RemoveAppBackupOperation(context: appContext) + removeAppBackupOperation.resultHandler = { (result) in + switch result + { + case .success: break + case .failure(let error): print("Failed to remove app backup.", error) + } + + // Throw the error from removeAppOperation, + // since that's the error we really care about. + if let error = appContext.error + { + completionHandler(.failure(error)) + } + else + { + completionHandler(.success(())) + } + } + removeAppBackupOperation.addDependency(removeAppOperation) + + self.run([removeAppOperation, removeAppBackupOperation], context: authenticationContext) + } + func installationProgress(for app: AppProtocol) -> Progress? { let progress = self.installationProgress[app.bundleIdentifier] diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 38bff614..d5d6f987 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -960,15 +960,31 @@ private extension MyAppsViewController func remove(_ installedApp: InstalledApp) { - let alertController = UIAlertController(title: nil, message: NSLocalizedString("Removing a sideloaded app only removes it from AltStore. You must also delete it from the home screen to fully uninstall the app.", comment: ""), preferredStyle: .actionSheet) + let title = String(format: NSLocalizedString("Remove “%@” from AltStore?", comment: ""), installedApp.name) + let message: String + + if UserDefaults.standard.isLegacyDeactivationSupported + { + message = NSLocalizedString("You must also delete it from the home screen to fully uninstall the app.", comment: "") + } + else + { + message = NSLocalizedString("This will also erase all backup data for this app.", comment: "") + } + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) alertController.addAction(.cancel) alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in - DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in - let installedApp = context.object(with: installedApp.objectID) as! InstalledApp - context.delete(installedApp) - - do { try context.save() } - catch { print("Failed to remove sideloaded app.", error) } + AppManager.shared.remove(installedApp) { (result) in + switch result + { + case .success: break + case .failure(let error): + DispatchQueue.main.async { + let toastView = ToastView(error: error) + toastView.show(in: self) + } + } } })) @@ -1161,39 +1177,43 @@ extension MyAppsViewController self.remove(installedApp) } - if installedApp.bundleIdentifier == StoreApp.altstoreAppID + guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else { + return [refreshAction] + } + + if installedApp.isActive { - actions = [refreshAction] + actions.append(refreshAction) + actions.append(deactivateAction) } else { - if installedApp.isActive - { - if UserDefaults.standard.activeAppsLimit != nil - { - actions = [refreshAction, deactivateAction] - } - else - { - actions = [refreshAction] - } - } - else - { - actions.append(activateAction) - } - - #if DEBUG - actions.append(removeAction) - #else - if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier) - { - // Only display option for legacy sideloaded apps. - actions.append(removeAction) - } - #endif + actions.append(activateAction) } + #if DEBUG + + if installedApp.bundleIdentifier != StoreApp.altstoreAppID + { + actions.append(removeAction) + } + + #else + + if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier) + { + // Legacy sideloaded app, so can't detect if it's deleted. + actions.append(removeAction) + } + else if !UserDefaults.standard.isLegacyDeactivationSupported && !installedApp.isActive + { + // Inactive apps are actually deleted, so we need another way + // for user to remove them from AltStore. + actions.append(removeAction) + } + + #endif + return actions } From 2d87c396f1d52328684bdf9b76d9a812f3c962e2 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sat, 16 May 2020 16:17:18 -0700 Subject: [PATCH 19/37] 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. --- AltStore.xcodeproj/project.pbxproj | 4 + AltStore/Info.plist | 10 + AltStore/Managing Apps/AppManager.swift | 279 +++++++++++++++++--- AltStore/Model/InstalledApp.swift | 10 + AltStore/My Apps/MyAppsViewController.swift | 11 +- AltStore/Operations/OperationContexts.swift | 2 +- AltStore/Operations/RefreshGroup.swift | 14 + AltStore/Operations/SendAppOperation.swift | 9 +- AltStore/Resources/AltBackup.ipa | Bin 0 -> 64161 bytes 9 files changed, 293 insertions(+), 46 deletions(-) create mode 100644 AltStore/Resources/AltBackup.ipa diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index d33aeef1..71cfe90d 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -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 = ""; }; BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LaunchAtLogin.framework; path = Carthage/Build/Mac/LaunchAtLogin.framework; sourceTree = ""; }; BF44EEEF246B08BA002A52F2 /* BackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupController.swift; sourceTree = ""; }; + BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = ""; }; BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppOperation.swift; sourceTree = ""; }; 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 = ""; }; @@ -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 */, diff --git a/AltStore/Info.plist b/AltStore/Info.plist index 1f9ca88e..90def6a1 100644 --- a/AltStore/Info.plist +++ b/AltStore/Info.plist @@ -53,6 +53,16 @@ altstore + + CFBundleTypeRole + Editor + CFBundleURLName + AltStore Backup + CFBundleURLSchemes + + altstore-com.rileytestut.AltStore + + CFBundleVersion 1 diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index c8688380..e5c89652 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -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) -> Void) + func deactivate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result) -> 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) @@ -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) -> Void) -> Progress + private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result) -> 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) -> 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) -> 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 } } } diff --git a/AltStore/Model/InstalledApp.swift b/AltStore/Model/InstalledApp.swift index dc4b2193..36170e0c 100644 --- a/AltStore/Model/InstalledApp.swift +++ b/AltStore/Model/InstalledApp.swift @@ -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) + } } diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index d5d6f987..232bb391 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -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() diff --git a/AltStore/Operations/OperationContexts.swift b/AltStore/Operations/OperationContexts.swift index 97bc0dfb..1cf990c0 100644 --- a/AltStore/Operations/OperationContexts.swift +++ b/AltStore/Operations/OperationContexts.swift @@ -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]? diff --git a/AltStore/Operations/RefreshGroup.swift b/AltStore/Operations/RefreshGroup.swift index 9d487b6b..721fc26e 100644 --- a/AltStore/Operations/RefreshGroup.swift +++ b/AltStore/Operations/RefreshGroup.swift @@ -21,6 +21,10 @@ class RefreshGroup: NSObject private(set) var results = [String: Result]() + // Keep strong references to managed object contexts + // so they don't die out from under us. + private(set) var _contexts = Set() + 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, 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() diff --git a/AltStore/Operations/SendAppOperation.swift b/AltStore/Operations/SendAppOperation.swift index 75b35ec8..9d8835dc 100644 --- a/AltStore/Operations/SendAppOperation.swift +++ b/AltStore/Operations/SendAppOperation.swift @@ -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(())) } } } diff --git a/AltStore/Resources/AltBackup.ipa b/AltStore/Resources/AltBackup.ipa new file mode 100644 index 0000000000000000000000000000000000000000..b08d0da0bb4b0bc2df87b5bfc09913026774ff37 GIT binary patch literal 64161 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%0Af9*s9!&xKzYW9~v}C3;=znOLlx+gznq)6yK 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 YNrJwUZ2$le-`|AqHG|pvp8){=ACsu}YXATM literal 0 HcmV?d00001 From de34e077ce024cf71da2b994ce3184556dddacdc Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sat, 16 May 2020 16:29:28 -0700 Subject: [PATCH 20/37] Activates apps by reinstalling then restoring backup on iOS 13.5+ To activate an inactive app that has been deleted from the phone, AltStore will reinstall the app, as well as restore any app data from when it was deactivated. --- AltStore/Managing Apps/AppManager.swift | 155 +++++++++++++++++++++++- 1 file changed, 149 insertions(+), 6 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index e5c89652..24d06980 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -319,13 +319,19 @@ extension AppManager func activate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result) -> Void) { - let group = self.refresh([installedApp], presentingViewController: presentingViewController) + let group = RefreshGroup() + + let operation = AppOperation.activate(installedApp) + self.perform([operation], presentingViewController: presentingViewController, group: group) + 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 { installedApp.isActive = true completionHandler(.success(installedApp)) @@ -445,13 +451,15 @@ private extension AppManager case install(AppProtocol) case update(AppProtocol) case refresh(InstalledApp) + case activate(InstalledApp) case deactivate(InstalledApp) var app: AppProtocol { switch self { case .install(let app), .update(let app), - .refresh(let app as AppProtocol), .deactivate(let app as AppProtocol): + .refresh(let app as AppProtocol), .activate(let app as AppProtocol), + .deactivate(let app as AppProtocol): return app } } @@ -533,7 +541,7 @@ private extension AppManager } progress?.addChild(installProgress, withPendingUnitCount: 80) - case .activate(let app): fallthrough + case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: 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? @@ -559,6 +567,12 @@ private extension AppManager progress?.addChild(installProgress, withPendingUnitCount: 80) } + case .activate(let app): + let activateProgress = self._activate(app, operation: operation, group: group) { (result) in + self.finish(operation, result: result, group: group, progress: progress) + } + progress?.addChild(activateProgress, 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) @@ -791,6 +805,135 @@ private extension AppManager return progress } + private func _activate(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result) -> 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 restoreAppOperation = BackupAppOperation(action: .restore, context: restoreContext) + restoreAppOperation.resultHandler = { (result) in + switch result + { + case .success: break + case .failure(let error): + restoreContext.error = error + appContext.error = error + } + } + restoreAppOperation.addDependency(installBackupAppOperation) + progress.addChild(restoreAppOperation.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 + switch result + { + case .success(let installedApp): appContext.installedApp = installedApp + case .failure(let error): appContext.error = error + } + + operation.finish() + } + installAppProgress.addChild(progress, withPendingUnitCount: 100) + } + } + installAppOperation.addDependency(restoreAppOperation) + progress.addChild(installAppProgress, withPendingUnitCount: 50) + + let cleanUpProgress = Progress.discreteProgress(totalUnitCount: 100) + let cleanUpOperation = RSTAsyncBlockOperation { (operation) in + do + { + let installedApp = try Result(appContext.installedApp, appContext.error).get() + + var result: Result! + installedApp.managedObjectContext?.performAndWait { + result = Result { try installedApp.managedObjectContext?.save() } + } + try result.get() + + // Successfully saved, so _now_ we can remove backup. + + let removeAppBackupOperation = RemoveAppBackupOperation(context: appContext) + removeAppBackupOperation.resultHandler = { (result) in + installedApp.managedObjectContext?.perform { + switch result + { + case .failure(let error): + // Don't report error, since it doesn't really matter. + print("Failed to delete app backup.", error) + + case .success: break + } + + completionHandler(.success(installedApp)) + operation.finish() + } + } + cleanUpProgress.addChild(removeAppBackupOperation.progress, withPendingUnitCount: 100) + + group.add([removeAppBackupOperation]) + self.run([removeAppBackupOperation], context: group.context) + } + catch let error where restoreContext.installedApp != nil + { + // Activation failed, but restore app was installed, so remove the app. + + // Remove error so operation doesn't quit early, + restoreContext.error = nil + + let removeAppOperation = RemoveAppOperation(context: restoreContext) + removeAppOperation.resultHandler = { (result) in + completionHandler(.failure(error)) + operation.finish() + } + cleanUpProgress.addChild(removeAppOperation.progress, withPendingUnitCount: 100) + + group.add([removeAppOperation]) + self.run([removeAppOperation], context: group.context) + } + catch + { + // Activation failed. + completionHandler(.failure(error)) + operation.finish() + } + } + cleanUpOperation.addDependency(installAppOperation) + progress.addChild(cleanUpProgress, withPendingUnitCount: 5) + + group.add([installBackupAppOperation, restoreAppOperation, installAppOperation, cleanUpOperation]) + self.run([installBackupAppOperation, installAppOperation, restoreAppOperation, cleanUpOperation], context: group.context) + + return progress + } + private func _deactivate(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result) -> Void) -> Progress { let progress = Progress.discreteProgress(totalUnitCount: 100) @@ -978,7 +1121,7 @@ private extension AppManager event = nil case .update: event = .updatedApp(installedApp) - case .deactivate: event = nil + case .activate, .deactivate: event = nil } if let event = event @@ -1036,7 +1179,7 @@ private extension AppManager switch operation { case .install, .update: return self.installationProgress[operation.bundleIdentifier] - case .refresh, .deactivate: return self.refreshProgress[operation.bundleIdentifier] + case .refresh, .activate, .deactivate: return self.refreshProgress[operation.bundleIdentifier] } } @@ -1045,7 +1188,7 @@ private extension AppManager switch operation { case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress - case .refresh, .deactivate: self.refreshProgress[operation.bundleIdentifier] = progress + case .refresh, .activate, .deactivate: self.refreshProgress[operation.bundleIdentifier] = progress } } } From 4a893d3c80b1f440d55361246f3f11b614815670 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sat, 16 May 2020 16:34:50 -0700 Subject: [PATCH 21/37] Adds option to export backups for inactive apps --- AltStore/My Apps/MyAppsViewController.swift | 49 ++++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 232bb391..287af528 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -31,6 +31,8 @@ extension MyAppsViewController class MyAppsViewController: UICollectionViewController { + private let coordinator = NSFileCoordinator() + private lazy var dataSource = self.makeDataSource() private lazy var noUpdatesDataSource = self.makeNoUpdatesDataSource() private lazy var updatesDataSource = self.makeUpdatesDataSource() @@ -999,6 +1001,15 @@ private extension MyAppsViewController self.present(alertController, animated: true, completion: nil) } + + func exportBackup(for installedApp: InstalledApp) + { + guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return } + + let documentPicker = UIDocumentPickerViewController(url: backupURL, in: .exportToService) + documentPicker.delegate = self + self.present(documentPicker, animated: true, completion: nil) + } } private extension MyAppsViewController @@ -1186,6 +1197,10 @@ extension MyAppsViewController self.remove(installedApp) } + let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc")) { (action) in + self.exportBackup(for: installedApp) + } + guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else { return [refreshAction] } @@ -1199,6 +1214,29 @@ extension MyAppsViewController { actions.append(activateAction) } + + if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp), !UserDefaults.standard.isLegacyDeactivationSupported + { + var backupExists = false + var outError: NSError? = nil + + self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in + #if DEBUG + backupExists = true + #else + backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path) + #endif + } + + if backupExists + { + actions.append(exportBackupAction) + } + else if let error = outError + { + print("Unable to check if backup exists:", error) + } + } #if DEBUG @@ -1615,8 +1653,15 @@ extension MyAppsViewController: UIDocumentPickerDelegate { guard let fileURL = urls.first else { return } - self.sideloadApp(at: fileURL) { (result) in - print("Sideloaded app at \(fileURL) with result:", result) + switch controller.documentPickerMode + { + case .import, .open: + self.sideloadApp(at: fileURL) { (result) in + print("Sideloaded app at \(fileURL) with result:", result) + } + + case .exportToService, .moveToService: break + @unknown default: break } } } From 60abb9ee078093499f1ff644273bb45875990133 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sat, 16 May 2020 16:39:02 -0700 Subject: [PATCH 22/37] Adds option to manually restore backup for active apps that have one --- AltStore/Managing Apps/AppManager.swift | 43 +++++++++++++++++++-- AltStore/My Apps/MyAppsViewController.swift | 42 ++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 24d06980..27140b45 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -389,6 +389,32 @@ extension AppManager } } + func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result) -> 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 { + installedApp.isActive = true + completionHandler(.success(installedApp)) + } + } + catch + { + completionHandler(.failure(error)) + } + } + + let operation = AppOperation.restore(installedApp) + self.perform([operation], presentingViewController: presentingViewController, group: group) + } + func remove(_ installedApp: InstalledApp, completionHandler: @escaping (Result) -> Void) { let authenticationContext = AuthenticatedOperationContext() @@ -453,13 +479,14 @@ private extension AppManager case refresh(InstalledApp) case activate(InstalledApp) case deactivate(InstalledApp) + case restore(InstalledApp) var app: AppProtocol { switch self { case .install(let app), .update(let app), .refresh(let app as AppProtocol), .activate(let app as AppProtocol), - .deactivate(let app as AppProtocol): + .deactivate(let app as AppProtocol), .restore(let app as AppProtocol): return app } } @@ -578,6 +605,14 @@ private extension AppManager self.finish(operation, result: result, group: group, progress: progress) } progress?.addChild(deactivateProgress, withPendingUnitCount: 80) + + case .restore(let app): + // Restoring, which is effectively just activating an app. + + let activateProgress = self._activate(app, operation: operation, group: group) { (result) in + self.finish(operation, result: result, group: group, progress: progress) + } + progress?.addChild(activateProgress, withPendingUnitCount: 80) } } } @@ -1121,7 +1156,7 @@ private extension AppManager event = nil case .update: event = .updatedApp(installedApp) - case .activate, .deactivate: event = nil + case .activate, .deactivate, .restore: event = nil } if let event = event @@ -1179,7 +1214,7 @@ private extension AppManager switch operation { case .install, .update: return self.installationProgress[operation.bundleIdentifier] - case .refresh, .activate, .deactivate: return self.refreshProgress[operation.bundleIdentifier] + case .refresh, .activate, .deactivate, .restore: return self.refreshProgress[operation.bundleIdentifier] } } @@ -1188,7 +1223,7 @@ private extension AppManager switch operation { case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress - case .refresh, .activate, .deactivate: self.refreshProgress[operation.bundleIdentifier] = progress + case .refresh, .activate, .deactivate, .restore: self.refreshProgress[operation.bundleIdentifier] = progress } } } diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 287af528..33473a1d 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -1002,6 +1002,39 @@ private extension MyAppsViewController self.present(alertController, animated: true, completion: nil) } + func restore(_ installedApp: InstalledApp) + { + let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name) + let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet) + alertController.addAction(.cancel) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Restore Backup", comment: ""), style: .destructive, handler: { (action) in + AppManager.shared.restore(installedApp, presentingViewController: self) { (result) in + do + { + let app = try result.get() + try? app.managedObjectContext?.save() + + print("Finished restoring app:", app.bundleIdentifier) + } + catch + { + print("Failed to restore app:", error) + + DispatchQueue.main.async { + let toastView = ToastView(error: error) + toastView.show(in: self) + } + } + } + + DispatchQueue.main.async { + self.collectionView.reloadSections([Section.activeApps.rawValue]) + } + })) + + self.present(alertController, animated: true, completion: nil) + } + func exportBackup(for installedApp: InstalledApp) { guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return } @@ -1201,6 +1234,10 @@ extension MyAppsViewController self.exportBackup(for: installedApp) } + let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in + self.restore(installedApp) + } + guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else { return [refreshAction] } @@ -1231,6 +1268,11 @@ extension MyAppsViewController if backupExists { actions.append(exportBackupAction) + + if installedApp.isActive + { + actions.append(restoreBackupAction) + } } else if let error = outError { From b3f2474456911442285a318bcf15cb6f82967ef0 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 17 May 2020 22:20:27 -0700 Subject: [PATCH 23/37] [AltBackup] UI reflects whether backup/restore/nothing is happening --- AltBackup/ViewController.swift | 107 ++++++++++++++++++++++++----- AltStore.xcodeproj/project.pbxproj | 2 - AltStore/Resources/AltBackup.ipa | Bin 64161 -> 66640 bytes 3 files changed, 91 insertions(+), 18 deletions(-) diff --git a/AltBackup/ViewController.swift b/AltBackup/ViewController.swift index 8f1adeda..0efb7112 100644 --- a/AltBackup/ViewController.swift +++ b/AltBackup/ViewController.swift @@ -8,10 +8,41 @@ import UIKit +extension Bundle +{ + var appName: String? { + let appName = + Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? + Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String + return appName + } +} + +extension ViewController +{ + enum BackupOperation + { + case backup + case restore + } +} + class ViewController: UIViewController { private let backupController = BackupController() + private var currentOperation: BackupOperation? { + didSet { + DispatchQueue.main.async { + self.update() + } + } + } + + private var textLabel: UILabel! + private var detailTextLabel: UILabel! + private var activityIndicatorView: UIActivityIndicatorView! + override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } @@ -22,6 +53,7 @@ class ViewController: UIViewController NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) } required init?(coder: NSCoder) { @@ -34,14 +66,21 @@ class ViewController: UIViewController self.view.backgroundColor = .altstoreBackground - let textLabel = UILabel(frame: .zero) - textLabel.font = UIFont.preferredFont(forTextStyle: .title2) - textLabel.textColor = .altstoreText - textLabel.text = NSLocalizedString("Backing up app data…", comment: "") + self.textLabel = UILabel(frame: .zero) + self.textLabel.font = UIFont.preferredFont(forTextStyle: .title2) + self.textLabel.textColor = .altstoreText + self.textLabel.textAlignment = .center + self.textLabel.numberOfLines = 0 - let activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge) - activityIndicatorView.color = .altstoreText - activityIndicatorView.startAnimating() + self.detailTextLabel = UILabel(frame: .zero) + self.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body) + self.detailTextLabel.textColor = .altstoreText + self.detailTextLabel.textAlignment = .center + self.detailTextLabel.numberOfLines = 0 + + self.activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge) + self.activityIndicatorView.color = .altstoreText + self.activityIndicatorView.startAnimating() #if DEBUG let button1 = UIButton(type: .system) @@ -56,9 +95,9 @@ class ViewController: UIViewController button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered) - let arrangedSubviews = [textLabel, activityIndicatorView, button1, button2] + let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2] #else - let arrangedSubviews = [textLabel, activityIndicatorView] + let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!] #endif let stackView = UIStackView(arrangedSubviews: arrangedSubviews) @@ -69,7 +108,11 @@ class ViewController: UIViewController self.view.addSubview(stackView) NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)]) + stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor), + stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0), + self.view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)]) + + self.update() } } @@ -77,10 +120,10 @@ private extension ViewController { @objc func backup() { + self.currentOperation = .backup + self.backupController.performBackup { (result) in - let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? - Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String ?? - NSLocalizedString("App", comment: "") + let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "") let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName) self.process(result, errorTitle: title) @@ -89,15 +132,41 @@ private extension ViewController @objc func restore() { + self.currentOperation = .restore + self.backupController.restoreBackup { (result) in - let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? - Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String ?? - NSLocalizedString("App", comment: "") + let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "") let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName) self.process(result, errorTitle: title) } } + + func update() + { + switch self.currentOperation + { + case .backup: + self.textLabel.text = NSLocalizedString("Backing up app data…", comment: "") + self.detailTextLabel.isHidden = true + self.activityIndicatorView.startAnimating() + + case .restore: + self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "") + self.detailTextLabel.isHidden = true + self.activityIndicatorView.startAnimating() + + case .none: + self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""), + Bundle.main.appName ?? NSLocalizedString("App", comment: "")) + + self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in AltStore to continue using it.", comment: ""), + Bundle.main.appName ?? NSLocalizedString("this app", comment: "")) + + self.detailTextLabel.isHidden = false + self.activityIndicatorView.stopAnimating() + } + } } private extension ViewController @@ -128,4 +197,10 @@ private extension ViewController NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result]) } } + + @objc func didEnterBackground(_ notification: Notification) + { + // Reset UI once we've left app (but not before). + self.currentOperation = nil + } } diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 71cfe90d..24241fb2 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -2228,7 +2228,6 @@ BF58048C246A28F9008AE704 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CODE_SIGN_ENTITLEMENTS = AltBackup/AltBackup.entitlements; CODE_SIGN_STYLE = Automatic; @@ -2250,7 +2249,6 @@ BF58048D246A28F9008AE704 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CODE_SIGN_ENTITLEMENTS = AltBackup/AltBackup.entitlements; CODE_SIGN_STYLE = Automatic; diff --git a/AltStore/Resources/AltBackup.ipa b/AltStore/Resources/AltBackup.ipa index b08d0da0bb4b0bc2df87b5bfc09913026774ff37..627b12de35721d23e20a8e5a8bd67257d33c2029 100644 GIT binary patch delta 56417 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

lC`L&5Q=);|BYbn$iurI`CkIo1I*d-2J&Aihh}4 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 delta 53901 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
+D_)LOpULv=e(cg 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 LoPl&&Vt)G{{u40@ From 8bd4e25b7fdd4411a013b90be956b4bb30227e81 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 17 May 2020 22:22:25 -0700 Subject: [PATCH 24/37] Uses real app icon for AltBackup icon --- AltStore/Managing Apps/AppManager.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 27140b45..cb0da7e0 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -1056,6 +1056,26 @@ private extension AppManager exportedUTIs.append(installedAppUTI) infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs + if let cachedApp = ALTApplication(fileURL: app.fileURL), let icon = cachedApp.icon?.resizing(to: CGSize(width: 180, height: 180)) + { + let iconFileURL = unzippedAppBundleURL.appendingPathComponent("AppIcon.png") + + if let iconData = icon.pngData() + { + do + { + try iconData.write(to: iconFileURL, options: .atomic) + + let bundleIcons = ["CFBundlePrimaryIcon": ["CFBundleIconFiles": [iconFileURL.lastPathComponent]]] + infoDictionary["CFBundleIcons"] = bundleIcons + } + catch + { + print("Failed to write app icon data.", error) + } + } + } + try (infoDictionary as NSDictionary).write(to: unzippedAppBundle.infoPlistURL) } From e0dea67380ba78b973224252e0269ab7caf2d355 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 17 May 2020 22:27:00 -0700 Subject: [PATCH 25/37] [AltServer] Adds wired connection reading timeout --- AltServer/Connections/ALTWiredConnection.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AltServer/Connections/ALTWiredConnection.m b/AltServer/Connections/ALTWiredConnection.m index 0b941730..75efac24 100644 --- a/AltServer/Connections/ALTWiredConnection.m +++ b/AltServer/Connections/ALTWiredConnection.m @@ -85,7 +85,7 @@ uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length); uint32_t receivedBytes = 0; - if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 0) != IDEVICE_E_SUCCESS) + if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 10000) != IDEVICE_E_SUCCESS) { return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]); } From 39b60a07d9bfc55ea36d19de9e5ed39f50f90f73 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 17 May 2020 23:36:30 -0700 Subject: [PATCH 26/37] Removes active app extension limits on 13.5 or later --- .../Extensions/UserDefaults+AltStore.swift | 7 ++- AltStore/Model/InstalledApp.swift | 5 ++ AltStore/My Apps/MyAppsViewController.swift | 49 ++++++++++++++++--- AltStore/Operations/InstallAppOperation.swift | 6 +-- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/AltStore/Extensions/UserDefaults+AltStore.swift b/AltStore/Extensions/UserDefaults+AltStore.swift index 2e04e5d5..e0e59f7e 100644 --- a/AltStore/Extensions/UserDefaults+AltStore.swift +++ b/AltStore/Extensions/UserDefaults+AltStore.swift @@ -23,6 +23,7 @@ extension UserDefaults @NSManaged var legacySideloadedApps: [String]? @NSManaged var isLegacyDeactivationSupported: Bool + @NSManaged var activeAppLimitIncludesExtensions: Bool var activeAppsLimit: Int? { get { @@ -43,9 +44,13 @@ extension UserDefaults func registerDefaults() { + let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0) + let activeAppLimitIncludesExtensions = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5) + self.register(defaults: [ #keyPath(UserDefaults.isBackgroundRefreshEnabled): true, - #keyPath(UserDefaults.isLegacyDeactivationSupported): false + #keyPath(UserDefaults.isLegacyDeactivationSupported): false, + #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions ]) } } diff --git a/AltStore/Model/InstalledApp.swift b/AltStore/Model/InstalledApp.swift index 36170e0c..decd28bd 100644 --- a/AltStore/Model/InstalledApp.swift +++ b/AltStore/Model/InstalledApp.swift @@ -56,6 +56,11 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol return 1 + self.appExtensions.count } + var requiredActiveSlots: Int { + let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1 + return requiredActiveSlots + } + private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { super.init(entity: entity, insertInto: context) diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 33473a1d..a74afe56 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -809,7 +809,18 @@ private extension MyAppsViewController @objc func presentInactiveAppsAlert() { - let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: ""), preferredStyle: .alert) + let message: String + + if UserDefaults.standard.activeAppLimitIncludesExtensions + { + message = NSLocalizedString("Free developer accounts are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: "") + } + else + { + message = NSLocalizedString("Free developer accounts are limited to 3 apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again.", comment: "") + } + + let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: message, preferredStyle: .alert) alertController.addAction(.ok) self.present(alertController, animated: true, completion: nil) } @@ -929,12 +940,34 @@ private extension MyAppsViewController let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext) .filter { $0.bundleIdentifier != installedApp.bundleIdentifier } // Don't count app towards total if it matches activating app - let activeAppsCount = activeApps.map { $0.appIDCount }.reduce(0, +) + var title: String = NSLocalizedString("Cannot Activate More than 3 Apps", comment: "") + let message: String + if UserDefaults.standard.activeAppLimitIncludesExtensions + { + if installedApp.appExtensions.isEmpty + { + message = NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions. Please choose an app to deactivate.", comment: "") + } + else + { + title = NSLocalizedString("Cannot Activate More than 3 Apps and App Extensions", comment: "") + + let appExtensionText = installedApp.appExtensions.count == 1 ? NSLocalizedString("app extension", comment: "") : NSLocalizedString("app extensions", comment: "") + message = String(format: NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), installedApp.name, NSNumber(value: installedApp.appExtensions.count), appExtensionText) + } + } + else + { + message = NSLocalizedString("Free developer accounts are limited to 3 active apps. Please choose an app to deactivate.", comment: "") + } + + let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +) + let availableActiveApps = max(activeAppsLimit - activeAppsCount, 0) - guard installedApp.appIDCount > availableActiveApps else { return completion(true) } + guard installedApp.requiredActiveSlots > availableActiveApps else { return completion(true) } - let alertController = UIAlertController(title: NSLocalizedString("Cannot Activate More than 3 Apps", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions. Please choose an app to deactivate.", comment: ""), preferredStyle: .alert) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { (action) in completion(false) }) @@ -942,8 +975,8 @@ private extension MyAppsViewController for app in activeApps where app.bundleIdentifier != StoreApp.altstoreAppID { alertController.addAction(UIAlertAction(title: app.name, style: .default) { (action) in - let availableActiveApps = availableActiveApps + app.appIDCount - if availableActiveApps >= installedApp.appIDCount + let availableActiveApps = availableActiveApps + app.requiredActiveSlots + if availableActiveApps >= installedApp.requiredActiveSlots { // There are enough slots now to activate the app, so pre-emptively // mark it as active to provide visual feedback sooner. @@ -1572,10 +1605,10 @@ extension MyAppsViewController: UICollectionViewDropDelegate return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) } - let activeAppsCount = (self.activeAppsDataSource.fetchedResultsController.fetchedObjects ?? []).map { $0.appIDCount }.reduce(0, +) + let activeAppsCount = (self.activeAppsDataSource.fetchedResultsController.fetchedObjects ?? []).map { $0.requiredActiveSlots }.reduce(0, +) let availableActiveApps = max(activeAppsLimit - activeAppsCount, 0) - if installedApp.appIDCount <= availableActiveApps + if installedApp.requiredActiveSlots <= availableActiveApps { // Enough active app slots, so no need to deactivate app first. return UICollectionViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath) diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index 0983f93c..52fdbabb 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -122,10 +122,10 @@ class InstallAppOperation: ResultOperation var activeApps = InstalledApp.fetch(fetchRequest, in: backgroundContext) if !activeApps.contains(installedApp) { - let availableActiveApps = max(sideloadedAppsLimit - activeApps.count, 0) - let requiredActiveAppSlots = 1 + installedExtensions.count // As of iOS 13.3.1, app extensions count as "apps" + let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +) - if requiredActiveAppSlots <= availableActiveApps + let availableActiveApps = max(sideloadedAppsLimit - activeAppsCount, 0) + if installedApp.requiredActiveSlots <= availableActiveApps { // This app has not been explicitly activated, but there are enough slots available, // so implicitly activate it. From 05dc365dffa5a5bc4337051a7129938293b27ec9 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 17 May 2020 23:44:36 -0700 Subject: [PATCH 27/37] =?UTF-8?q?Adds=20altstore://install=3Furl=3D[link]?= =?UTF-8?q?=20deep=20link=20to=20install=20remote=20.ipa=E2=80=99s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AltStore/AppDelegate.swift | 10 + AltStore/My Apps/MyAppsViewController.swift | 281 +++++++++++++++----- 2 files changed, 221 insertions(+), 70 deletions(-) diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index f31273b7..cf3cae3a 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -172,6 +172,16 @@ private extension AppDelegate return true + case "install": + let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:] + guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL]) + } + + return true + default: return false } } diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index a74afe56..4022bf90 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -32,6 +32,7 @@ extension MyAppsViewController class MyAppsViewController: UICollectionViewController { private let coordinator = NSFileCoordinator() + private let operationQueue = OperationQueue() private lazy var dataSource = self.makeDataSource() private lazy var noUpdatesDataSource = self.makeNoUpdatesDataSource() @@ -694,14 +695,157 @@ private extension MyAppsViewController self.present(documentPickerViewController, animated: true, completion: nil) } - func sideloadApp(at fileURL: URL, completion: @escaping (Result) -> Void) + func sideloadApp(at url: URL, completion: @escaping (Result) -> Void) { - let temporaryDirectory = FileManager.default.uniqueTemporaryURL() + let progress = Progress.discreteProgress(totalUnitCount: 100) self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true - func finish(_ result: Result) + class Context { + var fileURL: URL? + var application: ALTApplication? + var installedApp: InstalledApp? { + didSet { + self.installedAppContext = self.installedApp?.managedObjectContext + } + } + private var installedAppContext: NSManagedObjectContext? + + var error: Error? + } + + let temporaryDirectory = FileManager.default.uniqueTemporaryURL() + let unzippedAppDirectory = temporaryDirectory.appendingPathComponent("App") + + let context = Context() + + let downloadOperation: RSTAsyncBlockOperation? + + if url.isFileURL + { + downloadOperation = nil + context.fileURL = url + progress.totalUnitCount -= 20 + } + else + { + let downloadProgress = Progress.discreteProgress(totalUnitCount: 100) + downloadOperation = RSTAsyncBlockOperation { (operation) in + let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in + do + { + let (fileURL, _) = try Result((fileURL, response), error).get() + + try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) + + let destinationURL = temporaryDirectory.appendingPathComponent("App.ipa") + try FileManager.default.moveItem(at: fileURL, to: destinationURL) + + context.fileURL = destinationURL + } + catch + { + context.error = error + } + operation.finish() + } + downloadProgress.addChild(downloadTask.progress, withPendingUnitCount: 100) + downloadTask.resume() + } + progress.addChild(downloadProgress, withPendingUnitCount: 20) + } + + let unzipProgress = Progress.discreteProgress(totalUnitCount: 1) + let unzipAppOperation = BlockOperation { + do + { + if let error = context.error + { + throw error + } + + guard let fileURL = context.fileURL else { throw OperationError.invalidParameters } + + try FileManager.default.createDirectory(at: unzippedAppDirectory, withIntermediateDirectories: true, attributes: nil) + let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: unzippedAppDirectory) + + guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp } + context.application = application + + unzipProgress.completedUnitCount = 1 + } + catch + { + context.error = error + } + } + progress.addChild(unzipProgress, withPendingUnitCount: 10) + + if let downloadOperation = downloadOperation + { + unzipAppOperation.addDependency(downloadOperation) + } + + let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1) + let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in + do + { + if let error = context.error + { + throw error + } + + guard let application = context.application else { throw OperationError.invalidParameters } + + DispatchQueue.main.async { + self?.removeAppExtensions(from: application) { (result) in + switch result + { + case .success: removeAppExtensionsProgress.completedUnitCount = 1 + case .failure(let error): context.error = error + } + operation.finish() + } + } + } + catch + { + context.error = error + operation.finish() + } + } + removeAppExtensionsOperation.addDependency(unzipAppOperation) + progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5) + + let installProgress = Progress.discreteProgress(totalUnitCount: 100) + let installAppOperation = RSTAsyncBlockOperation { (operation) in + do + { + if let error = context.error + { + throw error + } + + guard let application = context.application else { throw OperationError.invalidParameters } + + let progress = AppManager.shared.install(application, presentingViewController: self) { (result) in + switch result + { + case .success(let installedApp): context.installedApp = installedApp + case .failure(let error): context.error = error + } + operation.finish() + } + installProgress.addChild(progress, withPendingUnitCount: 100) + } + catch + { + context.error = error + operation.finish() + } + } + installAppOperation.completionBlock = { try? FileManager.default.removeItem(at: temporaryDirectory) DispatchQueue.main.async { @@ -709,13 +853,17 @@ private extension MyAppsViewController self.sideloadingProgressView.observedProgress = nil self.sideloadingProgressView.setHidden(true, animated: true) - switch result + switch Result(context.installedApp, context.error) { case .success(let app): - print("Successfully installed app:", app.bundleIdentifier) completion(.success(())) - case .failure(OperationError.cancelled): break + app.managedObjectContext?.perform { + print("Successfully installed app:", app.bundleIdentifier) + } + + case .failure(OperationError.cancelled): + completion(.failure((OperationError.cancelled))) case .failure(let error): let toastView = ToastView(error: error) @@ -725,68 +873,16 @@ private extension MyAppsViewController } } } + progress.addChild(installProgress, withPendingUnitCount: 65) + installAppOperation.addDependency(removeAppExtensionsOperation) - DispatchQueue.global().async { - do - { - try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) - - let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory) - - guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp } - - func install() - { - self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in - finish(result.map { _ in application }) - } - - DispatchQueue.main.async { - self.sideloadingProgressView.progress = 0 - self.sideloadingProgressView.isHidden = false - self.sideloadingProgressView.observedProgress = self.sideloadingProgress - } - } - - if !application.appExtensions.isEmpty - { - DispatchQueue.main.async { - let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions. Would you like to remove this app's app extensions so they don't count towards your limit?", comment: ""), preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in - finish(.failure(OperationError.cancelled)) - })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in - install() - }) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in - do - { - for appExtension in application.appExtensions - { - try FileManager.default.removeItem(at: appExtension.fileURL) - } - - install() - } - catch - { - finish(.failure(error)) - } - }) - - self.present(alertController, animated: true, completion: nil) - } - } - else - { - install() - } - } - catch - { - finish(.failure(error)) - } - } + self.sideloadingProgress = progress + self.sideloadingProgressView.progress = 0 + self.sideloadingProgressView.isHidden = false + self.sideloadingProgressView.observedProgress = self.sideloadingProgress + + let operations = [downloadOperation, unzipAppOperation, removeAppExtensionsOperation, installAppOperation].compactMap { $0 } + self.operationQueue.addOperations(operations, waitUntilFinished: false) } @IBAction func activateApp(_ sender: UIButton) @@ -834,6 +930,49 @@ private extension MyAppsViewController cell.bannerView.iconImageView.isIndicatingActivity = false } + + func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result) -> Void) + { + guard !application.appExtensions.isEmpty else { return completion(.success(())) } + + let firstSentence: String + + if UserDefaults.standard.activeAppLimitIncludesExtensions + { + firstSentence = NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions.", comment: "") + } + else + { + firstSentence = NSLocalizedString("Free developer accounts are limited to creating 10 App IDs per week.", comment: "") + } + + let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "") + + let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in + completion(.failure(OperationError.cancelled)) + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in + completion(.success(())) + }) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in + do + { + for appExtension in application.appExtensions + { + try FileManager.default.removeItem(at: appExtension.fileURL) + } + + completion(.success(())) + } + catch + { + completion(.failure(error)) + } + }) + + self.present(alertController, animated: true, completion: nil) + } } private extension MyAppsViewController @@ -1098,12 +1237,14 @@ private extension MyAppsViewController // Make sure left UIBarButtonItem has been set. self.loadViewIfNeeded() - guard let fileURL = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return } + guard let url = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return } - self.sideloadApp(at: fileURL) { (result) in + self.sideloadApp(at: url) { (result) in + guard url.isFileURL else { return } + do { - try FileManager.default.removeItem(at: fileURL) + try FileManager.default.removeItem(at: url) } catch { From 17594a51d19cfcc1017a707b805f2ba35b029125 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 18 May 2020 00:03:37 -0700 Subject: [PATCH 28/37] Limits new (de-)activation flow to 13.5 or later --- AltStore/Extensions/UserDefaults+AltStore.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AltStore/Extensions/UserDefaults+AltStore.swift b/AltStore/Extensions/UserDefaults+AltStore.swift index e0e59f7e..af6d5bc8 100644 --- a/AltStore/Extensions/UserDefaults+AltStore.swift +++ b/AltStore/Extensions/UserDefaults+AltStore.swift @@ -45,11 +45,12 @@ extension UserDefaults func registerDefaults() { let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0) + let isLegacyDeactivationSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5) let activeAppLimitIncludesExtensions = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5) self.register(defaults: [ #keyPath(UserDefaults.isBackgroundRefreshEnabled): true, - #keyPath(UserDefaults.isLegacyDeactivationSupported): false, + #keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported, #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions ]) } From da2370d9ac03a1a5f58022076b7f95e6602019e9 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 18 May 2020 16:00:08 -0700 Subject: [PATCH 29/37] =?UTF-8?q?Fixes=20=E2=80=9Cinvalid=20entitlements?= =?UTF-8?q?=E2=80=9D=20when=20refreshing=20AltStore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces “resigned” app group ID with “base” app group ID before resigning AltStore. --- .../FetchProvisioningProfilesOperation.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift index a7bdb49d..9a3c0eab 100644 --- a/AltStore/Operations/FetchProvisioningProfilesOperation.swift +++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift @@ -362,7 +362,7 @@ extension FetchProvisioningProfilesOperation entitlements[key] = value } - let applicationGroups = entitlements[.appGroups] as? [String] ?? [] + var applicationGroups = entitlements[.appGroups] as? [String] ?? [] if applicationGroups.isEmpty { guard let isAppGroupsEnabled = appID.features[.appGroups] as? Bool, isAppGroupsEnabled else { @@ -374,6 +374,22 @@ extension FetchProvisioningProfilesOperation } } + if app.bundleIdentifier == StoreApp.altstoreAppID + { + // Updating app groups for this specific AltStore. + // Find the (unique) AltStore app group, then replace it + // with the correct "base" app group ID. + // Otherwise, we may append a duplicate team identifier to the end. + if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) }) + { + applicationGroups[index] = Bundle.baseAltStoreAppGroupID + } + else + { + applicationGroups.append(Bundle.baseAltStoreAppGroupID) + } + } + // Dispatch onto global queue to prevent appGroupsLock deadlock. DispatchQueue.global().async { From fff128e1ce1e64ad00e82128e4308a7a5f834f95 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 19 May 2020 11:47:43 -0700 Subject: [PATCH 30/37] Adds option to explicitly back up installed apps --- AltStore/Managing Apps/AppManager.swift | 106 ++++++++++++++++++-- AltStore/My Apps/MyAppsViewController.swift | 64 +++++++++++- 2 files changed, 162 insertions(+), 8 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index cb0da7e0..f710f71a 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -389,6 +389,31 @@ extension AppManager } } + func backup(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result) -> 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) -> Void) { let group = RefreshGroup() @@ -479,14 +504,15 @@ private extension AppManager case refresh(InstalledApp) case activate(InstalledApp) case deactivate(InstalledApp) + case backup(InstalledApp) case restore(InstalledApp) var app: AppProtocol { switch self { - case .install(let app), .update(let app), - .refresh(let app as AppProtocol), .activate(let app as AppProtocol), - .deactivate(let app as AppProtocol), .restore(let app as AppProtocol): + case .install(let app), .update(let app), .refresh(let app as AppProtocol), + .activate(let app as AppProtocol), .deactivate(let app as AppProtocol), + .backup(let app as AppProtocol), .restore(let app as AppProtocol): return app } } @@ -606,6 +632,12 @@ private extension AppManager } 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): // Restoring, which is effectively just activating an app. @@ -1017,6 +1049,68 @@ private extension AppManager return progress } + private func _backup(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result) -> 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) -> Void) -> Progress { let progress = Progress.discreteProgress(totalUnitCount: 100) @@ -1176,7 +1270,7 @@ private extension AppManager event = nil case .update: event = .updatedApp(installedApp) - case .activate, .deactivate, .restore: event = nil + case .activate, .deactivate, .backup, .restore: event = nil } if let event = event @@ -1234,7 +1328,7 @@ private extension AppManager switch operation { 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 { 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 } } } diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 4022bf90..a646a31b 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -1174,6 +1174,45 @@ private extension MyAppsViewController 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) { 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) } + 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 self.exportBackup(for: installedApp) } @@ -1419,14 +1462,26 @@ extension MyAppsViewController if installedApp.isActive { actions.append(refreshAction) - actions.append(deactivateAction) } else { actions.append(activateAction) } + + 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), !UserDefaults.standard.isLegacyDeactivationSupported + if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) { var backupExists = false var outError: NSError? = nil @@ -1454,6 +1509,11 @@ extension MyAppsViewController } } + if installedApp.isActive + { + actions.append(deactivateAction) + } + #if DEBUG if installedApp.bundleIdentifier != StoreApp.altstoreAppID From f564fc519022a1023c453e36b268cdce67058834 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 19 May 2020 18:30:53 -0700 Subject: [PATCH 31/37] [AltServer] Supports app groups when installing AltStore Necessary for (de-)activation to work as expected in AltStore 1.3.4. --- .../ALTDeviceManager+Installation.swift | 157 +++++++++++++++++- 1 file changed, 148 insertions(+), 9 deletions(-) diff --git a/AltServer/Devices/ALTDeviceManager+Installation.swift b/AltServer/Devices/ALTDeviceManager+Installation.swift index a4dee469..f2b0bb58 100644 --- a/AltServer/Devices/ALTDeviceManager+Installation.swift +++ b/AltServer/Devices/ALTDeviceManager+Installation.swift @@ -16,6 +16,8 @@ private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore-sta private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")! #endif +private let appGroupsLock = NSLock() + enum InstallError: LocalizedError { case cancelled @@ -125,18 +127,29 @@ extension ALTDeviceManager { let appID = try result.get() - self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in + self.updateAppGroups(for: appID, app: application, team: team, session: session) { (result) in do { - let provisioningProfile = try result.get() + let appID = try result.get() - self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in - finish(result.error, title: "Failed to Install AltStore") + self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in + do + { + let provisioningProfile = try result.get() + + self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in + finish(result.error, title: "Failed to Install AltStore") + } + } + catch + { + finish(error, title: "Failed to Fetch Provisioning Profile") + } } } catch { - finish(error, title: "Failed to Fetch Provisioning Profile") + finish(error, title: "Failed to Update App Groups") } } } @@ -468,11 +481,119 @@ To prevent this from happening, feel free to try again with another Apple ID to features[.appGroups] = true } - let appID = appID.copy() as! ALTAppID - appID.features = features + var updateFeatures = false - ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in - completionHandler(Result(appID, error)) + // Determine whether the required features are already enabled for the AppID. + for (feature, value) in features + { + if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue) + { + // AppID already has this feature enabled and the values are the same. + continue + } + else + { + // AppID either doesn't have this feature enabled or the value has changed, + // so we need to update it to reflect new values. + updateFeatures = true + break + } + } + + if updateFeatures + { + let appID = appID.copy() as! ALTAppID + appID.features = features + + ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in + completionHandler(Result(appID, error)) + } + } + else + { + completionHandler(.success(appID)) + } + } + + func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + { + let applicationGroups = app.entitlements[.appGroups] as? [String] ?? [] + if applicationGroups.isEmpty + { + guard let isAppGroupsEnabled = appID.features[.appGroups] as? Bool, isAppGroupsEnabled else { + // No app groups, and we also haven't enabled the feature, so don't continue. + // For apps with no app groups but have had the feature enabled already + // we'll continue and assign the app ID to an empty array + // in case we need to explicitly remove them. + return completionHandler(.success(appID)) + } + } + + // Dispatch onto global queue to prevent appGroupsLock deadlock. + DispatchQueue.global().async { + + // Ensure we're not concurrently fetching and updating app groups, + // which can lead to race conditions such as adding an app group twice. + appGroupsLock.lock() + + func finish(_ result: Result) + { + appGroupsLock.unlock() + completionHandler(result) + } + + ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in + switch Result(groups, error) + { + case .failure(let error): finish(.failure(error)) + case .success(let fetchedGroups): + let dispatchGroup = DispatchGroup() + + var groups = [ALTAppGroup]() + var errors = [Error]() + + for groupIdentifier in applicationGroups + { + let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier + + if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier }) + { + groups.append(group) + } + else + { + dispatchGroup.enter() + + // Not all characters are allowed in group names, so we replace periods with spaces (like Apple does). + let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ") + + ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in + switch Result(group, error) + { + case .success(let group): groups.append(group) + case .failure(let error): errors.append(error) + } + + dispatchGroup.leave() + } + } + } + + dispatchGroup.notify(queue: .global()) { + if let error = errors.first + { + finish(.failure(error)) + } + else + { + ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in + let result = Result(success, error) + finish(result.map { _ in appID }) + } + } + } + } + } } } @@ -520,6 +641,24 @@ To prevent this from happening, feel free to try again with another Apple ID to infoDictionary[Bundle.Info.deviceID] = device.identifier infoDictionary[Bundle.Info.serverID] = UserDefaults.standard.serverID infoDictionary[Bundle.Info.certificateID] = certificate.serialNumber + + let openAppURL = URL(string: "altstore-" + application.bundleIdentifier + "://")! + + var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? [] + + // Embed open URL so AltBackup can return to AltStore. + let altstoreURLScheme = ["CFBundleTypeRole": "Editor", + "CFBundleURLName": application.bundleIdentifier, + "CFBundleURLSchemes": [openAppURL.scheme!]] as [String : Any] + allURLSchemes.append(altstoreURLScheme) + + infoDictionary[Bundle.Info.urlTypes] = allURLSchemes + + if let appGroups = profile.entitlements[.appGroups] as? [String] + { + infoDictionary[Bundle.Info.appGroups] = appGroups + } + try (infoDictionary as NSDictionary).write(to: infoPlistURL) if From 540c9cc8afffaaf92e250f9d4c811d3410955bbf Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 19 May 2020 20:09:50 -0700 Subject: [PATCH 32/37] [AltServer] Updates app version to 1.3.1 --- AltStore.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 24241fb2..706fc11d 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -2056,7 +2056,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 21; DEVELOPMENT_TEAM = 6XVY5G3U44; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -2089,7 +2089,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14.4; - MARKETING_VERSION = 1.3; + MARKETING_VERSION = 1.3.1; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -2109,7 +2109,7 @@ CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 21; DEVELOPMENT_TEAM = 6XVY5G3U44; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -2142,7 +2142,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14.4; - MARKETING_VERSION = 1.3; + MARKETING_VERSION = 1.3.1; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; From 64f8983d2949ba918d71b1c8edd894eaff968eaf Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 19 May 2020 20:10:55 -0700 Subject: [PATCH 33/37] Updates app version to 1.3.4 --- AltStore.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 706fc11d..61ef0815 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -2457,7 +2457,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.2; + MARKETING_VERSION = 1.3.4; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2485,7 +2485,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.2; + MARKETING_VERSION = 1.3.4; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 2411cca51f46c4013ed8feab3e5b4e964134257c Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 21 May 2020 21:00:05 -0700 Subject: [PATCH 34/37] =?UTF-8?q?[AltServer]=20Suggests=20disabling=20?= =?UTF-8?q?=E2=80=9COffload=20Unused=20Apps=E2=80=9D=20in=20error=20messag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS 13.5 counts offloaded apps as active sideloaded apps (for some reason), so improve error messages to mention this. --- AltKit/CodableServerError.swift | 8 +++++++- AltKit/NSError+ALTServerError.m | 3 +++ AltServer/AppDelegate.swift | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/AltKit/CodableServerError.swift b/AltKit/CodableServerError.swift index 5dff7787..7ada01df 100644 --- a/AltKit/CodableServerError.swift +++ b/AltKit/CodableServerError.swift @@ -30,7 +30,13 @@ struct CodableServerError: Codable { self.errorCode = error.code - let userInfo = error.userInfo.compactMapValues { $0 as? String } + var userInfo = error.userInfo.compactMapValues { $0 as? String } + + if let localizedRecoverySuggestion = (error as NSError).localizedRecoverySuggestion + { + userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion + } + if !userInfo.isEmpty { self.userInfo = userInfo diff --git a/AltKit/NSError+ALTServerError.m b/AltKit/NSError+ALTServerError.m index c82da672..3fdf5ad1 100644 --- a/AltKit/NSError+ALTServerError.m +++ b/AltKit/NSError+ALTServerError.m @@ -112,6 +112,9 @@ NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdenti case ALTServerErrorPluginNotFound: return NSLocalizedString(@"Make sure Mail is running and the plug-in is enabled in Mail's preferences.", @""); + case ALTServerErrorMaximumFreeAppLimitReached: + return NSLocalizedString(@"Make sure “Offload Unused Apps” is disabled in Settings > iTunes & App Stores, then install or delete all offloaded apps.", @""); + default: return nil; } diff --git a/AltServer/AppDelegate.swift b/AltServer/AppDelegate.swift index 34a3790f..84382a6f 100644 --- a/AltServer/AppDelegate.swift +++ b/AltServer/AppDelegate.swift @@ -212,6 +212,10 @@ private extension AppDelegate { alert.informativeText = underlyingError.localizedDescription } + else if let recoverySuggestion = error.localizedRecoverySuggestion + { + alert.informativeText = error.localizedDescription + "\n\n" + recoverySuggestion + } else { alert.informativeText = error.localizedDescription From 284f90ccd379ce80281efa3ffe7d654cd6758c6a Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 21 May 2020 22:06:18 -0700 Subject: [PATCH 35/37] [AltServer] Improves error message when device is untrusted or locked during installation --- AltKit/NSError+ALTServerError.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AltKit/NSError+ALTServerError.m b/AltKit/NSError+ALTServerError.m index 3fdf5ad1..02bc5541 100644 --- a/AltKit/NSError+ALTServerError.m +++ b/AltKit/NSError+ALTServerError.m @@ -52,7 +52,11 @@ NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdenti return NSLocalizedString(@"An unknown error occured.", @""); case ALTServerErrorConnectionFailed: +#if TARGET_OS_OSX + return NSLocalizedString(@"Could not connect to device.", @""); +#else return NSLocalizedString(@"Could not connect to AltServer.", @""); +#endif case ALTServerErrorLostConnection: return NSLocalizedString(@"Lost connection to AltServer.", @""); @@ -107,7 +111,7 @@ NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdenti { case ALTServerErrorConnectionFailed: case ALTServerErrorDeviceNotFound: - return NSLocalizedString(@"Make sure you have trusted this phone with your computer and WiFi sync is enabled.", @""); + return NSLocalizedString(@"Make sure you have trusted this device with your computer and WiFi sync is enabled.", @""); case ALTServerErrorPluginNotFound: return NSLocalizedString(@"Make sure Mail is running and the plug-in is enabled in Mail's preferences.", @""); From f9342acb30396001d46d860c753049dfb6dd37d5 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Wed, 27 May 2020 10:10:32 -0700 Subject: [PATCH 36/37] [AltServer] Updates app version to 1.3.2 --- AltStore.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 61ef0815..8724c742 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -2089,7 +2089,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14.4; - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.3.2; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -2142,7 +2142,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14.4; - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.3.2; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; From 0b36214bb5d1664602e4f2474d04071b42fadcf9 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Wed, 27 May 2020 10:11:02 -0700 Subject: [PATCH 37/37] Updates apps.json for 1.3.4 --- AltStore/Resources/apps-alpha.json | 8 ++++---- AltStore/Resources/apps.json | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/AltStore/Resources/apps-alpha.json b/AltStore/Resources/apps-alpha.json index bf40d125..f462b3b4 100644 --- a/AltStore/Resources/apps-alpha.json +++ b/AltStore/Resources/apps-alpha.json @@ -7,10 +7,10 @@ "bundleIdentifier": "com.rileytestut.AltStore.Alpha", "developerName": "Riley Testut", "subtitle": "An alternative App Store for iOS.", - "version": "1.3.3a1", - "versionDate": "2020-05-09T12:00:00-07:00", - "versionDescription": "NEW\n• Fixes \"App ID already registered\" error when updating DolphiniOS\n• Verifies app's bundle ID matches the source's when downloading from a source.", - "downloadURL": "https://f000.backblazeb2.com/file/altstore/sources/alpha/altstore/1_3_3_a1.ipa", + "version": "1.3.4a6", + "versionDate": "2020-05-19T13:00:00-07:00", + "versionDescription": "** Requires latest AltServer beta available for download in Discord **\n\nNEW:\n- Adds \"Back Up\" option to installed apps context menu\n\nPREVIOUS:\n- Removes active app extension limits on devices running iOS 13.5 or later\n- Correctly says \"backing up\" or \"restoring\" when opening temporary app during (de-)activation\n- Uses real app icon for temporary app during (de-)activation \n- Limits new (de-)activation method to iOS 13.5 or later\n- Fixes invalid entitlements when AltStore refreshes itself\n\nDue to iOS 13.5 changes, apps are now backed up & deleted when marked as inactive. When you later activate an inactive app, AltStore will reinstall the app, then restore your data from before.", + "downloadURL": "https://f000.backblazeb2.com/file/altstore/sources/alpha/altstore/1_3_4_a6.ipa", "localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore allows you to install Delta as well as any app (.ipa) directly from the Files app.", "iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png", "tintColor": "018084", diff --git a/AltStore/Resources/apps.json b/AltStore/Resources/apps.json index 0956094f..806b4516 100644 --- a/AltStore/Resources/apps.json +++ b/AltStore/Resources/apps.json @@ -7,14 +7,14 @@ "name": "AltStore", "bundleIdentifier": "com.rileytestut.AltStore", "developerName": "Riley Testut", - "version": "1.3.3", - "versionDate": "2020-05-09T12:00:00-07:00", - "versionDescription": "NEW\n• Fixes \"App ID already registered\" and other errors when updating DolphiniOS\n\nPREVIOUS UPDATES:\n• Adds support for installing apps using private entitlements via Psychic Paper (https://siguza.github.io/psychicpaper/)\n• Displays \"Grant Permission\" alert that lists all private permissions when installing an app that uses them.\n• Sideload any app (.ipa file) directly from Files.\n• Manage which apps are (in-)active at any time from the My Apps tab (optionally via drag and drop)\n\nNOTE: No permission alert will appear for apps without private entitlements.", - "downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/altstore/1_3_3.ipa", + "version": "1.3.4", + "versionDate": "2020-05-20T10:00:00-07:00", + "versionDescription": "** iOS 13.5 Compatibility Update **\n\niOS 13.5 changes how active apps are counted. Previously, app extensions counted towards the 3 active app limit, and inactive apps could remain installed without counting. Now, app extensions are excluded, but inactive apps must be uninstalled to not count.\n\nOn devices running iOS 13.5 or later, apps will now be backed up & uninstalled when deactivated. When you later activate an inactive app, AltStore will reinstall the app and restore its data so you can continue using it as if it was never uninstalled.\n\nNOTE: This does not affect devices running iOS 13.4.1 or earlier.", + "downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/altstore/1_3_4.ipa", "localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis version of AltStore allows you to install Delta, an all-in-one emulator for iOS, as well as sideload other .ipa files from the Files app.", "iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png", "tintColor": "018084", - "size": 2541786, + "size": 2665222, "screenshotURLs": [ "https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG", "https://user-images.githubusercontent.com/705880/78942222-0fe6da00-7a6e-11ea-9f2a-dda16157583c.PNG", @@ -36,14 +36,14 @@ "bundleIdentifier": "com.rileytestut.AltStore.Beta", "developerName": "Riley Testut", "subtitle": "An alternative App Store for iOS.", - "version": "1.3.3b1", - "versionDate": "2020-05-08T12:00:00-07:00", - "versionDescription": "NEW\n• Fixes \"App ID already registered\" error when updating DolphiniOS\n• Verifies app's bundle ID matches the source's when downloading from a source.\n\nPREVIOUS UPDATES\n• Adds support for installing apps using private entitlements via Psychic Paper (https://siguza.github.io/psychicpaper/)\n• Displays \"Grant Permission\" alert that lists all private entitlements when installing an app that uses them.\n\nNOTE: No permission alert will appear for apps without private entitlements.", - "downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/altstore/1_3_3_b1.ipa", + "version": "1.3.4b3", + "versionDate": "2020-05-20T10:00:00-07:00", + "versionDescription": "NEW\n• Adds \"Back Up\" option when long pressing app to manually back up app data, which can then be restored later.\n• GM version of 1.3.4\n\nPREVIOUS UPDATE:\n\n**Requires AltServer 1.3.1 beta. Download from https://altstore.io/altserver/beta/**\n\nIMPORTANT\niOS 13.5 (due for release soon) changes how active apps are counted. Previously, app extensions counted towards the 3 active app limit, and inactive apps could remain installed without counting. Now, app extensions are excluded, but inactive apps must be uninstalled to not count.\n\nOn devices running iOS 13.5 or later, apps will now be backed up & uninstalled when deactivated. When you later activate an inactive app, AltStore will reinstall the app and restore its data so you can continue using it as if it was never uninstalled.\n\nNOTE: This does not affect devices running iOS 13.4.1 or earlier.", + "downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/altstore/1_3_4_b3.ipa", "localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore adds support for 3rd party sources, allowing you to download apps from other developers directly through AltStore.", "iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png", "tintColor": "018084", - "size": 2531143, + "size": 2665126, "beta": true, "screenshotURLs": [ "https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",