diff --git a/Kiwix-iOS/Controller/Library/DownloadTasksController.swift b/Kiwix-iOS/Controller/Library/DownloadTasksController.swift
index 6aacaced..11f8abf8 100644
--- a/Kiwix-iOS/Controller/Library/DownloadTasksController.swift
+++ b/Kiwix-iOS/Controller/Library/DownloadTasksController.swift
@@ -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
diff --git a/Kiwix-iOS/Info.plist b/Kiwix-iOS/Info.plist
index 609325d4..056d09e2 100644
--- a/Kiwix-iOS/Info.plist
+++ b/Kiwix-iOS/Info.plist
@@ -49,7 +49,7 @@
CFBundleVersion
- 1.7.1525
+ 1.7.1576
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/Kiwix-iOSWidgets/Bookmarks/Info.plist b/Kiwix-iOSWidgets/Bookmarks/Info.plist
index 0b682b4e..445f4b63 100644
--- a/Kiwix-iOSWidgets/Bookmarks/Info.plist
+++ b/Kiwix-iOSWidgets/Bookmarks/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 1.7.1892
+ 1.7.1983
NSExtension
NSExtensionMainStoryboard
diff --git a/Kiwix.xcworkspace/xcuserdata/chrisli.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Kiwix.xcworkspace/xcuserdata/chrisli.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
index dbfe494b..be55addd 100644
--- a/Kiwix.xcworkspace/xcuserdata/chrisli.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
+++ b/Kiwix.xcworkspace/xcuserdata/chrisli.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -19,5 +19,21 @@
landmarkType = "5">
+
+
+
+
diff --git a/Kiwix/CoreData/Classes/Book.swift b/Kiwix/CoreData/Classes/Book.swift
index 729ed6bf..aa538081 100644
--- a/Kiwix/CoreData/Classes/Book.swift
+++ b/Kiwix/CoreData/Classes/Book.swift
@@ -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}
diff --git a/Kiwix/CoreData/Classes/DownloadTask.swift b/Kiwix/CoreData/Classes/DownloadTask.swift
index c8c2bc58..889a9006 100644
--- a/Kiwix/CoreData/Classes/DownloadTask.swift
+++ b/Kiwix/CoreData/Classes/DownloadTask.swift
@@ -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
+ }()
}
diff --git a/Kiwix/Network/Network.swift b/Kiwix/Network/Network.swift
index 33b4c115..260b1161 100644
--- a/Kiwix/Network/Network.swift
+++ b/Kiwix/Network/Network.swift
@@ -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)
}
}
}
diff --git a/Kiwix/Tools/DownloadProgress.swift b/Kiwix/Tools/DownloadProgress.swift
index f1c8562e..b2244dfd 100644
--- a/Kiwix/Tools/DownloadProgress.swift
+++ b/Kiwix/Tools/DownloadProgress.swift
@@ -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! {
diff --git a/Kiwix/Tools/Preference.swift b/Kiwix/Tools/Preference.swift
index 5f346df0..f9f7a764 100644
--- a/Kiwix/Tools/Preference.swift
+++ b/Kiwix/Tools/Preference.swift
@@ -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("libraryHasShownPreferredLanguagePrompt")
static let langFilterSortByAlphabeticalAsc = DefaultsKey("langFilterSortByAlphabeticalAsc")
static let langFilterNameDisplayInOriginalLocale = DefaultsKey("langFilterNameDisplayInOriginalLocale")
+
+ static let resumeData = DefaultsKey<[String: AnyObject]>("resumeData")
}