diff --git a/.gitignore b/.gitignore index de72aeb7..4a60bc43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,67 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside *.xcuserstate -Pods -Kiwix/libkiwix/C&C++ -Kiwix/libkiwix/include -Kiwix/libkiwix/shared -Kiwix/libkiwix/static -*.a -Kiwix/libkiwix/iOS -Kiwix/libkiwix/macOS + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output + +Kiwix/libkiwix/ \ No newline at end of file diff --git a/Kiwix-iOS/Controller/Library/CloudBooksController.swift b/Kiwix-iOS/Controller/Library/CloudBooksController.swift index 30072101..bf096b6a 100644 --- a/Kiwix-iOS/Controller/Library/CloudBooksController.swift +++ b/Kiwix-iOS/Controller/Library/CloudBooksController.swift @@ -274,8 +274,10 @@ class CloudBooksController: CoreDataTableBaseController, UITableViewDelegate, UI switch book.spaceState { case .enough: let action = UITableViewRowAction(style: UITableViewRowActionStyle.normal, title: LocalizedStrings.download, handler: { _ in -// guard let download = DownloadBookOperation(bookID: book.id) else {return} -// Network.shared.queue.addOperation(download) + guard let url = book.url else {return} + let download = BookDownloadProcedure(session: DownloadManager.shared.session, bookID: book.id, url: url) + DownloadManager.shared.queue.add(operation: download) + self.tableView.setEditing(false, animated: true) }) action.backgroundColor = UIColor.defaultTint return [action] diff --git a/Kiwix-iOS/Info.plist b/Kiwix-iOS/Info.plist index 6a58f533..e2426d5b 100644 --- a/Kiwix-iOS/Info.plist +++ b/Kiwix-iOS/Info.plist @@ -49,7 +49,7 @@ CFBundleVersion - 1.8.3683 + 1.8.3835 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/Kiwix-iOSWidgets/Bookmarks/Info.plist b/Kiwix-iOSWidgets/Bookmarks/Info.plist index 7296d298..c58076dd 100644 --- a/Kiwix-iOSWidgets/Bookmarks/Info.plist +++ b/Kiwix-iOSWidgets/Bookmarks/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 1.8.3701 + 1.8.3853 NSExtension NSExtensionMainStoryboard diff --git a/Kiwix.xcodeproj/project.pbxproj b/Kiwix.xcodeproj/project.pbxproj index ddcfa388..253632ad 100644 --- a/Kiwix.xcodeproj/project.pbxproj +++ b/Kiwix.xcodeproj/project.pbxproj @@ -63,6 +63,8 @@ 975227CD1D0227E8001D1DDE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 975227CA1D0227E8001D1DDE /* Main.storyboard */; }; 975227CE1D0227E8001D1DDE /* Setting.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 975227CB1D0227E8001D1DDE /* Setting.storyboard */; }; 975227D01D022814001D1DDE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 975227CF1D022814001D1DDE /* LaunchScreen.storyboard */; }; + 9757C74A1E10660B008A9469 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9757C7491E10660B008A9469 /* DownloadManager.swift */; }; + 9757C74C1E106958008A9469 /* BackgroundDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9757C74B1E106958008A9469 /* BackgroundDownload.swift */; }; 975B90FE1CEB909100D13906 /* iOSExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975B90FD1CEB909100D13906 /* iOSExtensions.swift */; }; 9764CBD11D806AD800072D6A /* RefreshLibControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9764CBD01D806AD800072D6A /* RefreshLibControl.swift */; }; 9764F5931D830EF200E0B1C4 /* liblzma.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9764F5921D830EF200E0B1C4 /* liblzma.tbd */; }; @@ -217,6 +219,8 @@ 975227CA1D0227E8001D1DDE /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = "Kiwix-iOS/Storyboard/Main.storyboard"; sourceTree = SOURCE_ROOT; }; 975227CB1D0227E8001D1DDE /* Setting.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Setting.storyboard; path = "Kiwix-iOS/Storyboard/Setting.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; }; + 9757C7491E10660B008A9469 /* DownloadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; + 9757C74B1E106958008A9469 /* BackgroundDownload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundDownload.swift; sourceTree = ""; }; 975B90FD1CEB909100D13906 /* iOSExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = iOSExtensions.swift; path = "Kiwix-iOS/iOSExtensions.swift"; sourceTree = SOURCE_ROOT; }; 9763275D1D64FE0F0034F120 /* BookDetailController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookDetailController.swift; sourceTree = ""; }; 9764CBD01D806AD800072D6A /* RefreshLibControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshLibControl.swift; sourceTree = ""; }; @@ -751,6 +755,7 @@ isa = PBXGroup; children = ( 97DF259F1D6F996B001648A3 /* Network.swift */, + 9757C7491E10660B008A9469 /* DownloadManager.swift */, 9726591A1D8DB91200D1DFFB /* DownloadProgress.swift */, ); path = Network; @@ -773,6 +778,7 @@ children = ( 97D6811C1D6F70AC00E5FA99 /* GlobalQueue.swift */, 9764CBD21D8083AA00072D6A /* ArticleOperation.swift */, + 9757C74B1E106958008A9469 /* BackgroundDownload.swift */, 970A2A211DD562CB0078BB7C /* BookOperations.swift */, 973A5C981DEBC54800C7804C /* CloudKit.swift */, 973208281DD223DB00EDD3DC /* RefreshLibrary.swift */, @@ -1111,6 +1117,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9757C74C1E106958008A9469 /* BackgroundDownload.swift in Sources */, 970E7F771D9DBEA900741290 /* SettingController.swift in Sources */, 973A5C951DEA6DD000C7804C /* URLResponseCache.swift in Sources */, 973207A51DD1984700EDD3DC /* SearchBooksController.swift in Sources */, @@ -1137,6 +1144,7 @@ 97A1FD3A1D6F724E00A80EE2 /* reader.cpp in Sources */, 973207A31DD1983D00EDD3DC /* LanguageFilterController.swift in Sources */, 97D681371D6F711A00E5FA99 /* Article.swift in Sources */, + 9757C74A1E10660B008A9469 /* DownloadManager.swift in Sources */, 972F81571DDBFC79008D7289 /* Search.swift in Sources */, 970E7F831DA0305000741290 /* WelcomeController.swift in Sources */, 97A1FD3B1D6F724E00A80EE2 /* stringTools.cpp in Sources */, diff --git a/Kiwix.xcodeproj/xcuserdata/Chrisli.xcuserdatad/xcschemes/xcschememanagement.plist b/Kiwix.xcodeproj/xcuserdata/Chrisli.xcuserdatad/xcschemes/xcschememanagement.plist index efbae53c..3c9b9b3c 100644 --- a/Kiwix.xcodeproj/xcuserdata/Chrisli.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Kiwix.xcodeproj/xcuserdata/Chrisli.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ Bookmarks.xcscheme orderHint - 2 + 1 Kiwix-iOS.xcscheme @@ -20,57 +20,57 @@ 9722121A1D3ECCFE00C0DCF2 primary - + 973BCCE81CEB3FA400F10B44 primary - + 973BCCFB1CEB3FA400F10B44 primary - + 973BCD061CEB3FA500F10B44 primary - + 9779C2FD1D4574280064CC8E primary - + 9779C3121D4575AD0064CC8E primary - + 97A2AB871C1B80FF00052E74 primary - + 97A2AB9E1C1B80FF00052E74 primary - + 97A2ABA91C1B810000052E74 primary - + 97CF3EEA1D428F9600AE82FE primary - + 97E609EE1D103DED00EBCB9D primary - + diff --git a/Kiwix/CoreData/Classes/DownloadTask.swift b/Kiwix/CoreData/Classes/DownloadTask.swift index 03a06903..d1f041ae 100644 --- a/Kiwix/CoreData/Classes/DownloadTask.swift +++ b/Kiwix/CoreData/Classes/DownloadTask.swift @@ -12,7 +12,7 @@ import CoreData class DownloadTask: NSManagedObject { - class func fetch(_ book: Book, context: NSManagedObjectContext) -> DownloadTask? { + class func fetch(book: Book, context: NSManagedObjectContext) -> DownloadTask? { let fetchRequest = NSFetchRequest(entityName: "DownloadTask") fetchRequest.predicate = NSPredicate(format: "book = %@", book) let downloadTask = DownloadTask.fetch(fetchRequest, type: DownloadTask.self, context: context)?.first ?? insert(DownloadTask.self, context: context) diff --git a/Kiwix/Network/DownloadManager.swift b/Kiwix/Network/DownloadManager.swift new file mode 100644 index 00000000..a35eb5bf --- /dev/null +++ b/Kiwix/Network/DownloadManager.swift @@ -0,0 +1,62 @@ +// +// DownloadManager.swift +// Kiwix +// +// Created by Chris Li on 12/25/16. +// Copyright © 2016 Chris Li. All rights reserved. +// + +import ProcedureKit + +class DownloadManager: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate { + static let shared = DownloadManager() + let queue = ProcedureQueue() + private override init() { + super.init() + } + + private(set) lazy var session: Foundation.URLSession = { + let configuration = URLSessionConfiguration.background(withIdentifier: "org.kiwix.www") + configuration.allowsCellularAccess = false + configuration.isDiscretionary = false + return URLSession(configuration: configuration, delegate: self, delegateQueue: nil) + }() + + + + // MARK: - URLSessionDelegate + + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + + } + + // MARK: - URLSessionTaskDelegate + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let operation = queue.operations.flatMap({$0 as? BookDownloadProcedure}) + .filter({$0.task.taskDescription == task.taskDescription}) + .first else { + if let bookID = task.taskDescription, + let resumeData = (error as? NSError)?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data { + let operation = BookDownloadProcedure(session: session, bookID: bookID, resumeData: resumeData) + DownloadManager.shared.queue.add(operation: operation) + } + return + } + if let error = error { + operation.finish(withError: error) + } else { + operation.finish() + } + } + + // MARK: - URLSessionDownloadDelegate + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + + } +} diff --git a/Kiwix/Operations/BackgroundDownload.swift b/Kiwix/Operations/BackgroundDownload.swift new file mode 100644 index 00000000..09c4c691 --- /dev/null +++ b/Kiwix/Operations/BackgroundDownload.swift @@ -0,0 +1,80 @@ +// +// 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() + } +}