From d65cef88171b32d9624c3fb1c2fe1e4a3ef781e7 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 17 Jun 2019 14:49:23 -0700 Subject: [PATCH] [AltStore] Update apps from UpdatesViewController --- AltStore.xcodeproj/project.pbxproj | 6 +- AltStore/Apps/AppManager.swift | 25 ++++ AltStore/Apps/AppsViewController.swift | 28 ++--- AltStore/Base.lproj/Main.storyboard | 9 +- .../AltStore.xcdatamodel/contents | 8 +- AltStore/Model/InstalledApp.swift | 4 +- AltStore/My Apps/MyAppsViewController.swift | 2 +- .../Operations/DownloadAppOperation.swift | 13 +- AltStore/Operations/FetchAppsOperation.swift | 53 ++++++++ AltStore/Updates/UpdatesViewController.swift | 113 +++++++++++++++++- 10 files changed, 231 insertions(+), 30 deletions(-) create mode 100644 AltStore/Operations/FetchAppsOperation.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 6189beab..3ae1eb57 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = ""; }; @@ -738,8 +740,8 @@ BFD247962284D7C100981D42 /* Resources */ = { isa = PBXGroup; children = ( - BFD247762284B9A700981D42 /* Assets.xcassets */, BFB1169C22932DB100BB457C /* Apps.json */, + BFD247762284B9A700981D42 /* Assets.xcassets */, ); path = Resources; sourceTree = ""; @@ -810,6 +812,7 @@ BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */, BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */, BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */, + BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */, ); path = Operations; sourceTree = ""; @@ -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 */, diff --git a/AltStore/Apps/AppManager.swift b/AltStore/Apps/AppManager.swift index cd2d4cd8..857b8f47 100644 --- a/AltStore/Apps/AppManager.swift +++ b/AltStore/Apps/AppManager.swift @@ -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) -> Void) -> Progress diff --git a/AltStore/Apps/AppsViewController.swift b/AltStore/Apps/AppsViewController.swift index 12816124..59bd5700 100644 --- a/AltStore/Apps/AppsViewController.swift +++ b/AltStore/Apps/AppsViewController.swift @@ -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) + } } } } diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index eb67f297..d07ebf61 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -471,8 +471,15 @@ + + + + + + + @@ -636,7 +643,7 @@ - + diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents index 5c281e2d..64a4aa4f 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents @@ -34,9 +34,13 @@ - + + + + + @@ -53,7 +57,7 @@ - + \ No newline at end of file diff --git a/AltStore/Model/InstalledApp.swift b/AltStore/Model/InstalledApp.swift index 5ef25314..b1b27e74 100644 --- a/AltStore/Model/InstalledApp.swift +++ b/AltStore/Model/InstalledApp.swift @@ -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 diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 16f58941..eb2fb079 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -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 = """ diff --git a/AltStore/Operations/DownloadAppOperation.swift b/AltStore/Operations/DownloadAppOperation.swift index 6f17f60f..b0cf8eb2 100644 --- a/AltStore/Operations/DownloadAppOperation.swift +++ b/AltStore/Operations/DownloadAppOperation.swift @@ -45,11 +45,22 @@ class DownloadAppOperation: ResultOperation 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)) } } diff --git a/AltStore/Operations/FetchAppsOperation.swift b/AltStore/Operations/FetchAppsOperation.swift new file mode 100644 index 00000000..70d21b84 --- /dev/null +++ b/AltStore/Operations/FetchAppsOperation.swift @@ -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() + } +} diff --git a/AltStore/Updates/UpdatesViewController.swift b/AltStore/Updates/UpdatesViewController.swift index 11c7e82e..6a8d2011 100644 --- a/AltStore/Updates/UpdatesViewController.swift +++ b/AltStore/Updates/UpdatesViewController.swift @@ -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) + { + } }