2019-05-20 21:26:01 +02:00
|
|
|
//
|
|
|
|
|
// InstalledApp.swift
|
|
|
|
|
// AltStore
|
|
|
|
|
//
|
|
|
|
|
// Created by Riley Testut on 5/20/19.
|
|
|
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import CoreData
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
import AltSign
|
2023-01-19 07:52:47 -08:00
|
|
|
import SemanticVersion
|
2019-07-28 15:08:13 -07:00
|
|
|
|
2023-02-06 17:36:05 -06:00
|
|
|
extension InstalledApp
|
|
|
|
|
{
|
|
|
|
|
public static var freeAccountActiveAppsLimit: Int {
|
2024-12-07 17:45:09 +05:30
|
|
|
if UserDefaults.standard.isAppLimitDisabled
|
2023-02-06 17:36:05 -06:00
|
|
|
{
|
|
|
|
|
// MacDirtyCow exploit allows users to remove 3-app limit, so return 10 to match App ID limit per-week.
|
|
|
|
|
// Don't return nil because that implies there is no limit, which isn't quite true due to App ID limit.
|
|
|
|
|
return 10
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
|
|
|
|
|
return 3
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-01 13:05:31 -07:00
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
public protocol InstalledAppProtocol: Fetchable
|
2020-01-21 16:53:34 -08:00
|
|
|
{
|
|
|
|
|
var name: String { get }
|
|
|
|
|
var bundleIdentifier: String { get }
|
|
|
|
|
var resignedBundleIdentifier: String { get }
|
|
|
|
|
var version: String { get }
|
|
|
|
|
|
|
|
|
|
var refreshedDate: Date { get }
|
|
|
|
|
var expirationDate: Date { get }
|
|
|
|
|
var installedDate: Date { get }
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-20 21:26:01 +02:00
|
|
|
@objc(InstalledApp)
|
2025-02-08 04:45:22 +05:30
|
|
|
public class InstalledApp: BaseEntity, InstalledAppProtocol
|
2019-05-20 21:26:01 +02:00
|
|
|
{
|
|
|
|
|
/* Properties */
|
2020-09-03 16:39:08 -07:00
|
|
|
@NSManaged public var name: String
|
|
|
|
|
@NSManaged public var bundleIdentifier: String
|
|
|
|
|
@NSManaged public var resignedBundleIdentifier: String
|
|
|
|
|
@NSManaged public var version: String
|
2023-05-18 14:51:26 -05:00
|
|
|
@NSManaged public var buildVersion: String
|
2019-05-20 21:26:01 +02:00
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
@NSManaged public var refreshedDate: Date
|
|
|
|
|
@NSManaged public var expirationDate: Date
|
|
|
|
|
@NSManaged public var installedDate: Date
|
2019-05-20 21:26:01 +02:00
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
@NSManaged public var isActive: Bool
|
2020-10-01 11:51:39 -07:00
|
|
|
@NSManaged public var needsResign: Bool
|
2020-10-01 14:09:45 -07:00
|
|
|
@NSManaged public var hasAlternateIcon: Bool
|
2020-03-11 14:43:19 -07:00
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
@NSManaged public var certificateSerialNumber: String?
|
2023-05-26 19:12:13 -05:00
|
|
|
@NSManaged public var storeBuildVersion: String?
|
2020-03-06 17:08:35 -08:00
|
|
|
|
2020-09-08 13:12:40 -07:00
|
|
|
/* Transient */
|
|
|
|
|
@NSManaged public var isRefreshing: Bool
|
|
|
|
|
|
2019-05-20 21:26:01 +02:00
|
|
|
/* Relationships */
|
2020-09-03 16:39:08 -07:00
|
|
|
@NSManaged public var storeApp: StoreApp?
|
|
|
|
|
@NSManaged public var team: Team?
|
|
|
|
|
@NSManaged public var appExtensions: Set<InstalledExtension>
|
2019-05-20 21:26:01 +02:00
|
|
|
|
2022-09-08 16:14:28 -05:00
|
|
|
@NSManaged public private(set) var loggedErrors: NSSet /* Set<LoggedError> */ // Use NSSet to avoid eagerly fetching values.
|
|
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
public var isSideloaded: Bool {
|
2019-07-28 15:51:36 -07:00
|
|
|
return self.storeApp == nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-08 04:45:22 +05:30
|
|
|
|
|
|
|
|
// TODO: integrate the following into the hasUpdate such that altstore sources also work with SideStore, ex: pledge check etc for updates
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// let predicateFormat = [
|
|
|
|
|
// // isActive && storeApp != nil && latestSupportedVersion != nil
|
|
|
|
|
// "%K == YES AND %K != nil AND %K != nil",
|
|
|
|
|
//
|
|
|
|
|
// "AND",
|
|
|
|
|
//
|
|
|
|
|
// // 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)))",
|
|
|
|
|
//
|
|
|
|
|
// "AND",
|
|
|
|
|
//
|
|
|
|
|
// // !isPledgeRequired || isPledged
|
|
|
|
|
// "(%K == NO OR %K == YES)"
|
|
|
|
|
// ].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.storeBuildVersion),
|
|
|
|
|
// #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeBuildVersion),
|
|
|
|
|
// #keyPath(InstalledApp.storeApp.isPledgeRequired), #keyPath(InstalledApp.storeApp.isPledged))
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-01-19 07:52:47 -08:00
|
|
|
@objc public var hasUpdate: Bool {
|
2025-02-08 04:45:22 +05:30
|
|
|
// Basic validation
|
|
|
|
|
guard isActive,
|
|
|
|
|
let storeApp = self.storeApp,
|
|
|
|
|
let latestVersion = storeApp.latestSupportedVersion else
|
|
|
|
|
{
|
2024-12-17 21:01:33 +05:30
|
|
|
return false
|
|
|
|
|
}
|
2025-02-09 17:28:24 +05:30
|
|
|
|
2025-02-08 04:45:22 +05:30
|
|
|
// Check pledge requirements
|
|
|
|
|
guard !storeApp.isPledgeRequired || storeApp.isPledged else
|
|
|
|
|
{
|
|
|
|
|
return false
|
2023-01-19 07:52:47 -08:00
|
|
|
}
|
|
|
|
|
|
2025-02-08 04:45:22 +05:30
|
|
|
// Get current semantic versions
|
|
|
|
|
let currentSemVer = SemanticVersion(self.version)
|
|
|
|
|
let latestSemVer = SemanticVersion(latestVersion.version)
|
2024-12-17 21:01:33 +05:30
|
|
|
|
2025-02-08 04:45:22 +05:30
|
|
|
// If semantic versions can't be parsed, fall back to string comparison
|
|
|
|
|
if currentSemVer == nil || latestSemVer == nil {
|
|
|
|
|
return !matches(latestVersion)
|
2024-12-17 21:01:33 +05:30
|
|
|
}
|
2025-02-09 17:28:24 +05:30
|
|
|
let currentVer = SemanticVersion("\(currentSemVer!.major).\(currentSemVer!.minor).\(currentSemVer!.patch)")
|
|
|
|
|
let latestVer = SemanticVersion("\(latestSemVer!.major).\(latestSemVer!.minor).\(latestSemVer!.patch)")
|
2024-12-17 21:01:33 +05:30
|
|
|
|
2025-02-09 17:28:24 +05:30
|
|
|
// Compare by major.minor.patch
|
|
|
|
|
if latestVer! > latestVer! {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2025-02-08 04:45:22 +05:30
|
|
|
|
2025-02-09 17:28:24 +05:30
|
|
|
// Check beta updates if enabled
|
|
|
|
|
if UserDefaults.standard.isBetaUpdatesEnabled,
|
|
|
|
|
ReleaseTracks.betaTracks.contains(latestVersion.channel),
|
|
|
|
|
latestVer == currentVer, // major.minor.patch are matching
|
|
|
|
|
// now compare by preRelease and build to break the tie
|
|
|
|
|
// TODO: since multiple tracks can be independent, when a different version is available on selected track than installed
|
|
|
|
|
// we accept it, now ex: if the setup is consistent for upstream merge lets say from alpha to nightly and alpha can never fall behind nightly,
|
|
|
|
|
// then the preRelease+build combo will always be incremental and our below not-equals check will still work.
|
|
|
|
|
(latestSemVer!.build != currentSemVer!.build) || (latestSemVer!.preRelease != currentSemVer!.preRelease)
|
|
|
|
|
{
|
|
|
|
|
return true
|
|
|
|
|
}
|
2025-02-08 04:45:22 +05:30
|
|
|
|
|
|
|
|
// else include everything as-is when doing lexicographic comparison
|
|
|
|
|
// NOTE: stable x.y.z is always > x.y.z-abcd+1234
|
|
|
|
|
return latestSemVer! > currentSemVer!
|
2023-01-19 07:52:47 -08:00
|
|
|
}
|
2025-02-08 04:45:22 +05:30
|
|
|
|
2023-01-19 07:52:47 -08:00
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
public var appIDCount: Int {
|
2020-03-20 16:32:31 -07:00
|
|
|
return 1 + self.appExtensions.count
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
public var requiredActiveSlots: Int {
|
2020-05-17 23:36:30 -07:00
|
|
|
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1
|
|
|
|
|
return requiredActiveSlots
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-20 21:26:01 +02:00
|
|
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
|
|
|
|
{
|
|
|
|
|
super.init(entity: entity, insertInto: context)
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-26 19:12:13 -05:00
|
|
|
public init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, storeBuildVersion: String?, context: NSManagedObjectContext)
|
2019-05-20 21:26:01 +02:00
|
|
|
{
|
|
|
|
|
super.init(entity: InstalledApp.entity(), insertInto: context)
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
self.bundleIdentifier = originalBundleIdentifier
|
2019-06-21 11:20:03 -07:00
|
|
|
|
2022-12-03 17:25:15 -05:00
|
|
|
print("InstalledApp `self.bundleIdentifier`: \(self.bundleIdentifier)")
|
|
|
|
|
|
2020-01-24 15:03:16 -08:00
|
|
|
self.refreshedDate = Date()
|
|
|
|
|
self.installedDate = Date()
|
|
|
|
|
|
|
|
|
|
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
|
|
|
|
|
|
2023-05-26 19:12:13 -05:00
|
|
|
// 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)
|
2020-01-24 15:03:16 -08:00
|
|
|
}
|
2023-05-18 14:51:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public extension InstalledApp
|
|
|
|
|
{
|
|
|
|
|
var localizedVersion: String {
|
2023-05-26 19:12:13 -05:00
|
|
|
guard let storeBuildVersion else { return self.version }
|
|
|
|
|
|
|
|
|
|
let localizedVersion = "\(self.version) (\(storeBuildVersion))"
|
2023-05-18 14:51:26 -05:00
|
|
|
return localizedVersion
|
|
|
|
|
}
|
2020-01-24 15:03:16 -08:00
|
|
|
|
2023-05-26 19:12:13 -05:00
|
|
|
func update(resignedApp: ALTApplication, certificateSerialNumber: String?, storeBuildVersion: String?)
|
2020-01-24 15:03:16 -08:00
|
|
|
{
|
|
|
|
|
self.name = resignedApp.name
|
|
|
|
|
|
|
|
|
|
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
|
2019-07-28 15:08:13 -07:00
|
|
|
self.version = resignedApp.version
|
2025-02-08 04:45:22 +05:30
|
|
|
|
|
|
|
|
self.buildVersion = resignedApp.buildVersion
|
2023-05-26 19:12:13 -05:00
|
|
|
self.storeBuildVersion = storeBuildVersion
|
2020-03-06 17:08:35 -08:00
|
|
|
|
|
|
|
|
self.certificateSerialNumber = certificateSerialNumber
|
2023-05-18 14:51:26 -05:00
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
if let provisioningProfile = resignedApp.provisioningProfile
|
|
|
|
|
{
|
2020-03-06 17:08:35 -08:00
|
|
|
self.update(provisioningProfile: provisioningProfile)
|
2019-07-28 15:08:13 -07:00
|
|
|
}
|
2019-05-20 21:26:01 +02:00
|
|
|
}
|
2020-03-06 17:08:35 -08:00
|
|
|
|
2023-05-18 14:51:26 -05:00
|
|
|
func update(provisioningProfile: ALTProvisioningProfile)
|
2020-03-06 17:08:35 -08:00
|
|
|
{
|
|
|
|
|
self.refreshedDate = provisioningProfile.creationDate
|
|
|
|
|
self.expirationDate = provisioningProfile.expirationDate
|
|
|
|
|
}
|
2020-10-01 14:09:45 -07:00
|
|
|
|
2023-05-18 14:51:26 -05:00
|
|
|
func loadIcon(completion: @escaping (Result<UIImage?, Error>) -> Void)
|
2020-10-01 14:09:45 -07:00
|
|
|
{
|
2024-12-07 17:45:09 +05:30
|
|
|
// TODO: @mahee96: Fix this later (reason: alternateIcon is not available for appEx)
|
|
|
|
|
// if self.bundleIdentifier == StoreApp.altstoreAppID,
|
|
|
|
|
// let iconName = UIApplication.alt_shared?.alternateIconName
|
|
|
|
|
// {
|
|
|
|
|
// // Use alternate app icon for AltStore, if one is chosen.
|
|
|
|
|
//
|
|
|
|
|
// let image = UIImage(named: iconName)
|
|
|
|
|
// completion(.success(image))
|
|
|
|
|
//
|
|
|
|
|
// return
|
|
|
|
|
// }
|
2024-02-15 14:19:34 -06:00
|
|
|
|
2020-10-01 14:09:45 -07:00
|
|
|
let hasAlternateIcon = self.hasAlternateIcon
|
|
|
|
|
let alternateIconURL = self.alternateIconURL
|
|
|
|
|
let fileURL = self.fileURL
|
|
|
|
|
|
|
|
|
|
DispatchQueue.global().async {
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
if hasAlternateIcon,
|
|
|
|
|
case let data = try Data(contentsOf: alternateIconURL),
|
|
|
|
|
let icon = UIImage(data: data)
|
|
|
|
|
{
|
|
|
|
|
return completion(.success(icon))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let application = ALTApplication(fileURL: fileURL)
|
|
|
|
|
completion(.success(application?.icon))
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
completion(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-09 17:28:24 +05:30
|
|
|
|
|
|
|
|
func matches(_ appVersion: AppVersion) -> Bool
|
2023-05-18 14:51:26 -05:00
|
|
|
{
|
2023-05-26 19:12:13 -05:00
|
|
|
let matchesAppVersion = (self.version == appVersion.version && self.storeBuildVersion == appVersion.buildVersion)
|
2023-05-18 14:51:26 -05:00
|
|
|
return matchesAppVersion
|
|
|
|
|
}
|
2019-05-20 21:26:01 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
public extension InstalledApp
|
2019-05-20 21:26:01 +02:00
|
|
|
{
|
|
|
|
|
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp>
|
|
|
|
|
{
|
|
|
|
|
return NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
|
|
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
|
2025-02-09 17:28:24 +05:30
|
|
|
class func supportedUpdatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
2022-11-16 14:11:11 -06:00
|
|
|
{
|
2023-05-18 14:51:26 -05:00
|
|
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
2022-11-16 14:11:11 -06:00
|
|
|
|
2025-02-08 04:45:22 +05:30
|
|
|
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.hasUpdate))
|
2025-02-09 17:28:24 +05:30
|
|
|
|
2020-03-11 14:43:19 -07:00
|
|
|
return fetchRequest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class func activeAppsFetchRequest() -> NSFetchRequest<InstalledApp>
|
|
|
|
|
{
|
|
|
|
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
|
|
|
|
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
|
2022-12-03 17:25:15 -05:00
|
|
|
print("Active Apps Fetch Request: \(String(describing: fetchRequest.predicate))")
|
2019-07-24 13:52:58 -07:00
|
|
|
return fetchRequest
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp?
|
|
|
|
|
{
|
2023-02-02 08:09:15 -08:00
|
|
|
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
2022-12-03 17:25:15 -05:00
|
|
|
print("Fetch 'AltStore' Predicate: \(String(describing: predicate))")
|
2019-06-21 11:20:03 -07:00
|
|
|
let altStore = InstalledApp.first(satisfying: predicate, in: context)
|
|
|
|
|
return altStore
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 14:43:19 -07:00
|
|
|
class func fetchActiveApps(in context: NSManagedObjectContext) -> [InstalledApp]
|
|
|
|
|
{
|
|
|
|
|
let activeApps = InstalledApp.fetch(InstalledApp.activeAppsFetchRequest(), in: context)
|
|
|
|
|
return activeApps
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
|
|
|
|
{
|
2023-11-29 18:24:33 -06:00
|
|
|
let predicate = NSPredicate(format: "(%K == YES AND %K != %@) AND (%K == nil OR %K == NO OR %K == YES)",
|
|
|
|
|
#keyPath(InstalledApp.isActive),
|
|
|
|
|
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID,
|
|
|
|
|
#keyPath(InstalledApp.storeApp),
|
|
|
|
|
#keyPath(InstalledApp.storeApp.isPledgeRequired),
|
|
|
|
|
#keyPath(InstalledApp.storeApp.isPledged))
|
2019-06-21 11:20:03 -07:00
|
|
|
|
|
|
|
|
var installedApps = InstalledApp.all(satisfying: predicate,
|
|
|
|
|
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
|
|
|
|
in: context)
|
|
|
|
|
|
|
|
|
|
if let altStoreApp = InstalledApp.fetchAltStore(in: context)
|
|
|
|
|
{
|
|
|
|
|
// Refresh AltStore last since it causes app to quit.
|
2023-11-29 18:24:33 -06:00
|
|
|
|
2023-11-30 15:14:01 -06:00
|
|
|
if let storeApp = altStoreApp.storeApp
|
2023-11-29 18:24:33 -06:00
|
|
|
{
|
2023-11-30 15:14:01 -06:00
|
|
|
if !storeApp.isPledgeRequired || storeApp.isPledged
|
|
|
|
|
{
|
|
|
|
|
// Only add AltStore if it's the public version OR if it's the beta and we're pledged to it.
|
|
|
|
|
installedApps.append(altStoreApp)
|
|
|
|
|
}
|
2023-11-29 18:24:33 -06:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// No associated storeApp, so add it just to be safe.
|
|
|
|
|
installedApps.append(altStoreApp)
|
|
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return installedApps
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class func fetchAppsForBackgroundRefresh(in context: NSManagedObjectContext) -> [InstalledApp]
|
|
|
|
|
{
|
2019-08-28 11:07:49 -07:00
|
|
|
// Date 6 hours before now.
|
|
|
|
|
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
2019-06-21 11:20:03 -07:00
|
|
|
|
2023-11-29 18:24:33 -06:00
|
|
|
let predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@) AND (%K == nil OR %K == NO OR %K == YES)",
|
2020-03-11 14:43:19 -07:00
|
|
|
#keyPath(InstalledApp.isActive),
|
2019-06-21 11:20:03 -07:00
|
|
|
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
2023-11-29 18:24:33 -06:00
|
|
|
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID,
|
|
|
|
|
#keyPath(InstalledApp.storeApp),
|
|
|
|
|
#keyPath(InstalledApp.storeApp.isPledgeRequired),
|
|
|
|
|
#keyPath(InstalledApp.storeApp.isPledged)
|
|
|
|
|
)
|
2019-08-28 11:13:22 -07:00
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
var installedApps = InstalledApp.all(satisfying: predicate,
|
|
|
|
|
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
|
|
|
|
|
in: context)
|
|
|
|
|
|
|
|
|
|
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date
|
|
|
|
|
{
|
2023-11-30 15:14:01 -06:00
|
|
|
if let storeApp = altStoreApp.storeApp
|
2023-11-29 18:24:33 -06:00
|
|
|
{
|
2023-11-30 15:14:01 -06:00
|
|
|
if !storeApp.isPledgeRequired || storeApp.isPledged
|
|
|
|
|
{
|
|
|
|
|
// Only add AltStore if it's the public version OR if it's the beta and we're pledged to it.
|
|
|
|
|
installedApps.append(altStoreApp)
|
|
|
|
|
}
|
2023-11-29 18:24:33 -06:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// No associated storeApp, so add it just to be safe.
|
|
|
|
|
installedApps.append(altStoreApp)
|
|
|
|
|
}
|
2019-06-21 11:20:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return installedApps
|
|
|
|
|
}
|
2019-05-20 21:26:01 +02:00
|
|
|
}
|
2019-06-04 13:53:21 -07:00
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
public extension InstalledApp
|
2019-06-04 13:53:21 -07:00
|
|
|
{
|
2024-12-14 05:51:11 +05:30
|
|
|
// TODO: @mahee96: Do NOT hardcode app's url scheme prefixes as in here
|
|
|
|
|
// Need to get it dynamically from the Info.plist of other means
|
2019-06-04 13:53:21 -07:00
|
|
|
var openAppURL: URL {
|
2024-12-14 05:51:11 +05:30
|
|
|
let openAppURL = URL(string: "sidestore-" + self.bundleIdentifier + "://")!
|
2019-07-28 15:08:13 -07:00
|
|
|
return openAppURL
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-14 05:51:11 +05:30
|
|
|
// TODO: @mahee96: Do NOT hardcode app's url scheme prefixes as in here
|
|
|
|
|
// Need to get it dynamically from the Info.plist of other means
|
2019-07-28 15:08:13 -07:00
|
|
|
class func openAppURL(for app: AppProtocol) -> URL
|
|
|
|
|
{
|
2024-12-14 05:51:11 +05:30
|
|
|
let openAppURL = URL(string: "sidestore-" + app.bundleIdentifier + "://")!
|
2019-06-04 13:53:21 -07:00
|
|
|
return openAppURL
|
|
|
|
|
}
|
2023-11-30 18:50:54 -06:00
|
|
|
|
2025-02-08 04:45:22 +05:30
|
|
|
// var isUpdateAvailable: Bool {
|
|
|
|
|
// guard let storeApp = self.storeApp, let latestVersion = storeApp.latestSupportedVersion else { return false }
|
|
|
|
|
// guard !storeApp.isPledgeRequired || storeApp.isPledged else { return false }
|
2023-11-30 18:50:54 -06:00
|
|
|
|
2025-02-08 04:45:22 +05:30
|
|
|
// let isUpdateAvailable = !self.matches(latestVersion)
|
|
|
|
|
// return isUpdateAvailable
|
|
|
|
|
// }
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-03 16:39:08 -07:00
|
|
|
public extension InstalledApp
|
2019-06-04 13:53:21 -07:00
|
|
|
{
|
|
|
|
|
class var appsDirectoryURL: URL {
|
2020-09-14 14:31:46 -07:00
|
|
|
let baseDirectory = FileManager.default.altstoreSharedDirectory ?? FileManager.default.applicationSupportDirectory
|
|
|
|
|
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
|
2019-06-04 13:53:21 -07:00
|
|
|
|
|
|
|
|
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
|
2022-12-03 17:25:15 -05:00
|
|
|
catch { print("Creating App Directory Error: \(error)") }
|
2019-06-04 13:53:21 -07:00
|
|
|
return appsDirectoryURL
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-16 12:09:12 -07:00
|
|
|
class var legacyAppsDirectoryURL: URL {
|
|
|
|
|
let baseDirectory = FileManager.default.applicationSupportDirectory
|
|
|
|
|
let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps")
|
|
|
|
|
return appsDirectoryURL
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
class func fileURL(for app: AppProtocol) -> URL
|
2019-06-04 13:53:21 -07:00
|
|
|
{
|
2019-06-21 11:20:03 -07:00
|
|
|
let appURL = self.directoryURL(for: app).appendingPathComponent("App.app")
|
|
|
|
|
return appURL
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
class func refreshedIPAURL(for app: AppProtocol) -> URL
|
2019-06-10 15:03:47 -07:00
|
|
|
{
|
|
|
|
|
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
|
2022-12-03 17:25:15 -05:00
|
|
|
print("`ipaURL`: \(ipaURL.absoluteString)")
|
2019-06-10 15:03:47 -07:00
|
|
|
return ipaURL
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-28 15:08:13 -07:00
|
|
|
class func directoryURL(for app: AppProtocol) -> URL
|
2019-06-04 13:53:21 -07:00
|
|
|
{
|
2019-07-28 15:08:13 -07:00
|
|
|
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.bundleIdentifier)
|
2019-06-04 13:53:21 -07:00
|
|
|
|
|
|
|
|
do { try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) }
|
|
|
|
|
catch { print(error) }
|
|
|
|
|
|
|
|
|
|
return directoryURL
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-17 19:17:45 -08:00
|
|
|
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
|
|
|
|
{
|
2024-12-13 14:15:55 +05:30
|
|
|
let installedAppUTI = "io.sidestore.Installed." + bundleIdentifier
|
2019-12-17 19:17:45 -08:00
|
|
|
return installedAppUTI
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-16 16:17:18 -07:00
|
|
|
class func installedBackupAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
|
|
|
|
{
|
|
|
|
|
let installedBackupAppUTI = InstalledApp.installedAppUTI(forBundleIdentifier: bundleIdentifier) + ".backup"
|
|
|
|
|
return installedBackupAppUTI
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-01 14:09:45 -07:00
|
|
|
class func alternateIconURL(for app: AppProtocol) -> URL
|
|
|
|
|
{
|
|
|
|
|
let installedBackupAppUTI = self.directoryURL(for: app).appendingPathComponent("AltIcon.png")
|
|
|
|
|
return installedBackupAppUTI
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-04 13:53:21 -07:00
|
|
|
var directoryURL: URL {
|
2019-07-28 15:08:13 -07:00
|
|
|
return InstalledApp.directoryURL(for: self)
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|
|
|
|
|
|
2019-06-21 11:20:03 -07:00
|
|
|
var fileURL: URL {
|
2019-07-28 15:08:13 -07:00
|
|
|
return InstalledApp.fileURL(for: self)
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|
2019-06-10 15:03:47 -07:00
|
|
|
|
|
|
|
|
var refreshedIPAURL: URL {
|
2019-07-28 15:08:13 -07:00
|
|
|
return InstalledApp.refreshedIPAURL(for: self)
|
2019-06-10 15:03:47 -07:00
|
|
|
}
|
2019-12-17 19:17:45 -08:00
|
|
|
|
|
|
|
|
var installedAppUTI: String {
|
|
|
|
|
return InstalledApp.installedAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
|
|
|
|
}
|
2020-05-16 16:17:18 -07:00
|
|
|
|
|
|
|
|
var installedBackupAppUTI: String {
|
|
|
|
|
return InstalledApp.installedBackupAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
|
|
|
|
}
|
2020-10-01 14:09:45 -07:00
|
|
|
|
|
|
|
|
var alternateIconURL: URL {
|
|
|
|
|
return InstalledApp.alternateIconURL(for: self)
|
|
|
|
|
}
|
2019-06-04 13:53:21 -07:00
|
|
|
}
|