mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[AltStore] Revises database model to support both store apps and sideloaded apps
This commit is contained in:
@@ -14,10 +14,10 @@
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="App" representedClassName="App" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="developerName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="iconName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/>
|
||||
@@ -26,11 +26,11 @@
|
||||
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="app" inverseEntity="InstalledApp" syncable="YES"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
@@ -42,9 +42,11 @@
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="installedApp" inverseEntity="App" syncable="YES"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="installedApp" inverseEntity="App" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
@@ -66,8 +68,8 @@
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="App" positionX="-63" positionY="-18" width="128" height="255"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -108,34 +108,47 @@ private extension DatabaseManager
|
||||
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> 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
|
||||
|
||||
@@ -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<InstalledApp>
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user