diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index 59e452c8..d2832847 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -47,6 +47,9 @@ final class InstallAppOperation: ResultOperation return self.finish(.failure(OperationError.invalidParameters("InstallAppOperation.main: self.context.certificate or self.context.resignedApp or self.context.provisioningProfiles is nil"))) } + @Managed var appVersion = self.context.appVersion + let storeBuildVersion = $appVersion.buildVersion + let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() backgroundContext.perform { @@ -61,10 +64,14 @@ final class InstallAppOperation: ResultOperation } else { - installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, certificateSerialNumber: certificate.serialNumber, context: backgroundContext) + installedApp = InstalledApp(resignedApp: resignedApp, + originalBundleIdentifier: self.context.bundleIdentifier, + certificateSerialNumber: certificate.serialNumber, + storeBuildVersion: storeBuildVersion, + context: backgroundContext) } - installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber) + installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber, storeBuildVersion: storeBuildVersion) installedApp.needsResign = false if let team = DatabaseManager.shared.activeTeam(in: backgroundContext) diff --git a/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 12.xcdatamodel/contents b/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 12.xcdatamodel/contents index 37c2632f..c80ebb34 100644 --- a/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 12.xcdatamodel/contents +++ b/AltStoreCore/Model/AltStore.xcdatamodeld/AltStore 12.xcdatamodel/contents @@ -76,6 +76,7 @@ + diff --git a/AltStoreCore/Model/DatabaseManager.swift b/AltStoreCore/Model/DatabaseManager.swift index 26c18c14..6ec9a7f2 100644 --- a/AltStoreCore/Model/DatabaseManager.swift +++ b/AltStoreCore/Model/DatabaseManager.swift @@ -229,7 +229,7 @@ private extension DatabaseManager } else { - storeApp = StoreApp.makeAltStoreApp(version: localApp.version, buildVersion: localApp.buildVersion, in: context) + storeApp = StoreApp.makeAltStoreApp(version: localApp.version, buildVersion: nil, in: context) storeApp.source = altStoreSource } @@ -242,7 +242,10 @@ private extension DatabaseManager } else { - installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context) + //TODO: Support build versions. + // For backwards compatibility reasons, we cannot use localApp's buildVersion as storeBuildVersion, + // or else the latest update will _always_ be considered new because we don't use buildVersions in our source (yet). + installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, storeBuildVersion: nil, context: context) installedApp.storeApp = storeApp } @@ -322,7 +325,7 @@ private extension DatabaseManager let cachedExpirationDate = installedApp.expirationDate // Must go after comparing versions to see if we need to update our cached AltStore app bundle. - installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber) + installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber, storeBuildVersion: nil) if installedApp.refreshedDate < cachedRefreshedDate { diff --git a/AltStoreCore/Model/InstalledApp.swift b/AltStoreCore/Model/InstalledApp.swift index 9ac627d2..871b4c14 100644 --- a/AltStoreCore/Model/InstalledApp.swift +++ b/AltStoreCore/Model/InstalledApp.swift @@ -60,6 +60,7 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol @NSManaged public var hasAlternateIcon: Bool @NSManaged public var certificateSerialNumber: String? + @NSManaged public var storeBuildVersion: String? /* Transient */ @NSManaged public var isRefreshing: Bool @@ -104,7 +105,7 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol super.init(entity: entity, insertInto: context) } - public init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext) + public init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, storeBuildVersion: String?, context: NSManagedObjectContext) { super.init(entity: InstalledApp.entity(), insertInto: context) @@ -117,24 +118,30 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile. - self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber) + // In practice this update() is redundant because we always call update() again after init from callers, + // but better to have an init that is guaranteed to successfully initialize an object + // than one that has a hidden assumption a second method will be called. + self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber, storeBuildVersion: storeBuildVersion) } } public extension InstalledApp { var localizedVersion: String { - let localizedVersion = "\(self.version) (\(self.buildVersion))" + guard let storeBuildVersion else { return self.version } + + let localizedVersion = "\(self.version) (\(storeBuildVersion))" return localizedVersion } - func update(resignedApp: ALTApplication, certificateSerialNumber: String?) + func update(resignedApp: ALTApplication, certificateSerialNumber: String?, storeBuildVersion: String?) { self.name = resignedApp.name self.resignedBundleIdentifier = resignedApp.bundleIdentifier self.version = resignedApp.version self.buildVersion = resignedApp.buildVersion + self.storeBuildVersion = storeBuildVersion self.certificateSerialNumber = certificateSerialNumber @@ -178,14 +185,7 @@ public extension InstalledApp func matches(_ appVersion: AppVersion) -> Bool { - // Versions MUST match exactly. - guard self.version == appVersion.version else { return false } - - // If buildVersion is nil, return true because versions match. - guard let buildVersion = appVersion.buildVersion else { return true } - - // If buildVersion != nil, compare buildVersions and return result. - let matchesAppVersion = (self.buildVersion == buildVersion) + let matchesAppVersion = (self.version == appVersion.version && self.storeBuildVersion == appVersion.buildVersion) return matchesAppVersion } } @@ -207,14 +207,18 @@ public extension InstalledApp "AND", - // (latestSupportedVersion.version != installedApp.version || (latestSupportedVersion.buildVersion != nil && latestSupportedVersion.buildVersion != installedApp.buildVersion)) - "(%K != %K OR (%K != '' AND %K != %K))", + // latestSupportedVersion.version != installedApp.version || latestSupportedVersion.buildVersion != installedApp.storeBuildVersion + // + // We have to also check !(latestSupportedVersion.buildVersion == '' && installedApp.storeBuildVersion == nil) + // because latestSupportedVersion.buildVersion stores an empty string for nil, while installedApp.storeBuildVersion uses NULL. + "(%K != %K OR (%K != %K AND NOT (%K == '' AND %K == nil)))", ].joined(separator: " ") fetchRequest.predicate = NSPredicate(format: predicateFormat, #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion), #keyPath(InstalledApp.storeApp.latestSupportedVersion.version), #keyPath(InstalledApp.version), - #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.buildVersion)) + #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion), + #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion)) return fetchRequest }