[AltStore] Update apps from UpdatesViewController

This commit is contained in:
Riley Testut
2019-06-17 14:49:23 -07:00
parent 9538d05f9f
commit d65cef8817
10 changed files with 231 additions and 30 deletions

View File

@@ -90,6 +90,7 @@
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; };
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */; };
BF9B63C6229DD44E002F0A62 /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; };
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
@@ -310,6 +311,7 @@
BF4588962298DE6E00BD7491 /* libzip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libzip.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF4713A422976CFC00784A2F /* openssl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppsOperation.swift; sourceTree = "<group>"; };
BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
@@ -738,8 +740,8 @@
BFD247962284D7C100981D42 /* Resources */ = {
isa = PBXGroup;
children = (
BFD247762284B9A700981D42 /* Assets.xcassets */,
BFB1169C22932DB100BB457C /* Apps.json */,
BFD247762284B9A700981D42 /* Assets.xcassets */,
);
path = Resources;
sourceTree = "<group>";
@@ -810,6 +812,7 @@
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */,
BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
@@ -1170,6 +1173,7 @@
files = (
BFDB6A0F22AB2776007EA6D6 /* InstallAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */,
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */,

View File

@@ -14,6 +14,11 @@ import AltKit
import Roxas
extension AppManager
{
static let didFetchAppsNotification = Notification.Name("com.altstore.AppManager.didFetchApps")
}
class AppManager
{
static let shared = AppManager()
@@ -75,6 +80,26 @@ extension AppManager
}
}
extension AppManager
{
func fetchApps(completionHandler: @escaping (Result<[App], Error>) -> Void)
{
let fetchAppsOperation = FetchAppsOperation()
fetchAppsOperation.resultHandler = { (result) in
switch result
{
case .failure(let error):
completionHandler(.failure(error))
case .success(let apps):
completionHandler(.success(apps))
NotificationCenter.default.post(name: AppManager.didFetchAppsNotification, object: self)
}
}
self.operationQueue.addOperation(fetchAppsOperation)
}
}
extension AppManager
{
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress

View File

@@ -11,13 +11,7 @@ import Roxas
class AppsViewController: UITableViewController
{
private lazy var dataSource = self.makeDataSource()
private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter
}()
private lazy var dataSource = self.makeDataSource()
override func viewDidLoad()
{
@@ -82,23 +76,19 @@ private extension AppsViewController
func fetchApps()
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let appsFileURL = Bundle.main.url(forResource: "Apps", withExtension: "json")!
AppManager.shared.fetchApps { (result) in
do
{
let data = try Data(contentsOf: appsFileURL)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
decoder.managedObjectContext = context
_ = try decoder.decode([App].self, from: data)
try context.save()
let apps = try result.get()
try apps.first?.managedObjectContext?.save()
}
catch
{
fatalError("Failed to save fetched apps. \(error)")
DispatchQueue.main.async {
let toastView = RSTToastView(text: NSLocalizedString("Failed to fetch apps", comment: ""), detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
}
}
}
}

View File

@@ -471,8 +471,15 @@
</connections>
</tableView>
<navigationItem key="navigationItem" title="Updates" id="hvX-Ly-Y2C"/>
<connections>
<outlet property="progressView" destination="QaM-dt-nHj" id="Tlq-vj-Bo9"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="H7V-ct-fO1" userLabel="First Responder" sceneMemberID="firstResponder"/>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" id="QaM-dt-nHj">
<rect key="frame" x="0.0" y="0.0" width="150" height="2.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</progressView>
</objects>
<point key="canvasLocation" x="1518" y="1124"/>
</scene>
@@ -636,7 +643,7 @@
<image name="second" width="30" height="30"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="8jj-zE-2hk"/>
<segue reference="wBX-0o-Ywz"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Purple"/>
</document>

View File

@@ -34,9 +34,13 @@
<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="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" 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"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
@@ -53,7 +57,7 @@
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="App" positionX="-63" positionY="-18" width="128" height="210"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="105"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
</elements>
</model>

View File

@@ -18,8 +18,6 @@ class InstalledApp: NSManagedObject, Fetchable
@NSManaged var expirationDate: Date
@NSManaged var isBeta: Bool
/* Relationships */
@NSManaged private(set) var app: App!
@@ -34,7 +32,7 @@ class InstalledApp: NSManagedObject, Fetchable
let app = context.object(with: app.objectID) as! App
self.app = app
self.version = "0.9"
self.version = app.version
self.bundleIdentifier = bundleIdentifier
self.expirationDate = expirationDate

View File

@@ -79,7 +79,7 @@ private extension MyAppsViewController
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let app = installedApp.app else { return }
cell.textLabel?.text = app.name
cell.textLabel?.text = app.name + " (\(installedApp.version))"
let detailText =
"""

View File

@@ -45,11 +45,22 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let app = context.object(with: self.app.objectID) as! App
let installedApp = InstalledApp(app: app,
let installedApp: InstalledApp
if let app = app.installedApp
{
installedApp = app
}
else
{
installedApp = InstalledApp(app: app,
bundleIdentifier: app.identifier,
expirationDate: Date(),
context: context)
}
installedApp.version = app.version
self.finish(.success(installedApp))
}
}

View File

@@ -0,0 +1,53 @@
//
// FetchAppsOperation.swift
// AltStore
//
// Created by Riley Testut on 6/17/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
@objc(FetchAppsOperation)
class FetchAppsOperation: ResultOperation<[App]>
{
private let session = URLSession(configuration: .default)
private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter
}()
override func main()
{
super.main()
let appsURL = URL(string: "https://www.dropbox.com/s/z5tj1tx8zgeqbms/Apps.json?dl=1")!
let dataTask = self.session.dataTask(with: appsURL) { (data, response, error) in
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
do
{
let (data, _) = try Result((data, response), error).get()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
decoder.managedObjectContext = context
let apps = try decoder.decode([App].self, from: data)
self.finish(.success(apps))
}
catch
{
self.finish(.failure(error))
}
}
}
self.progress.addChild(dataTask.progress, withPendingUnitCount: 1)
dataTask.resume()
}
}

View File

@@ -21,19 +21,36 @@ class UpdatesViewController: UITableViewController
return dateFormatter
}()
@IBOutlet private var progressView: UIProgressView!
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(UpdatesViewController.didFetchApps(_:)), name: AppManager.didFetchAppsNotification, object: nil)
}
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
if let navigationBar = self.navigationController?.navigationBar
{
self.progressView.translatesAutoresizingMaskIntoConstraints = false
navigationBar.addSubview(self.progressView)
NSLayoutConstraint.activate([self.progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
self.progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
}
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
let count = self.tableView.numberOfRows(inSection: 0)
self.navigationController?.tabBarItem.badgeValue = count > 0 ? String(describing: count) : nil
self.update()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
@@ -71,6 +88,98 @@ private extension UpdatesViewController
cell.detailTextLabel?.text = detailText
}
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.text = NSLocalizedString("No Updates", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("There are no app updates at this time.", comment: "")
dataSource.placeholderView = placeholderView
return dataSource
}
func update()
{
if let count = self.dataSource.fetchedResultsController.fetchedObjects?.count, count > 0
{
self.navigationController?.tabBarItem.badgeValue = String(describing: count)
}
else
{
self.navigationController?.tabBarItem.badgeValue = nil
}
}
}
private extension UpdatesViewController
{
func update(_ installedApp: InstalledApp)
{
let toastView = RSTToastView(text: "Updating...", detailText: nil)
toastView.tintColor = .altPurple
toastView.activityIndicatorView.startAnimating()
toastView.show(in: self.navigationController?.view ?? self.view)
let progress = AppManager.shared.install(installedApp.app, presentingViewController: self) { (result) in
do
{
let app = try result.get()
try app.managedObjectContext?.save()
DispatchQueue.main.async {
let installedApp = DatabaseManager.shared.persistentContainer.viewContext.object(with: installedApp.objectID) as! InstalledApp
let toastView = RSTToastView(text: "Updated \(installedApp.app.name) to version \(installedApp.version)!", detailText: nil)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
self.update()
}
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Failed to update \(installedApp.app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
}
}
DispatchQueue.main.async {
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
}
}
self.progressView.observedProgress = progress
}
@objc func didFetchApps(_ notification: Notification)
{
DispatchQueue.main.async {
if self.dataSource.fetchedResultsController.fetchedObjects == nil
{
do { try self.dataSource.fetchedResultsController.performFetch() }
catch { print("Error fetching:", error) }
}
self.update()
}
}
}
extension UpdatesViewController
{
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?
{
let updateAction = UITableViewRowAction(style: .normal, title: "Update") { [weak self] (action, indexPath) in
guard let installedApp = self?.dataSource.item(at: indexPath) else { return }
self?.update(installedApp)
}
updateAction.backgroundColor = .altPurple
return [updateAction]
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
{
}
}