Implements error handling when importing games + controller skins

This commit is contained in:
Riley Testut 2017-01-14 11:27:18 -08:00
parent 4d5dee6cea
commit 2bb4c4d278
7 changed files with 248 additions and 53 deletions

View File

@ -31,6 +31,7 @@
BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */; };
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */; };
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */; };
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */; };
BF1DAD5D1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift */; };
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; };
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; };
@ -133,6 +134,7 @@
BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllersSettingsViewController.swift; path = Controllers/ControllersSettingsViewController.swift; sourceTree = "<group>"; };
BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewGameViewController.swift; sourceTree = "<group>"; };
BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmulatorCore+Cheats.swift"; sourceTree = "<group>"; };
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Importing.swift"; sourceTree = "<group>"; };
BF1DAD5C1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameTypeControllerSkinsViewController.swift; path = "Controller Skins/GameTypeControllerSkinsViewController.swift"; sourceTree = "<group>"; };
BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = "<group>"; };
BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = "<group>"; };
@ -239,6 +241,7 @@
BF5942911E09BD1A0051894B /* NSManagedObject+Conveniences.swift */,
BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */,
BFC314761E0C8CFC0056E3A8 /* GameType+Delta.swift */,
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -717,6 +720,7 @@
BF5942731E09BC700051894B /* Model.xcdatamodel in Sources */,
BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */,
BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */,
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */,
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */,
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */,
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */,

View File

@ -97,14 +97,40 @@ extension AppDelegate
private func importGame(at url: URL) -> Bool
{
DatabaseManager.shared.importGames(at: [url], completion: nil)
DatabaseManager.shared.importGames(at: [url]) { (games, errors) in
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .games, with: errors)
self.present(alertController)
}
}
return true
}
private func importControllerSkin(at url: URL) -> Bool
{
DatabaseManager.shared.importControllerSkins(at: [url], completion: nil)
DatabaseManager.shared.importControllerSkins(at: [url]) { (games, errors) in
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors)
self.present(alertController)
}
}
return true
}
private func present(_ alertController: UIAlertController)
{
var rootViewController = self.window?.rootViewController
while rootViewController?.presentedViewController != nil
{
rootViewController = rootViewController?.presentedViewController
}
rootViewController?.present(alertController, animated: true, completion: nil)
}
}

View File

@ -15,8 +15,8 @@ import MobileCoreServices
protocol ImportControllerDelegate
{
func importController(_ importController: ImportController, didImport games: Set<Game>)
func importController(_ importController: ImportController, didImport controllerSkins: Set<ControllerSkin>)
func importController(_ importController: ImportController, didImport games: Set<Game>, with errors: Set<DatabaseManager.ImportError>)
func importController(_ importController: ImportController, didImport controllerSkins: Set<ControllerSkin>, with errors: Set<DatabaseManager.ImportError>)
/** Optional **/
func importControllerDidCancel(_ importController: ImportController)
@ -73,10 +73,10 @@ class ImportController: NSObject
DatabaseManager.shared.performBackgroundTask { (context) in
let controllerSkinURLs = contents.filter { $0.pathExtension.lowercased() == "deltaskin" }
self.importControllerSkins(at: controllerSkinURLs)
self.importControllerSkins(at: Set(controllerSkinURLs))
let gameURLs = contents.filter { GameType.gameType(forFileExtension: $0.pathExtension) != .unknown || $0.pathExtension.lowercased() == "zip" }
self.importGames(at: gameURLs)
self.importGames(at: Set(gameURLs))
}
}
@ -99,37 +99,17 @@ class ImportController: NSObject
self.presentingViewController?.present(alertController, animated: true, completion: completion)
}
fileprivate func importGames(at urls: [URL])
fileprivate func importGames(at urls: Set<URL>)
{
DatabaseManager.shared.importGames(at: urls) { identifiers in
DatabaseManager.shared.viewContext.perform() {
let predicate = NSPredicate(format: "%K IN (%@)", #keyPath(Game.identifier), identifiers)
let games = Game.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: Game.self)
self.delegate?.importController(self, didImport: Set(games))
self.presentingViewController?.importController = nil
}
DatabaseManager.shared.importGames(at: urls) { (games, errors) in
self.delegate?.importController(self, didImport: games, with: errors)
}
}
fileprivate func importControllerSkins(at urls: [URL])
fileprivate func importControllerSkins(at urls: Set<URL>)
{
DatabaseManager.shared.importControllerSkins(at: urls) { identifiers in
DatabaseManager.shared.viewContext.perform() {
let predicate = NSPredicate(format: "%K IN (%@)", #keyPath(ControllerSkin.identifier), identifiers)
let controllerSkins = ControllerSkin.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: ControllerSkin.self)
self.delegate?.importController(self, didImport: Set(controllerSkins))
self.presentingViewController?.importController = nil
}
DatabaseManager.shared.importControllerSkins(at: urls) { (controllerSkins, errors) in
self.delegate?.importController(self, didImport: controllerSkins, with: errors)
}
}
}

View File

@ -16,6 +16,43 @@ import ZipZap
// Pods
import FileMD5Hash
extension DatabaseManager
{
enum ImportError: Error, Hashable
{
case doesNotExist(URL)
case invalid(URL)
case unknown(URL, NSError)
case saveFailed(Set<URL>, NSError)
var hashValue: Int {
switch self
{
case .doesNotExist: return 0
case .invalid: return 1
case .unknown: return 2
case .saveFailed: return 3
}
}
static func ==(lhs: ImportError, rhs: ImportError) -> Bool
{
switch (lhs, rhs)
{
case (let .doesNotExist(url1), let .doesNotExist(url2)) where url1 == url2: return true
case (let .invalid(url1), let .invalid(url2)) where url1 == url2: return true
case (let .unknown(url1, error1), let .unknown(url2, error2)) where url1 == url2 && error1 == error2: return true
case (let .saveFailed(urls1, error1), let .saveFailed(urls2, error2)) where urls1 == urls2 && error1 == error2: return true
case (.doesNotExist, _): return false
case (.invalid, _): return false
case (.unknown, _): return false
case (.saveFailed, _): return false
}
}
}
}
final class DatabaseManager: NSPersistentContainer
{
static let shared = DatabaseManager()
@ -108,14 +145,19 @@ private extension DatabaseManager
/// Importing
extension DatabaseManager
{
func importGames(at urls: [URL], completion: ((Set<String>) -> Void)?)
func importGames(at urls: Set<URL>, completion: ((Set<Game>, Set<ImportError>) -> Void)?)
{
var errors = Set<ImportError>()
let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" }
if zipFileURLs.count > 0
{
self.extractCompressedGames(at: zipFileURLs) { (extractedURLs) in
self.extractCompressedGames(at: Set(zipFileURLs)) { (extractedURLs, extractErrors) in
let gameURLs = urls.filter { $0.pathExtension.lowercased() != "zip" } + extractedURLs
self.importGames(at: gameURLs, completion: completion)
self.importGames(at: Set(gameURLs)) { (importedGames, importErrors) in
let allErrors = importErrors.union(extractErrors)
completion?(importedGames, allErrors)
}
}
return
@ -127,7 +169,10 @@ extension DatabaseManager
for url in urls
{
guard FileManager.default.fileExists(atPath: url.path) else { continue }
guard FileManager.default.fileExists(atPath: url.path) else {
errors.insert(.doesNotExist(url))
continue
}
let identifier = FileHash.sha1HashOfFile(atPath: url.path) as String
@ -159,38 +204,56 @@ extension DatabaseManager
identifiers.insert(game.identifier)
}
catch
catch let error as NSError
{
print("Import Games error:", error)
game.managedObjectContext?.delete(game)
errors.insert(.unknown(url, error))
}
}
do
{
try context.save()
}
catch
catch let error as NSError
{
print("Failed to save import context:", error)
identifiers.removeAll()
errors.insert(.saveFailed(urls, error))
}
completion?(identifiers)
DatabaseManager.shared.viewContext.perform {
let predicate = NSPredicate(format: "%K IN (%@)", #keyPath(Game.identifier), identifiers)
let games = Game.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: Game.self)
completion?(Set(games), errors)
}
}
}
func importControllerSkins(at urls: [URL], completion: ((Set<String>) -> Void)?)
func importControllerSkins(at urls: Set<URL>, completion: ((Set<ControllerSkin>, Set<ImportError>) -> Void)?)
{
var errors = Set<ImportError>()
self.performBackgroundTask { (context) in
var identifiers = Set<String>()
for url in urls
{
guard let deltaControllerSkin = DeltaCore.ControllerSkin(fileURL: url) else { continue }
guard FileManager.default.fileExists(atPath: url.path) else {
errors.insert(.doesNotExist(url))
continue
}
guard let deltaControllerSkin = DeltaCore.ControllerSkin(fileURL: url) else {
errors.insert(.invalid(url))
continue
}
let controllerSkin = ControllerSkin(context: context)
controllerSkin.filename = deltaControllerSkin.identifier + ".deltaskin"
@ -212,10 +275,12 @@ extension DatabaseManager
identifiers.insert(controllerSkin.identifier)
}
catch
catch let error as NSError
{
print("Import Controller Skins error:", error)
controllerSkin.managedObjectContext?.delete(controllerSkin)
errors.insert(.unknown(url, error))
}
}
@ -223,26 +288,35 @@ extension DatabaseManager
{
try context.save()
}
catch
catch let error as NSError
{
print("Failed to save controller skin import context:", error)
identifiers.removeAll()
errors.insert(.saveFailed(urls, error))
}
completion?(identifiers)
DatabaseManager.shared.viewContext.perform {
let predicate = NSPredicate(format: "%K IN (%@)", #keyPath(Game.identifier), identifiers)
let controllerSkins = ControllerSkin.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: ControllerSkin.self)
completion?(Set(controllerSkins), errors)
}
}
}
private func extractCompressedGames(at urls: [URL], completion: @escaping ((Set<URL>) -> Void))
private func extractCompressedGames(at urls: Set<URL>, completion: @escaping ((Set<URL>, Set<ImportError>) -> Void))
{
DispatchQueue.global().async {
var semaphores = Set<DispatchSemaphore>()
var outputURLs = Set<URL>()
var errors = Set<ImportError>()
for url in urls
{
var archiveContainsValidGameFile = false
do
{
let archive = try ZZArchive(url: url)
@ -257,6 +331,11 @@ extension DatabaseManager
guard gameType != .unknown else { continue }
// At least one entry is a valid game file, so we set archiveContainsValidGameFile to true
// This will result in this archive being considered valid, and thus we will not return an ImportError.invalid error for the archive
// However, if this game file does turn out to be invalid when extracting, we'll return an ImportError.invalid error specific to this game file
archiveContainsValidGameFile = true
// ROMs may potentially be very large, so we extract using file streams and not raw Data
let inputStream = try entry.newStream()
@ -290,12 +369,15 @@ extension DatabaseManager
}
print(error)
errors.insert(.invalid(outputURL))
}
else
{
outputURLs.insert(outputURL)
semaphore.signal()
}
semaphore.signal()
}
}
}
@ -303,6 +385,11 @@ extension DatabaseManager
{
print(error)
}
if !archiveContainsValidGameFile
{
errors.insert(.invalid(url))
}
}
for semaphore in semaphores
@ -325,7 +412,7 @@ extension DatabaseManager
}
}
completion(outputURLs)
completion(outputURLs, errors)
}
}
}

View File

@ -0,0 +1,80 @@
//
// UIAlertController+Importing.swift
// Delta
//
// Created by Riley Testut on 1/13/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
extension UIAlertController
{
enum ImportType
{
case games
case controllerSkins
}
class func alertController(for importType: ImportType, with errors: Set<DatabaseManager.ImportError>) -> UIAlertController
{
let title: String
switch importType
{
case .games: title = NSLocalizedString("Error Importing Games", comment: "")
case .controllerSkins: title = NSLocalizedString("Error Importing Controller Skins", comment: "")
}
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
var urls = Set<URL>()
for error in errors
{
switch error
{
case .doesNotExist(let url): urls.insert(url)
case .invalid(let url): urls.insert(url)
case .unknown(let url, _): urls.insert(url)
case .saveFailed(let errorURLs, _): urls.formUnion(errorURLs)
}
}
let filenames = urls.map{ $0.lastPathComponent }.sorted()
if filenames.count > 0
{
var message: String
switch importType
{
case .games: message = NSLocalizedString("The following game files could not be imported:", comment: "") + "\n"
case .controllerSkins: message = NSLocalizedString("The following controller skin files could not be imported:", comment: "") + "\n"
}
for filename in filenames
{
message += "\n" + filename
}
alertController.message = message
}
else
{
// This branch can be executed when there are no input URLs when importing, but there is an error saving the database anyway.
switch importType
{
case .games: alertController.message = NSLocalizedString("Delta was unable to import games. Please try again later.", comment: "")
case .controllerSkins: alertController.message = NSLocalizedString("Delta was unable to import controller skins. Please try again later.", comment: "")
}
}
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil))
return alertController
}
}

View File

@ -276,14 +276,32 @@ extension GamesViewController: ImportControllerDelegate
}
//MARK: - ImportControllerDelegate
@nonobjc func importController(_ importController: ImportController, didImport games: Set<Game>)
@nonobjc func importController(_ importController: ImportController, didImport games: Set<Game>, with errors: Set<DatabaseManager.ImportError>)
{
print(games)
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .games, with: errors)
self.present(alertController, animated: true, completion: nil)
}
if games.count > 0
{
print("Imported Games:", games.map { $0.name })
}
}
@nonobjc func importController(_ importController: ImportController, didImport controllerSkins: Set<ControllerSkin>)
@nonobjc func importController(_ importController: ImportController, didImport controllerSkins: Set<ControllerSkin>, with errors: Set<DatabaseManager.ImportError>)
{
print(controllerSkins)
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors)
self.present(alertController, animated: true, completion: nil)
}
if controllerSkins.count > 0
{
print("Imported Controller Skins:", controllerSkins.map { $0.name })
}
}
}

2
External/Roxas vendored

@ -1 +1 @@
Subproject commit 4ceaf192ac161e04adf4a1b6ab964ddf689367b4
Subproject commit df42ea9b1f360756387995917541e5fe7a6178e4