diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 98309fc4..a6966542 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 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 */; }; + BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF258CE222EBAE2800023032 /* AppProtocol.swift */; }; BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; }; BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; }; BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; }; @@ -259,6 +260,7 @@ BF1E314922A060F400370A3C /* NSError+ALTServerError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSError+ALTServerError.m"; sourceTree = ""; }; BF1E315022A0616100370A3C /* libAltKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAltKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; BF219A7E22CAC431007676A6 /* AltStore.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltStore.entitlements; sourceTree = ""; }; + BF258CE222EBAE2800023032 /* AppProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = ""; }; BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = ""; }; BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = ""; }; BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = ""; }; @@ -877,6 +879,7 @@ isa = PBXGroup; children = ( BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */, + BF258CE222EBAE2800023032 /* AppProtocol.swift */, ); path = Protocols; sourceTree = ""; @@ -1290,6 +1293,7 @@ BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, + BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */, BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */, BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */, BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */, diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index bb3e6773..4b50c50b 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -147,7 +147,7 @@ extension AppDelegate self.runningApplications = [] - let identifiers = installedApps.compactMap { $0.app?.identifier } + let identifiers = installedApps.compactMap { $0.bundleIdentifier } print("Apps to refresh:", identifiers) DispatchQueue.global().async { @@ -222,11 +222,13 @@ extension AppDelegate for update in updates { - guard !previousUpdates.contains(where: { $0.app.identifier == update.app.identifier }) else { continue } + guard !previousUpdates.contains(where: { $0.bundleIdentifier == update.bundleIdentifier }) else { continue } + + guard let storeApp = update.storeApp else { continue } let content = UNMutableNotificationContent() content.title = NSLocalizedString("New Update Available", comment: "") - content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.app.name, update.app.version) + content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) @@ -272,12 +274,12 @@ extension AppDelegate dispatchGroup.leave() - let filteredApps = installedApps.filter { !(self.runningApplications?.contains($0.app.identifier) ?? false) } - print("Filtered Apps to Refresh:", filteredApps.map { $0.app.identifier }) + let filteredApps = installedApps.filter { !(self.runningApplications?.contains($0.bundleIdentifier) ?? false) } + print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier }) let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil) group.beginInstallationHandler = { (installedApp) in - guard installedApp.app.identifier == App.altstoreAppID else { return } + guard installedApp.bundleIdentifier == App.altstoreAppID else { return } // We're starting to install AltStore, which means the app is about to quit. // So, we schedule a "refresh successful" local notification to be displayed after a delay, @@ -293,7 +295,7 @@ extension AppDelegate else { var results = group.results - results[installedApp.app.identifier] = .success(installedApp) + results[installedApp.bundleIdentifier] = .success(installedApp) self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: refreshIdentifier, isLaunching: isLaunching) } diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index 74393fbe..7a53944d 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -55,9 +55,8 @@ private extension BrowseViewController func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource { let fetchRequest = App.fetchRequest() as NSFetchRequest - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)] fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \App.name, ascending: false)] - fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.identifier), App.altstoreAppID) + fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.bundleIdentifier), App.altstoreAppID) fetchRequest.returnsObjectsAsFaults = false let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) @@ -152,7 +151,7 @@ private extension BrowseViewController let toastView = ToastView(text: error.localizedDescription, detailText: nil) toastView.show(in: self.navigationController?.view ?? self.view, duration: 2) - case .success: print("Installed app:", app.identifier) + case .success: print("Installed app:", app.bundleIdentifier) } self.collectionView.reloadItems(at: [indexPath]) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 4e4977ed..493c53e9 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -47,12 +47,12 @@ extension AppManager let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext() let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)] + fetchRequest.returnsObjectsAsFaults = false do { let installedApps = try context.fetch(fetchRequest) - for app in installedApps + for app in installedApps where app.storeApp != nil { if UIApplication.shared.canOpenURL(app.openAppURL) { @@ -106,20 +106,20 @@ extension AppManager extension AppManager { - func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result) -> Void) -> Progress + func install(_ app: AppProtocol, presentingViewController: UIViewController, completionHandler: @escaping (Result) -> Void) -> Progress { if let progress = self.installationProgress(for: app) { return progress } - let appIdentifier = app.identifier + let bundleIdentifier = app.bundleIdentifier let group = self.install([app], forceDownload: true, presentingViewController: presentingViewController) group.completionHandler = { (result) in do { - self.installationProgress[appIdentifier] = nil + self.installationProgress[bundleIdentifier] = nil guard let (_, result) = try result.get().first else { throw OperationError.unknown } completionHandler(result) @@ -130,42 +130,42 @@ extension AppManager } } - self.installationProgress[app.identifier] = group.progress + self.installationProgress[bundleIdentifier] = group.progress return group.progress } func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup { - let apps = installedApps.compactMap { $0.app }.filter { self.refreshProgress(for: $0) == nil } + let apps = installedApps.filter { self.refreshProgress(for: $0) == nil } let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group) for app in apps { guard let progress = group.progress(for: app) else { continue } - self.refreshProgress[app.identifier] = progress + self.refreshProgress[app.bundleIdentifier] = progress } return group } - func installationProgress(for app: App) -> Progress? + func installationProgress(for app: AppProtocol) -> Progress? { - let progress = self.installationProgress[app.identifier] + let progress = self.installationProgress[app.bundleIdentifier] return progress } - func refreshProgress(for app: App) -> Progress? + func refreshProgress(for app: AppProtocol) -> Progress? { - let progress = self.refreshProgress[app.identifier] + let progress = self.refreshProgress[app.bundleIdentifier] return progress } } private extension AppManager { - func install(_ apps: [App], forceDownload: Bool, presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup + func install(_ apps: [AppProtocol], forceDownload: Bool, presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup { // Authenticate -> Download (if necessary) -> Resign -> Send -> Install. let group = group ?? OperationGroup() @@ -197,15 +197,15 @@ private extension AppManager for app in apps { - let context = AppOperationContext(appIdentifier: app.identifier, group: group) + let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, group: group) let progress = Progress.discreteProgress(totalUnitCount: 100) /* Resign */ let resignAppOperation = ResignAppOperation(context: context) resignAppOperation.resultHandler = { (result) in - guard let fileURL = self.process(result, context: context) else { return } - context.resignedFileURL = fileURL + guard let resignedApp = self.process(result, context: context) else { return } + context.resignedApp = resignedApp } resignAppOperation.addDependency(authenticationOperation) progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) @@ -214,18 +214,27 @@ private extension AppManager /* Download */ let fileURL = InstalledApp.fileURL(for: app) - if let installedApp = app.installedApp, FileManager.default.fileExists(atPath: fileURL.path), !forceDownload + + var localApp: ALTApplication? + + let managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() + managedObjectContext.performAndWait { + let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), context.bundleIdentifier) + + if let installedApp = InstalledApp.first(satisfying: predicate, in: managedObjectContext), FileManager.default.fileExists(atPath: fileURL.path), !forceDownload + { + localApp = ALTApplication(fileURL: installedApp.fileURL) + } + } + + if let localApp = localApp { // Already installed, don't need to download. // If we don't need to download the app, reduce the total unit count by 40. progress.totalUnitCount -= 40 - let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() - backgroundContext.performAndWait { - let installedApp = backgroundContext.object(with: installedApp.objectID) as! InstalledApp - context.installedApp = installedApp - } + context.app = localApp } else { @@ -233,15 +242,14 @@ private extension AppManager let downloadOperation = DownloadAppOperation(app: app) downloadOperation.resultHandler = { (result) in - guard let installedApp = self.process(result, context: context) else { return } - context.installedApp = installedApp + guard let app = self.process(result, context: context) else { return } + context.app = app } progress.addChild(downloadOperation.progress, withPendingUnitCount: 40) resignAppOperation.addDependency(downloadOperation) operations.append(downloadOperation) } - /* Send */ let sendAppOperation = SendAppOperation(context: context) sendAppOperation.resultHandler = { (result) in @@ -261,6 +269,16 @@ private extension AppManager context.error = error } + if let installedApp = result.value + { + if let app = app as? App, let storeApp = installedApp.managedObjectContext?.object(with: app.objectID) as? App + { + installedApp.storeApp = storeApp + } + + context.installedApp = installedApp + } + self.finishAppOperation(context) // Finish operation no matter what. } progress.addChild(installOperation.progress, withPendingUnitCount: 30) @@ -302,13 +320,15 @@ private extension AppManager guard !context.isFinished else { return } context.isFinished = true + self.refreshProgress[context.bundleIdentifier] = nil + if let error = context.error { - context.group.results[context.appIdentifier] = .failure(error) + context.group.results[context.bundleIdentifier] = .failure(error) } else if let installedApp = context.installedApp { - context.group.results[context.appIdentifier] = .success(installedApp) + context.group.results[context.bundleIdentifier] = .success(installedApp) // Save after each installation. installedApp.managedObjectContext?.performAndWait { @@ -317,9 +337,10 @@ private extension AppManager } } - self.refreshProgress[context.appIdentifier] = nil + do { try FileManager.default.removeItem(at: context.temporaryDirectory) } + catch { print("Failed to remove temporary directory.", error) } - print("Finished operation!", context.appIdentifier) + print("Finished operation!", context.bundleIdentifier) if context.group.results.count == context.group.progress.totalUnitCount { diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents index ed9210cf..f0784211 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents @@ -14,10 +14,10 @@ + - @@ -26,11 +26,11 @@ - + - + @@ -42,9 +42,11 @@ + + - + @@ -66,8 +68,8 @@ - - + + \ No newline at end of file diff --git a/AltStore/Model/App.swift b/AltStore/Model/App.swift index d506cff4..f424b642 100644 --- a/AltStore/Model/App.swift +++ b/AltStore/Model/App.swift @@ -10,6 +10,7 @@ import Foundation import CoreData import Roxas +import AltSign extension App { @@ -21,7 +22,7 @@ class App: NSManagedObject, Decodable, Fetchable { /* Properties */ @NSManaged private(set) var name: String - @NSManaged private(set) var identifier: String + @NSManaged private(set) var bundleIdentifier: String @NSManaged private(set) var subtitle: String? @NSManaged private(set) var developerName: String @@ -38,7 +39,7 @@ class App: NSManagedObject, Decodable, Fetchable @NSManaged private(set) var tintColor: UIColor? /* Relationships */ - @NSManaged private(set) var installedApp: InstalledApp? + @NSManaged var installedApp: InstalledApp? @objc(permissions) @NSManaged var _permissions: NSOrderedSet @nonobjc var permissions: [AppPermission] { @@ -53,7 +54,7 @@ class App: NSManagedObject, Decodable, Fetchable private enum CodingKeys: String, CodingKey { case name - case identifier + case bundleIdentifier case developerName case localizedDescription case version @@ -75,7 +76,7 @@ class App: NSManagedObject, Decodable, Fetchable let container = try decoder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self, forKey: .name) - self.identifier = try container.decode(String.self, forKey: .identifier) + self.bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier) self.developerName = try container.decode(String.self, forKey: .developerName) self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription) @@ -119,7 +120,7 @@ extension App { let app = App(context: context) app.name = "AltStore" - app.identifier = "com.rileytestut.AltStore" + app.bundleIdentifier = App.altstoreAppID app.developerName = "Riley Testut" app.localizedDescription = "AltStore is an alternative App Store." app.iconName = "" diff --git a/AltStore/Model/DatabaseManager.swift b/AltStore/Model/DatabaseManager.swift index 58082454..464b8c7a 100644 --- a/AltStore/Model/DatabaseManager.swift +++ b/AltStore/Model/DatabaseManager.swift @@ -108,34 +108,47 @@ private extension DatabaseManager func prepareDatabase(completionHandler: @escaping (Result) -> Void) { self.persistentContainer.performBackgroundTask { (context) in - let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0" + guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return } - let altStoreApp: App + let storeApp: App - if let app = App.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(App.identifier), App.altstoreAppID), in: context) + if let app = App.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(App.bundleIdentifier), App.altstoreAppID), in: context) { - altStoreApp = app + storeApp = app } else { - altStoreApp = App.makeAltStoreApp(in: context) - altStoreApp.version = version + storeApp = App.makeAltStoreApp(in: context) + storeApp.version = localApp.version } let installedApp: InstalledApp - if let app = altStoreApp.installedApp + if let app = storeApp.installedApp { installedApp = app } else { - installedApp = InstalledApp(app: altStoreApp, bundleIdentifier: altStoreApp.identifier, context: context) + installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: App.altstoreAppID, context: context) + installedApp.storeApp = storeApp } - installedApp.version = version + let fileURL = installedApp.fileURL - if let provisioningProfileURL = Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision"), let provisioningProfile = ALTProvisioningProfile(url: provisioningProfileURL) + if !FileManager.default.fileExists(atPath: fileURL.path) + { + do + { + try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: fileURL) + } + catch + { + print("Failed to copy AltStore app bundle to its proper location.", error) + } + } + + if let provisioningProfile = localApp.provisioningProfile { installedApp.refreshedDate = provisioningProfile.creationDate installedApp.expirationDate = provisioningProfile.expirationDate diff --git a/AltStore/Model/InstalledApp.swift b/AltStore/Model/InstalledApp.swift index 4d46a9da..4810e231 100644 --- a/AltStore/Model/InstalledApp.swift +++ b/AltStore/Model/InstalledApp.swift @@ -9,36 +9,48 @@ import Foundation import CoreData +import AltSign + @objc(InstalledApp) class InstalledApp: NSManagedObject, Fetchable { /* Properties */ + @NSManaged var name: String @NSManaged var bundleIdentifier: String + @NSManaged var resignedBundleIdentifier: String @NSManaged var version: String @NSManaged var refreshedDate: Date @NSManaged var expirationDate: Date /* Relationships */ - @NSManaged private(set) var app: App! + @NSManaged var storeApp: App? private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { super.init(entity: entity, insertInto: context) } - init(app: App, bundleIdentifier: String, context: NSManagedObjectContext) + init(resignedApp: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext) { super.init(entity: InstalledApp.entity(), insertInto: context) - let app = context.object(with: app.objectID) as! App - self.app = app - self.version = app.version + self.name = resignedApp.name + self.bundleIdentifier = originalBundleIdentifier + self.resignedBundleIdentifier = resignedApp.bundleIdentifier - self.bundleIdentifier = bundleIdentifier - - self.refreshedDate = Date() - self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile. + self.version = resignedApp.version + + if let provisioningProfile = resignedApp.provisioningProfile + { + self.refreshedDate = provisioningProfile.creationDate + self.expirationDate = provisioningProfile.expirationDate + } + else + { + self.refreshedDate = Date() + self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile. + } } } @@ -52,13 +64,13 @@ extension InstalledApp class func updatesFetchRequest() -> NSFetchRequest { let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest - fetchRequest.predicate = NSPredicate(format: "%K != %K", #keyPath(InstalledApp.version), #keyPath(InstalledApp.app.version)) + fetchRequest.predicate = NSPredicate(format: "%K != nil AND %K != %K", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version)) return fetchRequest } class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp? { - let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.app.identifier), App.altstoreAppID) + let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), App.altstoreAppID) let altStore = InstalledApp.first(satisfying: predicate, in: context) return altStore @@ -66,7 +78,7 @@ extension InstalledApp class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp] { - let predicate = NSPredicate(format: "%K != %@", #keyPath(InstalledApp.app.identifier), App.altstoreAppID) + let predicate = NSPredicate(format: "%K != %@", #keyPath(InstalledApp.bundleIdentifier), App.altstoreAppID) var installedApps = InstalledApp.all(satisfying: predicate, sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)], @@ -87,7 +99,7 @@ extension InstalledApp let predicate = NSPredicate(format: "(%K < %@) AND (%K != %@)", #keyPath(InstalledApp.refreshedDate), date as NSDate, - #keyPath(InstalledApp.app.identifier), App.altstoreAppID) + #keyPath(InstalledApp.bundleIdentifier), App.altstoreAppID) var installedApps = InstalledApp.all(satisfying: predicate, sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)], @@ -106,8 +118,13 @@ extension InstalledApp extension InstalledApp { var openAppURL: URL { - // Don't use the actual bundle ID yet since we're hardcoding support for the first apps in AltStore. - let openAppURL = URL(string: "altstore-" + self.app.identifier + "://")! + let openAppURL = URL(string: "altstore-" + self.bundleIdentifier + "://")! + return openAppURL + } + + class func openAppURL(for app: AppProtocol) -> URL + { + let openAppURL = URL(string: "altstore-" + app.bundleIdentifier + "://")! return openAppURL } } @@ -123,21 +140,21 @@ extension InstalledApp return appsDirectoryURL } - class func fileURL(for app: App) -> URL + class func fileURL(for app: AppProtocol) -> URL { let appURL = self.directoryURL(for: app).appendingPathComponent("App.app") return appURL } - class func refreshedIPAURL(for app: App) -> URL + class func refreshedIPAURL(for app: AppProtocol) -> URL { let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa") return ipaURL } - class func directoryURL(for app: App) -> URL + class func directoryURL(for app: AppProtocol) -> URL { - let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier) + let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.bundleIdentifier) do { try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) } catch { print(error) } @@ -146,14 +163,14 @@ extension InstalledApp } var directoryURL: URL { - return InstalledApp.directoryURL(for: self.app) + return InstalledApp.directoryURL(for: self) } var fileURL: URL { - return InstalledApp.fileURL(for: self.app) + return InstalledApp.fileURL(for: self) } var refreshedIPAURL: URL { - return InstalledApp.refreshedIPAURL(for: self.app) + return InstalledApp.refreshedIPAURL(for: self) } } diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 4a38b1b6..62f599da 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -94,7 +94,7 @@ class MyAppsViewController: UICollectionViewController let installedApp = self.dataSource.item(at: indexPath) let appViewController = segue.destination as! AppViewController - appViewController.app = installedApp.app + appViewController.app = installedApp.storeApp } } @@ -125,15 +125,15 @@ private extension MyAppsViewController func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource { let fetchRequest = InstalledApp.updatesFetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.app?.versionDate, ascending: true), - NSSortDescriptor(keyPath: \InstalledApp.app?.name, ascending: true)] + fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.versionDate, ascending: true), + NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)] fetchRequest.returnsObjectsAsFaults = false let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) dataSource.liveFetchLimit = maximumCollapsedUpdatesCount dataSource.cellIdentifierHandler = { _ in "UpdateCell" } dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in - guard let app = installedApp.app else { return } + guard let app = installedApp.storeApp else { return } let cell = cell as! UpdateCollectionViewCell cell.tintColor = app.tintColor ?? .altGreen @@ -144,7 +144,7 @@ private extension MyAppsViewController cell.updateButton.isIndicatingActivity = false cell.updateButton.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered) - if self.expandedAppUpdates.contains(app.identifier) + if self.expandedAppUpdates.contains(app.bundleIdentifier) { cell.mode = .expanded } @@ -178,16 +178,16 @@ private extension MyAppsViewController func makeInstalledAppsDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource { let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)] + fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.storeApp)] fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true), NSSortDescriptor(keyPath: \InstalledApp.refreshedDate, ascending: false), - NSSortDescriptor(keyPath: \InstalledApp.app?.name, ascending: true)] + NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)] fetchRequest.returnsObjectsAsFaults = false let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) dataSource.cellIdentifierHandler = { _ in "AppCell" } dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in - guard let app = installedApp.app else { return } + guard let app = installedApp.storeApp else { return } let tintColor = app.tintColor ?? .altGreen @@ -311,7 +311,7 @@ private extension MyAppsViewController } } - if installedApps.contains(where: { $0.app.identifier == App.altstoreAppID }) + if installedApps.contains(where: { $0.bundleIdentifier == App.altstoreAppID }) { let alertController = UIAlertController(title: NSLocalizedString("Refresh AltStore?", comment: ""), message: NSLocalizedString("AltStore will quit when it is finished refreshing.", comment: ""), preferredStyle: .alert) alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in @@ -376,18 +376,18 @@ private extension MyAppsViewController let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell - if self.expandedAppUpdates.contains(installedApp.app.identifier) + if self.expandedAppUpdates.contains(installedApp.bundleIdentifier) { - self.expandedAppUpdates.remove(installedApp.app.identifier) + self.expandedAppUpdates.remove(installedApp.bundleIdentifier) cell?.mode = .collapsed } else { - self.expandedAppUpdates.insert(installedApp.app.identifier) + self.expandedAppUpdates.insert(installedApp.bundleIdentifier) cell?.mode = .expanded } - self.cachedUpdateSizes[installedApp.app.identifier] = nil + self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil self.collectionView.performBatchUpdates({ self.collectionView.collectionViewLayout.invalidateLayout() @@ -401,7 +401,7 @@ private extension MyAppsViewController let installedApp = self.dataSource.item(at: indexPath) - let previousProgress = AppManager.shared.refreshProgress(for: installedApp.app) + let previousProgress = AppManager.shared.refreshProgress(for: installedApp) guard previousProgress == nil else { previousProgress?.cancel() return @@ -432,15 +432,15 @@ private extension MyAppsViewController let point = self.collectionView.convert(sender.center, from: sender.superview) guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return } - let app = self.dataSource.item(at: indexPath).app! + guard let storeApp = self.dataSource.item(at: indexPath).storeApp else { return } - let previousProgress = AppManager.shared.installationProgress(for: app) + let previousProgress = AppManager.shared.installationProgress(for: storeApp) guard previousProgress == nil else { previousProgress?.cancel() return } - _ = AppManager.shared.install(app, presentingViewController: self) { (result) in + _ = AppManager.shared.install(storeApp, presentingViewController: self) { (result) in DispatchQueue.main.async { switch result { @@ -454,7 +454,7 @@ private extension MyAppsViewController self.collectionView.reloadItems(at: [indexPath]) case .success: - print("Updated app:", app.identifier) + print("Updated app:", storeApp.bundleIdentifier) // No need to reload, since the the update cell is gone now. } @@ -548,7 +548,7 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout case .updates: let item = self.dataSource.item(at: indexPath) - if let previousHeight = self.cachedUpdateSizes[item.app!.identifier] + if let previousHeight = self.cachedUpdateSizes[item.bundleIdentifier] { return previousHeight } @@ -560,7 +560,7 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout self.dataSource.cellConfigurationHandler(self.prototypeUpdateCell, item, indexPath) let size = self.prototypeUpdateCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) - self.cachedUpdateSizes[item.app!.identifier] = size + self.cachedUpdateSizes[item.bundleIdentifier] = size return size case .installedApps: diff --git a/AltStore/Operations/AppOperationContext.swift b/AltStore/Operations/AppOperationContext.swift index 6738a4b2..a673a296 100644 --- a/AltStore/Operations/AppOperationContext.swift +++ b/AltStore/Operations/AppOperationContext.swift @@ -10,11 +10,27 @@ import Foundation import CoreData import Network +import AltSign + class AppOperationContext { - var appIdentifier: String - var group: OperationGroup + lazy var temporaryDirectory: URL = { + let temporaryDirectory = FileManager.default.uniqueTemporaryURL() + do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) } + catch { self.error = error } + + return temporaryDirectory + }() + + var bundleIdentifier: String + var group: OperationGroup + + var app: ALTApplication? + var resignedApp: ALTApplication? + + var connection: NWConnection? + var installedApp: InstalledApp? { didSet { self.installedAppContext = self.installedApp?.managedObjectContext @@ -22,9 +38,6 @@ class AppOperationContext } private var installedAppContext: NSManagedObjectContext? - var resignedFileURL: URL? - var connection: NWConnection? - var isFinished = false var error: Error? { @@ -37,9 +50,9 @@ class AppOperationContext } private var _error: Error? - init(appIdentifier: String, group: OperationGroup) + init(bundleIdentifier: String, group: OperationGroup) { - self.appIdentifier = appIdentifier + self.bundleIdentifier = bundleIdentifier self.group = group } } diff --git a/AltStore/Operations/DownloadAppOperation.swift b/AltStore/Operations/DownloadAppOperation.swift index f7f0d9a5..60e68b4a 100644 --- a/AltStore/Operations/DownloadAppOperation.swift +++ b/AltStore/Operations/DownloadAppOperation.swift @@ -12,24 +12,21 @@ import Roxas import AltSign @objc(DownloadAppOperation) -class DownloadAppOperation: ResultOperation +class DownloadAppOperation: ResultOperation { - let app: App + let app: AppProtocol - var useCachedAppIfAvailable = false - lazy var context = DatabaseManager.shared.persistentContainer.newBackgroundContext() - - private let appIdentifier: String + private let bundleIdentifier: String private let sourceURL: URL private let destinationURL: URL private let session = URLSession(configuration: .default) - init(app: App) + init(app: AppProtocol) { self.app = app - self.appIdentifier = app.identifier - self.sourceURL = app.downloadURL + self.bundleIdentifier = app.bundleIdentifier + self.sourceURL = app.url self.destinationURL = InstalledApp.fileURL(for: app) super.init() @@ -41,7 +38,7 @@ class DownloadAppOperation: ResultOperation { super.main() - print("Downloading App:", self.appIdentifier) + print("Downloading App:", self.bundleIdentifier) func finishOperation(_ result: Result) { @@ -77,24 +74,8 @@ class DownloadAppOperation: ResultOperation try FileManager.default.copyItem(at: appBundleURL, to: self.destinationURL, shouldReplace: true) - self.context.perform { - let app = self.context.object(with: self.app.objectID) as! App - - let installedApp: InstalledApp - - if let app = app.installedApp - { - installedApp = app - - } - else - { - installedApp = InstalledApp(app: app, bundleIdentifier: app.identifier, context: self.context) - } - - installedApp.version = app.version - self.finish(.success(installedApp)) - } + guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp } + self.finish(.success(copiedApplication)) } catch { diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index bc5a946b..f772adca 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -10,10 +10,11 @@ import Foundation import Network import AltKit +import AltSign import Roxas @objc(InstallAppOperation) -class InstallAppOperation: ResultOperation +class InstallAppOperation: ResultOperation { let context: AppOperationContext @@ -37,35 +38,53 @@ class InstallAppOperation: ResultOperation } guard - let installedApp = self.context.installedApp, + let resignedApp = self.context.resignedApp, let connection = self.context.connection, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) } - installedApp.managedObjectContext?.perform { - print("Installing app:", installedApp.app.identifier) - self.context.group.beginInstallationHandler?(installedApp) - } - - let request = BeginInstallationRequest() - server.send(request, via: connection) { (result) in - switch result + let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() + backgroundContext.perform { + let installedApp: InstalledApp + + // Fetch + update rather than insert + resolve merge conflicts to prevent potential context-level conflicts. + if let app = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), self.context.bundleIdentifier), in: backgroundContext) { - case .failure(let error): self.finish(.failure(error)) - case .success: - - self.receive(from: connection, server: server) { (result) in - switch result - { - case .success: - installedApp.managedObjectContext?.performAndWait { - installedApp.refreshedDate = Date() - } - - case .failure: break - } + installedApp = app + } + else + { + installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext) + } + + if let profile = resignedApp.provisioningProfile + { + installedApp.refreshedDate = profile.creationDate + installedApp.expirationDate = profile.expirationDate + } + + self.context.group.beginInstallationHandler?(installedApp) + + let request = BeginInstallationRequest() + server.send(request, via: connection) { (result) in + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success: - self.finish(result) + self.receive(from: connection, server: server) { (result) in + switch result + { + case .success: + backgroundContext.perform { + installedApp.refreshedDate = Date() + self.finish(.success(installedApp)) + } + + case .failure(let error): + self.finish(.failure(error)) + } + } } } } @@ -81,12 +100,12 @@ class InstallAppOperation: ResultOperation if let error = response.error { - self.finish(.failure(error)) + completionHandler(.failure(error)) } else if response.progress == 1.0 { self.progress.completedUnitCount = self.progress.totalUnitCount - self.finish(.success(())) + completionHandler(.success(())) } else { diff --git a/AltStore/Operations/OperationGroup.swift b/AltStore/Operations/OperationGroup.swift index 5f3f04aa..83b765f3 100644 --- a/AltStore/Operations/OperationGroup.swift +++ b/AltStore/Operations/OperationGroup.swift @@ -25,7 +25,7 @@ class OperationGroup var results = [String: Result]() - private var progressByApp = [App: Progress]() + private var progressByBundleIdentifier = [String: Progress]() private let operationQueue = OperationQueue() private let installOperationQueue = OperationQueue() @@ -63,17 +63,17 @@ class OperationGroup } } - func set(_ progress: Progress, for app: App) + func set(_ progress: Progress, for app: AppProtocol) { - self.progressByApp[app] = progress + self.progressByBundleIdentifier[app.bundleIdentifier] = progress self.progress.totalUnitCount += 1 self.progress.addChild(progress, withPendingUnitCount: 1) } - func progress(for app: App) -> Progress? + func progress(for app: AppProtocol) -> Progress? { - let progress = self.progressByApp[app] + let progress = self.progressByBundleIdentifier[app.bundleIdentifier] return progress } } diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index 64322063..0cfbb5f1 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -12,12 +12,10 @@ import Roxas import AltSign @objc(ResignAppOperation) -class ResignAppOperation: ResultOperation +class ResignAppOperation: ResultOperation { let context: AppOperationContext - private let temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL() - init(context: AppOperationContext) { self.context = context @@ -31,16 +29,6 @@ class ResignAppOperation: ResultOperation { super.main() - do - { - try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil) - } - catch - { - self.finish(.failure(error)) - return - } - if let error = self.context.error { self.finish(.failure(error)) @@ -48,68 +36,49 @@ class ResignAppOperation: ResultOperation } guard - let installedApp = self.context.installedApp, - let appContext = installedApp.managedObjectContext, + let app = self.context.app, let signer = self.context.group.signer else { return self.finish(.failure(OperationError.invalidParameters)) } - appContext.perform { - let appIdentifier = installedApp.app.identifier + // Register Device + self.registerCurrentDevice(for: signer.team) { (result) in + guard let _ = self.process(result) else { return } - // Register Device - self.registerCurrentDevice(for: signer.team) { (result) in - guard let _ = self.process(result) else { return } + // Prepare Provisioning Profiles + self.prepareProvisioningProfiles(app.fileURL, team: signer.team) { (result) in + guard let profiles = self.process(result) else { return } - // Prepare Provisioning Profiles - appContext.perform { - self.prepareProvisioningProfiles(installedApp.fileURL, team: signer.team) { (result) in - guard let profiles = self.process(result) else { return } + // Prepare app bundle + let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2) + self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3) + + let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in + guard let appBundleURL = self.process(result) else { return } + + print("Resigning App:", self.context.bundleIdentifier) + + // Resign app bundle + let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in + guard let resignedURL = self.process(result) else { return } - // Prepare app bundle - appContext.perform { - let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2) - self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3) + // Finish + do + { + let destinationURL = InstalledApp.refreshedIPAURL(for: app) + try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true) - let prepareAppBundleProgress = self.prepareAppBundle(for: installedApp, profiles: profiles) { (result) in - guard let appBundleURL = self.process(result) else { return } - - print("Resigning App:", appIdentifier) - - // Resign app bundle - let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in - guard let resignedURL = self.process(result) else { return } - - // Finish - appContext.perform { - do - { - installedApp.refreshedDate = Date() - - if let profile = profiles[installedApp.app.identifier] - { - installedApp.expirationDate = profile.expirationDate - } - else - { - installedApp.expirationDate = installedApp.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) - } - - try FileManager.default.copyItem(at: resignedURL, to: installedApp.refreshedIPAURL, shouldReplace: true) - - self.finish(.success(installedApp.refreshedIPAURL)) - } - catch - { - self.finish(.failure(error)) - } - } - } - prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1) - } - prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1) + // Use appBundleURL since we need an app bundle, not .ipa. + guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp } + self.finish(.success(resignedApplication)) + } + catch + { + self.finish(.failure(error)) } } + prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1) } + prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1) } } } @@ -131,17 +100,6 @@ class ResignAppOperation: ResultOperation return value } } - - override func finish(_ result: Result) - { - super.finish(result) - - if FileManager.default.fileExists(atPath: self.temporaryDirectory.path, isDirectory: nil) - { - do { try FileManager.default.removeItem(at: self.temporaryDirectory) } - catch { print("Failed to remove app bundle.", error) } - } - } } private extension ResignAppOperation @@ -386,15 +344,14 @@ private extension ResignAppOperation } } - func prepareAppBundle(for installedApp: InstalledApp, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result) -> Void) -> Progress + func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result) -> Void) -> Progress { let progress = Progress.discreteProgress(totalUnitCount: 1) - let bundleIdentifier = installedApp.bundleIdentifier - let openURL = installedApp.openAppURL - let appIdentifier = installedApp.app.identifier + let bundleIdentifier = app.bundleIdentifier + let openURL = InstalledApp.openAppURL(for: app) - let fileURL = installedApp.fileURL + let fileURL = app.fileURL func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws { @@ -420,7 +377,7 @@ private extension ResignAppOperation DispatchQueue.global().async { do { - let appBundleURL = self.temporaryDirectory.appendingPathComponent("App.app") + let appBundleURL = self.context.temporaryDirectory.appendingPathComponent("App.app") try FileManager.default.copyItem(at: fileURL, to: appBundleURL) // Become current so we can observe progress from unzipAppBundle(). @@ -438,7 +395,7 @@ private extension ResignAppOperation var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes] - if appIdentifier == App.altstoreAppID + if self.context.bundleIdentifier == App.altstoreAppID { guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID } additionalValues[Bundle.Info.deviceID] = udid diff --git a/AltStore/Operations/SendAppOperation.swift b/AltStore/Operations/SendAppOperation.swift index ad96ece1..558c33ee 100644 --- a/AltStore/Operations/SendAppOperation.swift +++ b/AltStore/Operations/SendAppOperation.swift @@ -39,7 +39,10 @@ class SendAppOperation: ResultOperation return } - guard let fileURL = self.context.resignedFileURL, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) } + guard let app = self.context.app, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) } + + // self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa. + let fileURL = InstalledApp.refreshedIPAURL(for: app) // Connect to server. self.connect(to: server) { (result) in diff --git a/AltStore/Protocols/AppProtocol.swift b/AltStore/Protocols/AppProtocol.swift new file mode 100644 index 00000000..fa331fe8 --- /dev/null +++ b/AltStore/Protocols/AppProtocol.swift @@ -0,0 +1,38 @@ +// +// AppProtocol.swift +// AltStore +// +// Created by Riley Testut on 7/26/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import AltSign + +protocol AppProtocol +{ + var name: String { get } + var bundleIdentifier: String { get } + var url: URL { get } +} + +extension ALTApplication: AppProtocol +{ + var url: URL { + return self.fileURL + } +} + +extension App: AppProtocol +{ + var url: URL { + return self.downloadURL + } +} + +extension InstalledApp: AppProtocol +{ + var url: URL { + return self.fileURL + } +} diff --git a/AltStore/Resources/Apps-Dev.json b/AltStore/Resources/Apps-Dev.json index 8158fe95..209eb92f 100644 --- a/AltStore/Resources/Apps-Dev.json +++ b/AltStore/Resources/Apps-Dev.json @@ -1,7 +1,7 @@ [ { "name": "AltStore", - "identifier": "com.rileytestut.AltStore", + "bundleIdentifier": "com.rileytestut.AltStore", "developerName": "Riley Testut", "version": "0.8", "versionDate": "2019-07-16", @@ -22,7 +22,7 @@ }, { "name": "Delta", - "identifier": "com.rileytestut.Delta", + "bundleIdentifier": "com.rileytestut.Delta", "developerName": "Riley Testut", "subtitle": "Classic games in your pocket.", "version": "0.8", @@ -47,7 +47,7 @@ }, { "name": "Clipboard Manager", - "identifier": "com.rileytestut.ClipboardManager", + "bundleIdentifier": "com.rileytestut.ClipboardManager", "subtitle": "Manage your clipboard history with ease.", "developerName": "Riley Testut", "version": "0.8", diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 2c0303e6..7d538aad 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 2c0303e685c4f9ba36fdcb5668707df7d14fe387 +Subproject commit 7d538aad4a7493e9a987309a4449d09cd1d54b59