mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
[AltStore] Revises database model to support both store apps and sideloaded apps
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
BF258CE222EBAE2800023032 /* AppProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
|
||||
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
|
||||
BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; };
|
||||
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; };
|
||||
@@ -877,6 +879,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */,
|
||||
BF258CE222EBAE2800023032 /* AppProtocol.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -55,9 +55,8 @@ private extension BrowseViewController
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage>
|
||||
{
|
||||
let fetchRequest = App.fetchRequest() as NSFetchRequest<App>
|
||||
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<App, UIImage>(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])
|
||||
|
||||
@@ -47,12 +47,12 @@ extension AppManager
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext()
|
||||
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
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<InstalledApp, Error>) -> Void) -> Progress
|
||||
func install(_ app: AppProtocol, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> 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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<InstalledApp, UIImage>
|
||||
{
|
||||
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<InstalledApp, UIImage>(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<InstalledApp, UIImage>
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
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<InstalledApp, UIImage>(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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,21 @@ import Roxas
|
||||
import AltSign
|
||||
|
||||
@objc(DownloadAppOperation)
|
||||
class DownloadAppOperation: ResultOperation<InstalledApp>
|
||||
class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
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<InstalledApp>
|
||||
{
|
||||
super.main()
|
||||
|
||||
print("Downloading App:", self.appIdentifier)
|
||||
print("Downloading App:", self.bundleIdentifier)
|
||||
|
||||
func finishOperation(_ result: Result<URL, Error>)
|
||||
{
|
||||
@@ -77,24 +74,8 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -10,10 +10,11 @@ import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@objc(InstallAppOperation)
|
||||
class InstallAppOperation: ResultOperation<Void>
|
||||
class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
@@ -37,35 +38,53 @@ class InstallAppOperation: ResultOperation<Void>
|
||||
}
|
||||
|
||||
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<Void>
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ class OperationGroup
|
||||
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,10 @@ import Roxas
|
||||
import AltSign
|
||||
|
||||
@objc(ResignAppOperation)
|
||||
class ResignAppOperation: ResultOperation<URL>
|
||||
class ResignAppOperation: ResultOperation<ALTApplication>
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
private let temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
init(context: AppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
@@ -31,16 +29,6 @@ class ResignAppOperation: ResultOperation<URL>
|
||||
{
|
||||
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<URL>
|
||||
}
|
||||
|
||||
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<URL>
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<URL, Error>)
|
||||
{
|
||||
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<URL, Error>) -> Void) -> Progress
|
||||
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> 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
|
||||
|
||||
@@ -39,7 +39,10 @@ class SendAppOperation: ResultOperation<NWConnection>
|
||||
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
|
||||
|
||||
38
AltStore/Protocols/AppProtocol.swift
Normal file
38
AltStore/Protocols/AppProtocol.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
2
Dependencies/AltSign
vendored
2
Dependencies/AltSign
vendored
Submodule Dependencies/AltSign updated: 2c0303e685...7d538aad4a
Reference in New Issue
Block a user