diff --git a/Kiwix-iOS/Controller/Library/LibraryBooksController.swift b/Kiwix-iOS/Controller/Library/LibraryBooksController.swift index a99bef68..a6f6d577 100644 --- a/Kiwix-iOS/Controller/Library/LibraryBooksController.swift +++ b/Kiwix-iOS/Controller/Library/LibraryBooksController.swift @@ -131,7 +131,7 @@ class LibraryBooksController: CoreDataCollectionBaseController, UICollectionView book.articleCountDescription ].flatMap({$0}).joined(separator: " ") cell.descriptionLabel.text = book.desc - cell.hasPicLabel.isHidden = book.hasPic + cell.hasPicLabel.isHidden = !book.hasPic return cell } diff --git a/Kiwix-iOS/Storyboard/Library.storyboard b/Kiwix-iOS/Storyboard/Library.storyboard index 9dcffbc1..03a60b0f 100644 --- a/Kiwix-iOS/Storyboard/Library.storyboard +++ b/Kiwix-iOS/Storyboard/Library.storyboard @@ -1,12 +1,13 @@ - + - + + @@ -52,7 +53,7 @@ - + @@ -174,13 +175,22 @@ - - - - - - - + + + + + + + + + + + + + + + + @@ -246,6 +256,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -274,7 +313,7 @@ - + @@ -311,7 +350,7 @@ - + @@ -330,6 +369,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/Kiwix.xcodeproj/project.pbxproj b/Kiwix.xcodeproj/project.pbxproj index e3d49b24..2c00f944 100644 --- a/Kiwix.xcodeproj/project.pbxproj +++ b/Kiwix.xcodeproj/project.pbxproj @@ -54,7 +54,6 @@ 974C49681DA4266200E276E1 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 974C49671DA4266200E276E1 /* CloudKit.framework */; }; 975227CD1D0227E8001D1DDE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 975227CA1D0227E8001D1DDE /* Main.storyboard */; }; 975227D01D022814001D1DDE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 975227CF1D022814001D1DDE /* LaunchScreen.storyboard */; }; - 9757C74C1E106958008A9469 /* BackgroundDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9757C74B1E106958008A9469 /* BackgroundDownload.swift */; }; 97599AA21E26D3B000BA15EF /* BookmarkBooksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97599AA11E26D3B000BA15EF /* BookmarkBooksController.swift */; }; 97599AE21E28193D00BA15EF /* BookmarkCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97599AE11E28193D00BA15EF /* BookmarkCollectionController.swift */; }; 975B90FE1CEB909100D13906 /* iOSExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975B90FD1CEB909100D13906 /* iOSExtensions.swift */; }; @@ -213,7 +212,6 @@ 974C49671DA4266200E276E1 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 975227CA1D0227E8001D1DDE /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = "Kiwix-iOS/Storyboard/Main.storyboard"; sourceTree = SOURCE_ROOT; }; 975227CF1D022814001D1DDE /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = "Kiwix-iOS/Storyboard/LaunchScreen.storyboard"; sourceTree = SOURCE_ROOT; }; - 9757C74B1E106958008A9469 /* BackgroundDownload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundDownload.swift; sourceTree = ""; }; 97599AA11E26D3B000BA15EF /* BookmarkBooksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkBooksController.swift; sourceTree = ""; }; 97599AE11E28193D00BA15EF /* BookmarkCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkCollectionController.swift; sourceTree = ""; }; 975B90FD1CEB909100D13906 /* iOSExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = iOSExtensions.swift; path = "Kiwix-iOS/iOSExtensions.swift"; sourceTree = SOURCE_ROOT; }; @@ -271,7 +269,6 @@ 97D6811C1D6F70AC00E5FA99 /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = ""; }; 97D6811E1D6F70AC00E5FA99 /* ScanLocalBook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanLocalBook.swift; sourceTree = ""; }; 97D681211D6F70AC00E5FA99 /* UpdateWidgetDataSourceOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateWidgetDataSourceOperation.swift; sourceTree = ""; }; - 97D681221D6F70AC00E5FA99 /* URLSessionDownloadTaskOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionDownloadTaskOperation.swift; sourceTree = ""; }; 97D6812B1D6F70DE00E5FA99 /* 1.5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = 1.5.xcdatamodel; sourceTree = ""; }; 97D6812C1D6F70DE00E5FA99 /* 1.7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = 1.7.xcdatamodel; sourceTree = ""; }; 97D6812D1D6F70DE00E5FA99 /* Kiwix.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Kiwix.xcdatamodel; sourceTree = ""; }; @@ -757,7 +754,6 @@ children = ( 97D6811C1D6F70AC00E5FA99 /* Queue.swift */, 9764CBD21D8083AA00072D6A /* ArticleOperation.swift */, - 9757C74B1E106958008A9469 /* BackgroundDownload.swift */, 970A2A211DD562CB0078BB7C /* BookOperations.swift */, 973A5C981DEBC54800C7804C /* CloudKit.swift */, 973208281DD223DB00EDD3DC /* RefreshLibrary.swift */, @@ -765,7 +761,6 @@ 972F81561DDBFC79008D7289 /* Search.swift */, 97D4D64E1D874E6E00C1B065 /* SearchOperation.swift */, 97D681211D6F70AC00E5FA99 /* UpdateWidgetDataSourceOperation.swift */, - 97D681221D6F70AC00E5FA99 /* URLSessionDownloadTaskOperation.swift */, 976C1DD31E300695005EDEC4 /* UIProcedure.swift */, ); name = Operation; @@ -1059,7 +1054,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9757C74C1E106958008A9469 /* BackgroundDownload.swift in Sources */, 973A5C951DEA6DD000C7804C /* URLResponseCache.swift in Sources */, 973207A51DD1984700EDD3DC /* SearchScopeAndHistoryController.swift in Sources */, 973208231DD19C7600EDD3DC /* DownloadProgress.swift in Sources */, diff --git a/Kiwix/CoreData/Classes/DownloadTask.swift b/Kiwix/CoreData/Classes/DownloadTask.swift index d1f041ae..8ca68dab 100644 --- a/Kiwix/CoreData/Classes/DownloadTask.swift +++ b/Kiwix/CoreData/Classes/DownloadTask.swift @@ -11,14 +11,15 @@ import CoreData class DownloadTask: NSManagedObject { - - class func fetch(book: Book, context: NSManagedObjectContext) -> DownloadTask? { - let fetchRequest = NSFetchRequest(entityName: "DownloadTask") + + class func fetch(bookID: String, context: NSManagedObjectContext) -> DownloadTask? { + let fetchRequest = DownloadTask.fetchRequest() as! NSFetchRequest + guard let book = Book.fetch(bookID, context: context) else {return nil} fetchRequest.predicate = NSPredicate(format: "book = %@", book) - let downloadTask = DownloadTask.fetch(fetchRequest, type: DownloadTask.self, context: context)?.first ?? insert(DownloadTask.self, context: context) - downloadTask?.creationTime = Date() - downloadTask?.book = book + guard let downloadTask = try? context.fetch(fetchRequest).first ?? DownloadTask(context: context) else {return nil} + downloadTask.creationTime = Date() + downloadTask.book = book return downloadTask } diff --git a/Kiwix/Network/Network.swift b/Kiwix/Network/Network.swift index fa039aad..ad7b12eb 100644 --- a/Kiwix/Network/Network.swift +++ b/Kiwix/Network/Network.swift @@ -8,7 +8,82 @@ import UIKit -class Network: NSObject { +class Network: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate { static let shared = Network() private override init() {} + var progresses = [String: Int64]() + let managedObjectContext = AppDelegate.persistentContainer.viewContext + var timer: Timer? + + lazy var wifiSession: URLSession = { + let configuration = URLSessionConfiguration.background(withIdentifier: "org.kiwix.wifi") + configuration.allowsCellularAccess = false + configuration.isDiscretionary = false + return URLSession(configuration: configuration, delegate: self, delegateQueue: nil) + }() + lazy var cellularSession: URLSession = { + let configuration = URLSessionConfiguration.background(withIdentifier: "org.kiwix.cellular") + configuration.allowsCellularAccess = true + configuration.isDiscretionary = false + return URLSession(configuration: configuration, delegate: self, delegateQueue: nil) + }() + + // MARK: - actions + + func start(book: Book) { + guard let url = book.url else {return} + let task = (book.fileSize > 100000000 ? wifiSession: cellularSession).downloadTask(with: url) + task.taskDescription = book.id + task.resume() + + let downloadTask = DownloadTask.fetch(bookID: book.id, context: managedObjectContext) + downloadTask?.state = .queued + + progresses[book.id] = 0 + if progresses.count == 1 { startTimer() } + } + + func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in + for (bookID, bytesWritten) in self.progresses { + guard let book = Book.fetch(bookID, context: self.managedObjectContext) else {continue} + book.downloadTask?.totalBytesWritten = bytesWritten + } + }) + } + + // MARK: - URLSessionTaskDelegate + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + + } + + // MARK: - URLSessionDownloadDelegate + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) { + + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + guard let bookID = downloadTask.taskDescription else {return} + progresses[bookID] = bytesWritten + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + managedObjectContext.perform { + guard let bookID = downloadTask.taskDescription, + let book = Book.fetch(bookID, context: self.managedObjectContext), + let downloadTask = DownloadTask.fetch(bookID: bookID, context: self.managedObjectContext) else {return} + //book.state = .local + self.managedObjectContext.delete(downloadTask) + print("finish downloading") + + self.progresses[bookID] = nil + if self.progresses.count == 0 { self.timer?.invalidate() } + } + + + } + + } diff --git a/Kiwix/Operations/BackgroundDownload.swift b/Kiwix/Operations/BackgroundDownload.swift deleted file mode 100644 index 09c4c691..00000000 --- a/Kiwix/Operations/BackgroundDownload.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// BookDownload.swift -// Kiwix -// -// Created by Chris Li on 12/25/16. -// Copyright © 2016 Chris Li. All rights reserved. -// - -import ProcedureKit - -class BackgroundDownloadProcedure: Procedure { - let task: URLSessionDownloadTask - let resumeDataProcessing: (Data?) -> Void - private let stateLock = NSLock() - private var produceResumeData = false - - init(task: URLSessionDownloadTask, resumeData: @escaping (Data?) -> Void) { - self.task = task - self.resumeDataProcessing = resumeData - super.init() - - add(observer: NetworkObserver()) - addDidCancelBlockObserver { procedure, errors in - procedure.stateLock.withCriticalScope { - if procedure.produceResumeData { - procedure.task.cancel(byProducingResumeData: self.resumeDataProcessing) - } else { - procedure.task.cancel() - } - } - } - } - - override func execute() { - stateLock.withCriticalScope { - guard !isCancelled, task.state == .suspended else { return } - task.resume() - } - } - - func pause() { - produceResumeData = true - cancel() - } -} - -class BookDownloadProcedure: BackgroundDownloadProcedure { - - init(task: URLSessionDownloadTask) { - super.init(task: task) { data in - print("cancelled, resume data length = \(data?.count)") - } - addDidFinishBlockObserver { (procedure, errors) in - print("Download has finished") - } - } - - convenience init(session: URLSession, bookID: String, url: URL) { - let task = session.downloadTask(with: url) - task.taskDescription = bookID - self.init(task: task) - } - - convenience init(session: URLSession, bookID: String, resumeData: Data) { - let task = session.downloadTask(withResumeData: resumeData) - task.taskDescription = bookID - self.init(task: task) - } - - override func execute() { - let context = AppDelegate.persistentContainer.viewContext - context.performAndWait { - guard let bookID = self.task.taskDescription, - let book = Book.fetch(bookID, context: context), - let downloadTask = DownloadTask.fetch(book: book, context: context) else {return} - downloadTask.state = .queued - } - super.execute() - } -} diff --git a/Kiwix/Operations/UIProcedure.swift b/Kiwix/Operations/UIProcedure.swift index 1b4eeb69..982127ba 100644 --- a/Kiwix/Operations/UIProcedure.swift +++ b/Kiwix/Operations/UIProcedure.swift @@ -91,6 +91,7 @@ extension AlertProcedure { let alert = AlertProcedure(presentAlertFrom: context, withPreferredStyle: .actionSheet, waitForDismissal: true) alert.title = book.title alert.add(actionWithTitle: Localized.Library.download, style: .default) { _ in + Network.shared.start(book: book) alert.finish() } alert.add(actionWithTitle: Localized.Library.copyURL, style: .default) { _ in diff --git a/Kiwix/Operations/URLSessionDownloadTaskOperation.swift b/Kiwix/Operations/URLSessionDownloadTaskOperation.swift deleted file mode 100644 index 672f3f9d..00000000 --- a/Kiwix/Operations/URLSessionDownloadTaskOperation.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// URLSessionDownloadTaskOperation.swift -// Kiwix -// -// Created by Chris Li on 7/11/16. -// Copyright © 2016 Chris Li. All rights reserved. -// - -import ProcedureKit - -class URLSessionDownloadTaskOperation: Procedure { - - enum KeyPath: String { - case State = "state" - } - - let task: URLSessionTask - - fileprivate(set) var produceResumeData = false - fileprivate var removedObserved = false - fileprivate let lock = NSLock() - - init(downloadTask: URLSessionDownloadTask) { - self.task = downloadTask - super.init() - - add(observer: NetworkObserver()) - addObserver(DidCancelObserver { _ in - if self.produceResumeData { - downloadTask.cancelByProducingResumeData({ _ in }) - } else { - downloadTask.cancel() - } - }) - } - - func cancel(produceResumeData: Bool) { - self.produceResumeData = produceResumeData - cancel() - } - - override func execute() { - guard task.state == .suspended || task.state == .running else { - finish() - return - } - task.addObserver(self, forKeyPath: "state", options: NSKeyValueObservingOptions(), context: &URLSessionTaskOperationKVOContext) - if task.state == .suspended { - task.resume() - } - } - - override func - - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [String : AnyObject]?, context: UnsafeMutableRawPointer) { - guard context == &URLSessionTaskOperationKVOContext else { return } - - lock.withCriticalScope { - if object === task && keyPath == KeyPath.State.rawValue && !removedObserved { - - if case .completed = task.state { - finish(task.error) - } - - switch task.state { - case .completed, .canceling: - task.removeObserver(self, forKeyPath: KeyPath.State.rawValue) - removedObserved = true - default: - break - } - } - } - } -} - -// swiftlint:disable variable_name -private var URLSessionTaskOperationKVOContext = 0 -// swiftlint:enable variable_name - -extension NSLock { - func withCriticalScope(_ block: () -> T) -> T { - lock() - let value = block() - unlock() - return value - } -}