Library pause / resume

This commit is contained in:
Chris Li 2016-08-29 15:37:18 -04:00
parent 1aeff3dbd0
commit 9879fae22a
9 changed files with 165 additions and 50 deletions

View File

@ -107,17 +107,32 @@ class DownloadTasksController: UITableViewController, NSFetchedResultsController
cell.titleLabel.text = book.title
cell.favIcon.image = UIImage(data: book.favIcon ?? NSData())
guard let progress = Network.shared.operations[id]?.progress else {return}
cell.progressLabel.text = progress.fractionCompletedDescription
cell.progressView.setProgress(Float(progress.fractionCompleted), animated: animated)
cell.detailLabel.text = {
let string = progress.progressAndSpeedDescription
if string.containsString("") {
return string.stringByReplacingOccurrencesOfString("", withString: "\n")
} else {
return string + "\n" + NSLocalizedString("Estimating Speed and Remaining Time", comment: "")
}
}()
if let progress = Network.shared.operations[id]?.progress {
cell.progressLabel.text = progress.fractionCompletedDescription
cell.progressView.setProgress(Float(progress.fractionCompleted), animated: animated)
cell.detailLabel.text = {
let string = progress.progressAndSpeedDescription
if downloadTask.state == .Downloading {
if string.containsString("") {
return string.stringByReplacingOccurrencesOfString("", withString: "\n")
} else {
return string + "\n" + "Estimating"
}
} else {
return string + "\n" + String(downloadTask.state)
}
}()
} else {
let progress = Double(downloadTask.totalBytesWritten) / Double(book.fileSize)
cell.progressLabel.text = DownloadTask.percentFormatter.stringFromNumber(NSNumber(double: progress))
cell.progressView.setProgress(Float(progress), animated: animated)
cell.detailLabel.text = {
let downloadedSize = NSByteCountFormatter.stringFromByteCount(downloadTask.totalBytesWritten, countStyle: .File)
let fileSize = book.fileSizeDescription
return String(format: "%@ of %@ completed", downloadedSize, fileSize) + "\n" + String(downloadTask.state)
}()
}
}
// MARK: Other Data Source
@ -159,27 +174,67 @@ class DownloadTasksController: UITableViewController, NSFetchedResultsController
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {}
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let pause = UITableViewRowAction(style: .Normal, title: "Pause") { (action, indexPath) in
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask else {return}
self.managedObjectContext.performBlock({
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask else {return nil}
var actions = [UITableViewRowAction]()
switch downloadTask.state {
case .Downloading:
let pause = UITableViewRowAction(style: .Normal, title: "Pause") { (action, indexPath) in
downloadTask.state = .Paused
})
guard let bookID = downloadTask.book?.id else {return}
Network.shared.operations[bookID]?.cancel(produceResumeData: true)
guard let bookID = downloadTask.book?.id else {return}
Network.shared.operations[bookID]?.cancel(produceResumeData: true)
tableView.setEditing(false, animated: true)
}
actions.insert(pause, atIndex: 0)
case .Paused:
if let book = downloadTask.book,
let resumeData = Network.shared.data {
let resume = UITableViewRowAction(style: .Normal, title: "Resume") { (action, indexPath) in
let task = Network.shared.session.downloadTaskWithResumeData(resumeData)
let operation = DownloadBookOperation(downloadTask: task)
Network.shared.queue.addOperation(operation)
tableView.setEditing(false, animated: true)
}
actions.insert(resume, atIndex: 0)
} else {
let restart = UITableViewRowAction(style: .Normal, title: "Restart") { (action, indexPath) in
guard let bookID = downloadTask.book?.id,
let operation = DownloadBookOperation(bookID: bookID) else {return}
Network.shared.queue.addOperation(operation)
tableView.setEditing(false, animated: true)
}
actions.insert(restart, atIndex: 0)
}
default:
break
}
let cancel = UITableViewRowAction(style: .Destructive, title: LocalizedStrings.Common.cancel) { (action, indexPath) -> Void in
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask else {return}
if let bookID = downloadTask.book?.id {
// Cancel the download operation, did cancel observer will do the rest
Network.shared.operations[bookID]?.cancel(produceResumeData: false)
if let operation = Network.shared.operations[bookID] {
// When download is ongoing
// Cancel the download operation
// URLSessionTaskDelegate will update coredata and do clean up
operation.cancel(produceResumeData: false)
} else {
// When download is paused
// Remove resume data
// Delete downloadTask object and set book to not local
downloadTask.book?.removeResumeData()
downloadTask.book?.isLocal = NSNumber(bool: false)
self.managedObjectContext.deleteObject(downloadTask)
}
} else {
// In case of something goes wrong, and cannot find the book related to a download task, allow user to delete the row
self.managedObjectContext.performBlock({
self.managedObjectContext.deleteObject(downloadTask)
})
self.managedObjectContext.deleteObject(downloadTask)
}
tableView.setEditing(false, animated: true)
}
return [cancel, pause]
actions.insert(cancel, atIndex: 0)
return actions
}
// MARK: - Fetched Results Controller

View File

@ -49,7 +49,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>1.7.1525</string>
<string>1.7.1576</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.7.1892</string>
<string>1.7.1983</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionMainStoryboard</key>

View File

@ -19,5 +19,21 @@
landmarkType = "5">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Kiwix-iOS/Controller/Library/DownloadTasksController.swift"
timestampString = "494191501.485551"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "195"
endingLineNumber = "195"
landmarkName = "tableView(_:editActionsForRowAtIndexPath:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -74,6 +74,16 @@ class Book: NSManagedObject {
return urlComponents?.URL
}
var resumeDataURL: NSURL? {
guard let id = id,
let folderURL = NSURL(fileURLWithPath: NSFileManager.libDirURL.path!).URLByAppendingPathComponent("DownloadTemp", isDirectory: true),
let folderPath = folderURL.path else {return nil}
if !NSFileManager.defaultManager().fileExistsAtPath(folderPath) {
_ = try? NSFileManager.defaultManager().createDirectoryAtURL(folderURL, withIntermediateDirectories: true, attributes: [NSURLIsExcludedFromBackupKey: true])
}
return folderURL.URLByAppendingPathComponent(id)
}
// MARK: - Fetch
class func fetchAll(context: NSManagedObjectContext) -> [Book] {
@ -103,10 +113,9 @@ class Book: NSManagedObject {
// MARK: - Manage
func removeCache() {
guard let bookID = id,
let cacheFileURL = NSFileManager.cacheDirURL.URLByAppendingPathComponent(bookID) else {return}
_ = try? NSFileManager.defaultManager().removeItemAtURL(cacheFileURL)
func removeResumeData() {
guard let id = id else {return}
Preference.resumeData[id] = nil
}
// MARK: - Properties Description
@ -120,7 +129,7 @@ class Book: NSManagedObject {
return formatter.stringFromDate(date)
}
var fileSizeDescription: String? {
var fileSizeDescription: String {
return NSByteCountFormatter.stringFromByteCount(fileSize, countStyle: .File)
}
@ -152,7 +161,7 @@ class Book: NSManagedObject {
var detailedDescription: String? {
var descriptions = [String]()
if let dateDescription = dateDescription {descriptions.append(dateDescription)}
if let fileSizeDescription = fileSizeDescription {descriptions.append(fileSizeDescription)}
descriptions.append(fileSizeDescription)
if let articleCountDescription = articleCountDescription {descriptions.append(articleCountDescription)}
guard descriptions.count != 0 else {return nil}

View File

@ -40,6 +40,16 @@ class DownloadTask: NSManagedObject {
stateRaw = Int16(newValue.rawValue)
}
}
static let percentFormatter: NSNumberFormatter = {
let formatter = NSNumberFormatter()
formatter.numberStyle = .PercentStyle
formatter.minimumFractionDigits = 1
formatter.maximumIntegerDigits = 3
formatter.minimumFractionDigits = 2
formatter.maximumIntegerDigits = 2
return formatter
}()
}

View File

@ -13,7 +13,9 @@ import Operations
class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate, OperationQueueDelegate {
static let shared = Network()
let queue = OperationQueue()
let context = NSManagedObjectContext.mainQueueContext
private(set) var operations = [String: DownloadBookOperation]()
var data: NSData?
private override init() {
super.init()
@ -49,9 +51,30 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
// MARK: - NSURLSessionTaskDelegate
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
print(error?.code)
print(error?.localizedDescription)
print(error?.userInfo.keys)
guard let error = error, let bookID = task.taskDescription else {return}
self.context.performBlockAndWait {
guard let book = Book.fetch(bookID, context: self.context) else {return}
if error.code == NSURLErrorCancelled {
// If download task doesnt exist, it must mean download is cancelled by user
// DownloadTask object will have been deleted when user tap Cancel button / table row action
guard let downloadTask = book.downloadTask else {return}
downloadTask.totalBytesWritten = task.countOfBytesReceived
downloadTask.state = .Paused
// Save resue data to disk
guard let resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData] as? NSData else {return}
let task = Network.shared.session.downloadTaskWithResumeData(resumeData)
task.resume()
guard let book = downloadTask.book else {return}
self.data = resumeData
// guard let resumeDataPath = book.resumeDataURL?.path,
// let resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData] as? NSData else {return}
// resumeData.writeToFile(resumeDataPath, atomically: true)
} else {
// Handle other errors
}
}
}
// MARK: - NSURLSessionDownloadDelegate
@ -60,6 +83,11 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
guard let bookID = downloadTask.taskDescription,
let operation = operations[bookID] else {return}
operation.progress.completedUnitCount = totalBytesWritten
context.performBlock {
guard let downloadTask = Book.fetch(bookID, context: self.context)?.downloadTask where downloadTask.state == .Queued else {return}
downloadTask.state = .Downloading
}
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
@ -73,12 +101,11 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
_ = try? NSFileManager.defaultManager().moveItemAtURL(location, toURL: destination)
// Perform clean up (remove cache and delete download task)
let context = NSManagedObjectContext.mainQueueContext
context.performBlock {
guard let book = Book.fetch(bookID, context: context) else {return}
book.removeCache()
guard let book = Book.fetch(bookID, context: self.context) else {return}
book.removeResumeData()
guard let downloadTask = book.downloadTask else {return}
context.deleteObject(downloadTask)
self.context.deleteObject(downloadTask)
}
}
}

View File

@ -14,16 +14,6 @@ class DownloadProgress: NSProgress {
private let timePointMinCount: Int = 20
private let timePointMaxCount: Int = 200
private lazy var percentFormatter: NSNumberFormatter = {
let formatter = NSNumberFormatter()
formatter.numberStyle = .PercentStyle
formatter.minimumFractionDigits = 1
formatter.maximumIntegerDigits = 3
formatter.minimumFractionDigits = 2
formatter.maximumIntegerDigits = 2
return formatter
}()
init(completedUnitCount: Int64 = 0, totalUnitCount: Int64) {
super.init(parent: nil, userInfo: [NSProgressFileOperationKindKey: NSProgressFileOperationKindDownloading])
self.kind = NSProgressKindFile
@ -40,7 +30,7 @@ class DownloadProgress: NSProgress {
// MARK: - Descriptions
var fractionCompletedDescription: String? {
return percentFormatter.stringFromNumber(NSNumber(double: fractionCompleted))
return DownloadTask.percentFormatter.stringFromNumber(NSNumber(double: fractionCompleted))
}
var progressAndSpeedDescription: String! {

View File

@ -91,6 +91,12 @@ class Preference {
set{Defaults[.langFilterNameDisplayInOriginalLocale] = newValue}
}
}
// MARK: - Resume Data
class var resumeData: [String: NSData] {
get{return Defaults[.resumeData] as? [String: NSData] ?? [String: NSData]()}
set{Defaults[.resumeData] = newValue}}
}
extension DefaultsKeys {
@ -108,4 +114,6 @@ extension DefaultsKeys {
static let libraryHasShownPreferredLanguagePrompt = DefaultsKey<Bool>("libraryHasShownPreferredLanguagePrompt")
static let langFilterSortByAlphabeticalAsc = DefaultsKey<Bool>("langFilterSortByAlphabeticalAsc")
static let langFilterNameDisplayInOriginalLocale = DefaultsKey<Bool>("langFilterNameDisplayInOriginalLocale")
static let resumeData = DefaultsKey<[String: AnyObject]>("resumeData")
}