Files
SideStore/AltStore/Model/InstalledApp.swift
Riley Testut 2d87c396f1 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.
2020-05-17 23:47:26 -07:00

276 lines
9.7 KiB
Swift

//
// InstalledApp.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
let ALTActiveAppsLimit = 3
protocol InstalledAppProtocol: Fetchable
{
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 }
}
@objc(InstalledApp)
class InstalledApp: NSManagedObject, InstalledAppProtocol
{
/* 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
@NSManaged var installedDate: Date
@NSManaged var isActive: Bool
@NSManaged var certificateSerialNumber: String?
/* Relationships */
@NSManaged var storeApp: StoreApp?
@NSManaged var team: Team?
@NSManaged var appExtensions: Set<InstalledExtension>
var isSideloaded: Bool {
return self.storeApp == nil
}
var appIDCount: Int {
return 1 + self.appExtensions.count
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext)
{
super.init(entity: InstalledApp.entity(), insertInto: context)
self.bundleIdentifier = originalBundleIdentifier
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.
self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
}
func update(resignedApp: ALTApplication, certificateSerialNumber: String?)
{
self.name = resignedApp.name
self.resignedBundleIdentifier = resignedApp.bundleIdentifier
self.version = resignedApp.version
self.certificateSerialNumber = certificateSerialNumber
if let provisioningProfile = resignedApp.provisioningProfile
{
self.update(provisioningProfile: provisioningProfile)
}
}
func update(provisioningProfile: ALTProvisioningProfile)
{
self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
}
}
extension InstalledApp
{
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp>
{
return NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
}
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
{
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version))
return fetchRequest
}
class func activeAppsFetchRequest() -> NSFetchRequest<InstalledApp>
{
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
return fetchRequest
}
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp?
{
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
let altStore = InstalledApp.first(satisfying: predicate, in: context)
return altStore
}
class func fetchActiveApps(in context: NSManagedObjectContext) -> [InstalledApp]
{
let activeApps = InstalledApp.fetch(InstalledApp.activeAppsFetchRequest(), in: context)
return activeApps
}
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
{
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
// No additional predicate
}
else
{
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
}
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.
installedApps.append(altStoreApp)
}
return installedApps
}
class func fetchAppsForBackgroundRefresh(in context: NSManagedObjectContext) -> [InstalledApp]
{
// Date 6 hours before now.
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
#keyPath(InstalledApp.isActive),
#keyPath(InstalledApp.refreshedDate), date as NSDate,
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
// No additional predicate
}
else
{
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate,
NSPredicate(format: "%K == nil OR %K == NO", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta))])
}
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
{
// Refresh AltStore last since it may cause app to quit.
installedApps.append(altStoreApp)
}
return installedApps
}
}
extension InstalledApp
{
var openAppURL: URL {
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
}
}
extension InstalledApp
{
class var appsDirectoryURL: URL {
let appsDirectoryURL = FileManager.default.applicationSupportDirectory.appendingPathComponent("Apps")
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
catch { print(error) }
return appsDirectoryURL
}
class func fileURL(for app: AppProtocol) -> URL
{
let appURL = self.directoryURL(for: app).appendingPathComponent("App.app")
return appURL
}
class func refreshedIPAURL(for app: AppProtocol) -> URL
{
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
return ipaURL
}
class func directoryURL(for app: AppProtocol) -> URL
{
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.bundleIdentifier)
do { try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) }
catch { print(error) }
return directoryURL
}
class func installedAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
{
let installedAppUTI = "io.altstore.Installed." + bundleIdentifier
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)
}
var fileURL: URL {
return InstalledApp.fileURL(for: self)
}
var refreshedIPAURL: URL {
return InstalledApp.refreshedIPAURL(for: self)
}
var installedAppUTI: String {
return InstalledApp.installedAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
}
var installedBackupAppUTI: String {
return InstalledApp.installedBackupAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
}
}