--- /dev/null
+//
+// MZDownloadManager.swift
+// MZDownloadManager
+//
+// Created by Muhammad Zeeshan on 19/04/2016.
+// Copyright © 2016 ideamakerz. All rights reserved.
+//
+
+import UIKit
+fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
+ switch (lhs, rhs) {
+ case let (l?, r?):
+ return l < r
+ case (nil, _?):
+ return true
+ default:
+ return false
+ }
+}
+
+fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
+ switch (lhs, rhs) {
+ case let (l?, r?):
+ return l > r
+ default:
+ return rhs < lhs
+ }
+}
+
+
+@objc public protocol MZDownloadManagerDelegate: class {
+ /**A delegate method called each time whenever any download task's progress is updated
+ */
+ @objc func downloadRequestDidUpdateProgress(_ downloadModel: MZDownloadModel, index: Int)
+ /**A delegate method called when interrupted tasks are repopulated
+ */
+ @objc func downloadRequestDidPopulatedInterruptedTasks(_ downloadModel: [MZDownloadModel])
+ /**A delegate method called each time whenever new download task is start downloading
+ */
+ @objc optional func downloadRequestStarted(_ downloadModel: MZDownloadModel, index: Int)
+ /**A delegate method called each time whenever running download task is paused. If task is already paused the action will be ignored
+ */
+ @objc optional func downloadRequestDidPaused(_ downloadModel: MZDownloadModel, index: Int)
+ /**A delegate method called each time whenever any download task is resumed. If task is already downloading the action will be ignored
+ */
+ @objc optional func downloadRequestDidResumed(_ downloadModel: MZDownloadModel, index: Int)
+ /**A delegate method called each time whenever any download task is resumed. If task is already downloading the action will be ignored
+ */
+ @objc optional func downloadRequestDidRetry(_ downloadModel: MZDownloadModel, index: Int)
+ /**A delegate method called each time whenever any download task is cancelled by the user
+ */
+ @objc optional func downloadRequestCanceled(_ downloadModel: MZDownloadModel, index: Int)
+ /**A delegate method called each time whenever any download task is finished successfully
+ */
+ @objc optional func downloadRequestFinished(_ downloadModel: MZDownloadModel, index: Int)
+ /**A delegate method called each time whenever any download task is failed due to any reason
+ */
+ @objc optional func downloadRequestDidFailedWithError(_ error: NSError, downloadModel: MZDownloadModel, index: Int)
+ /**A delegate method called each time whenever specified destination does not exists. It will be called on the session queue. It provides the opportunity to handle error appropriately
+ */
+ @objc optional func downloadRequestDestinationDoestNotExists(_ downloadModel: MZDownloadModel, index: Int, location: URL)
+
+}
+
+open class MZDownloadManager: NSObject {
+
+ fileprivate var sessionManager: URLSession!
+
+ fileprivate var backgroundSessionCompletionHandler: (() -> Void)?
+
+ fileprivate let TaskDescFileNameIndex = 0
+ fileprivate let TaskDescFileURLIndex = 1
+ fileprivate let TaskDescFileDestinationIndex = 2
+
+ fileprivate weak var delegate: MZDownloadManagerDelegate?
+
+ open var downloadingArray: [MZDownloadModel] = []
+
+ public convenience init(session sessionIdentifer: String, delegate: MZDownloadManagerDelegate) {
+ self.init()
+
+ self.delegate = delegate
+ self.sessionManager = backgroundSession(identifier: sessionIdentifer)
+ self.populateOtherDownloadTasks()
+ }
+
+ public convenience init(session sessionIdentifer: String, delegate: MZDownloadManagerDelegate, completion: (() -> Void)?) {
+ self.init(session: sessionIdentifer, delegate: delegate)
+ self.backgroundSessionCompletionHandler = completion
+ }
+
+ fileprivate func backgroundSession(identifier: String) -> URLSession {
+ let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: identifier)
+ let session = Foundation.URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil)
+ return session
+ }
+}
+
+// MARK: Private Helper functions
+
+extension MZDownloadManager {
+
+ fileprivate func downloadTasks() -> [URLSessionDownloadTask] {
+ var tasks: [URLSessionDownloadTask] = []
+ let semaphore : DispatchSemaphore = DispatchSemaphore(value: 0)
+ sessionManager.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
+ tasks = downloadTasks
+ semaphore.signal()
+ }
+
+ let _ = semaphore.wait(timeout: DispatchTime.distantFuture)
+
+ debugPrint("MZDownloadManager: pending tasks \(tasks)")
+
+ return tasks
+ }
+
+ fileprivate func populateOtherDownloadTasks() {
+
+ let downloadTasks = self.downloadTasks()
+
+ for downloadTask in downloadTasks {
+ let taskDescComponents: [String] = downloadTask.taskDescription!.components(separatedBy: ",")
+ let fileName = taskDescComponents[TaskDescFileNameIndex]
+ let fileURL = taskDescComponents[TaskDescFileURLIndex]
+ let destinationPath = taskDescComponents[TaskDescFileDestinationIndex]
+
+ let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
+ downloadModel.task = downloadTask
+ downloadModel.startTime = Date()
+
+ if downloadTask.state == .running {
+ downloadModel.status = TaskStatus.downloading.description()
+ downloadingArray.append(downloadModel)
+ } else if(downloadTask.state == .suspended) {
+ downloadModel.status = TaskStatus.paused.description()
+ downloadingArray.append(downloadModel)
+ } else {
+ downloadModel.status = TaskStatus.failed.description()
+ }
+ }
+ }
+
+ fileprivate func isValidResumeData(_ resumeData: Data?) -> Bool {
+
+ guard resumeData != nil || resumeData?.count > 0 else {
+ return false
+ }
+
+ do {
+ var resumeDictionary : AnyObject!
+ resumeDictionary = try PropertyListSerialization.propertyList(from: resumeData!, options: PropertyListSerialization.MutabilityOptions(), format: nil) as AnyObject!
+ var localFilePath = (resumeDictionary?["NSURLSessionResumeInfoLocalPath"] as? String)
+
+ if localFilePath == nil || localFilePath?.count < 1 {
+ localFilePath = (NSTemporaryDirectory() as String) + (resumeDictionary["NSURLSessionResumeInfoTempFileName"] as! String)
+ }
+
+ let fileManager : FileManager! = FileManager.default
+ debugPrint("resume data file exists: \(fileManager.fileExists(atPath: localFilePath! as String))")
+ return fileManager.fileExists(atPath: localFilePath! as String)
+ } catch let error as NSError {
+ debugPrint("resume data is nil: \(error)")
+ return false
+ }
+ }
+}
+
+extension MZDownloadManager: URLSessionDownloadDelegate {
+
+ public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
+ for (index, downloadModel) in self.downloadingArray.enumerated() {
+ if downloadTask.isEqual(downloadModel.task) {
+ DispatchQueue.main.async(execute: { () -> Void in
+
+ let receivedBytesCount = Double(downloadTask.countOfBytesReceived)
+ let totalBytesCount = Double(downloadTask.countOfBytesExpectedToReceive)
+ let progress = Float(receivedBytesCount / totalBytesCount)
+
+ let taskStartedDate = downloadModel.startTime!
+ let timeInterval = taskStartedDate.timeIntervalSinceNow
+ let downloadTime = TimeInterval(-1 * timeInterval)
+
+ let speed = Float(totalBytesWritten) / Float(downloadTime)
+
+ let remainingContentLength = totalBytesExpectedToWrite - totalBytesWritten
+
+ let remainingTime = remainingContentLength / Int64(speed)
+ let hours = Int(remainingTime) / 3600
+ let minutes = (Int(remainingTime) - hours * 3600) / 60
+ let seconds = Int(remainingTime) - hours * 3600 - minutes * 60
+
+ let totalFileSize = MZUtility.calculateFileSizeInUnit(totalBytesExpectedToWrite)
+ let totalFileSizeUnit = MZUtility.calculateUnit(totalBytesExpectedToWrite)
+
+ let downloadedFileSize = MZUtility.calculateFileSizeInUnit(totalBytesWritten)
+ let downloadedSizeUnit = MZUtility.calculateUnit(totalBytesWritten)
+
+ let speedSize = MZUtility.calculateFileSizeInUnit(Int64(speed))
+ let speedUnit = MZUtility.calculateUnit(Int64(speed))
+
+ downloadModel.remainingTime = (hours, minutes, seconds)
+ downloadModel.file = (totalFileSize, totalFileSizeUnit as String)
+ downloadModel.downloadedFile = (downloadedFileSize, downloadedSizeUnit as String)
+ downloadModel.speed = (speedSize, speedUnit as String)
+ downloadModel.progress = progress
+
+ self.downloadingArray[index] = downloadModel
+
+ self.delegate?.downloadRequestDidUpdateProgress(downloadModel, index: index)
+ })
+ break
+ }
+ }
+ }
+
+ public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
+ for (index, downloadModel) in downloadingArray.enumerated() {
+ if downloadTask.isEqual(downloadModel.task) {
+ let fileName = downloadModel.fileName as NSString
+ let basePath = downloadModel.destinationPath == "" ? MZUtility.baseFilePath : downloadModel.destinationPath
+ let destinationPath = (basePath as NSString).appendingPathComponent(fileName as String)
+
+ let fileManager : FileManager = FileManager.default
+
+ //If all set just move downloaded file to the destination
+ if fileManager.fileExists(atPath: basePath) {
+ let fileURL = URL(fileURLWithPath: destinationPath as String)
+ debugPrint("directory path = \(destinationPath)")
+
+ do {
+ try fileManager.moveItem(at: location, to: fileURL)
+ } catch let error as NSError {
+ debugPrint("Error while moving downloaded file to destination path:\(error)")
+ DispatchQueue.main.async(execute: { () -> Void in
+ self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
+ })
+ }
+ } else {
+ //Opportunity to handle the folder doesnot exists error appropriately.
+ //Move downloaded file to destination
+ //Delegate will be called on the session queue
+ //Otherwise blindly give error Destination folder does not exists
+
+ if let _ = self.delegate?.downloadRequestDestinationDoestNotExists {
+ self.delegate?.downloadRequestDestinationDoestNotExists?(downloadModel, index: index, location: location)
+ } else {
+ let error = NSError(domain: "FolderDoesNotExist", code: 404, userInfo: [NSLocalizedDescriptionKey : "Destination folder does not exists"])
+ self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
+ }
+ }
+
+ break
+ }
+ }
+ }
+
+ public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+ debugPrint("task id: \(task.taskIdentifier)")
+ /***** Any interrupted tasks due to any reason will be populated in failed state after init *****/
+
+ DispatchQueue.main.async {
+
+ let err = error as NSError?
+
+ if (err?.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? NSNumber)?.intValue == NSURLErrorCancelledReasonUserForceQuitApplication || (err?.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? NSNumber)?.intValue == NSURLErrorCancelledReasonBackgroundUpdatesDisabled {
+
+ let downloadTask = task as! URLSessionDownloadTask
+ let taskDescComponents: [String] = downloadTask.taskDescription!.components(separatedBy: ",")
+ let fileName = taskDescComponents[self.TaskDescFileNameIndex]
+ let fileURL = taskDescComponents[self.TaskDescFileURLIndex]
+ let destinationPath = taskDescComponents[self.TaskDescFileDestinationIndex]
+
+ let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
+ downloadModel.status = TaskStatus.failed.description()
+ downloadModel.task = downloadTask
+
+ let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
+
+ var newTask = downloadTask
+ if self.isValidResumeData(resumeData) == true {
+ newTask = self.sessionManager.downloadTask(withResumeData: resumeData!)
+ } else {
+ newTask = self.sessionManager.downloadTask(with: URL(string: fileURL as String)!)
+ }
+
+ newTask.taskDescription = downloadTask.taskDescription
+ downloadModel.task = newTask
+
+ self.downloadingArray.append(downloadModel)
+
+ self.delegate?.downloadRequestDidPopulatedInterruptedTasks(self.downloadingArray)
+
+ } else {
+ for(index, object) in self.downloadingArray.enumerated() {
+ let downloadModel = object
+ if task.isEqual(downloadModel.task) {
+ if err?.code == NSURLErrorCancelled || err == nil {
+ self.downloadingArray.remove(at: index)
+
+ if err == nil {
+ self.delegate?.downloadRequestFinished?(downloadModel, index: index)
+ } else {
+ self.delegate?.downloadRequestCanceled?(downloadModel, index: index)
+ }
+
+ } else {
+ let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
+ var newTask = task
+ if self.isValidResumeData(resumeData) == true {
+ newTask = self.sessionManager.downloadTask(withResumeData: resumeData!)
+ } else {
+ newTask = self.sessionManager.downloadTask(with: URL(string: downloadModel.fileURL)!)
+ }
+
+ newTask.taskDescription = task.taskDescription
+ downloadModel.status = TaskStatus.failed.description()
+ downloadModel.task = newTask as? URLSessionDownloadTask
+
+ self.downloadingArray[index] = downloadModel
+
+ if let error = err {
+ self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
+ } else {
+ let error: NSError = NSError(domain: "MZDownloadManagerDomain", code: 1000, userInfo: [NSLocalizedDescriptionKey : "Unknown error occurred"])
+
+ self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
+ if let backgroundCompletion = self.backgroundSessionCompletionHandler {
+ DispatchQueue.main.async(execute: {
+ backgroundCompletion()
+ })
+ }
+ debugPrint("All tasks are finished")
+ }
+}
+
+//MARK: Public Helper Functions
+
+extension MZDownloadManager {
+
+ @objc public func addDownloadTask(_ fileName: String, fileURL: String, destinationPath: String) {
+
+ let url = URL(string: fileURL as String)!
+ let request = URLRequest(url: url)
+
+ let downloadTask = sessionManager.downloadTask(with: request)
+ downloadTask.taskDescription = [fileName, fileURL, destinationPath].joined(separator: ",")
+ downloadTask.resume()
+
+ debugPrint("session manager:\(sessionManager) url:\(url) request:\(request)")
+
+ let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
+ downloadModel.startTime = Date()
+ downloadModel.status = TaskStatus.downloading.description()
+ downloadModel.task = downloadTask
+
+ downloadingArray.append(downloadModel)
+ delegate?.downloadRequestStarted?(downloadModel, index: downloadingArray.count - 1)
+ }
+
+ @objc public func addDownloadTask(_ fileName: String, fileURL: String) {
+ addDownloadTask(fileName, fileURL: fileURL, destinationPath: "")
+ }
+
+ @objc public func pauseDownloadTaskAtIndex(_ index: Int) {
+
+ let downloadModel = downloadingArray[index]
+
+ guard downloadModel.status != TaskStatus.paused.description() else {
+ return
+ }
+
+ let downloadTask = downloadModel.task
+ downloadTask!.suspend()
+ downloadModel.status = TaskStatus.paused.description()
+ downloadModel.startTime = Date()
+
+ downloadingArray[index] = downloadModel
+
+ delegate?.downloadRequestDidPaused?(downloadModel, index: index)
+ }
+
+ @objc public func resumeDownloadTaskAtIndex(_ index: Int) {
+
+ let downloadModel = downloadingArray[index]
+
+ guard downloadModel.status != TaskStatus.downloading.description() else {
+ return
+ }
+
+ let downloadTask = downloadModel.task
+ downloadTask!.resume()
+ downloadModel.status = TaskStatus.downloading.description()
+
+ downloadingArray[index] = downloadModel
+
+ delegate?.downloadRequestDidResumed?(downloadModel, index: index)
+ }
+
+ @objc public func retryDownloadTaskAtIndex(_ index: Int) {
+ let downloadModel = downloadingArray[index]
+
+ guard downloadModel.status != TaskStatus.downloading.description() else {
+ return
+ }
+
+ let downloadTask = downloadModel.task
+
+ downloadTask!.resume()
+ downloadModel.status = TaskStatus.downloading.description()
+ downloadModel.startTime = Date()
+ downloadModel.task = downloadTask
+
+ downloadingArray[index] = downloadModel
+ }
+
+ @objc public func cancelTaskAtIndex(_ index: Int) {
+ let downloadInfo = downloadingArray[index]
+ let downloadTask = downloadInfo.task
+ downloadTask!.cancel()
+ }
+
+ @objc public func presentNotificationForDownload(_ notifAction: String, notifBody: String) {
+ let application = UIApplication.shared
+ let applicationState = application.applicationState
+
+ if applicationState == UIApplicationState.background {
+ let localNotification = UILocalNotification()
+ localNotification.alertBody = notifBody
+ localNotification.alertAction = notifAction
+ localNotification.soundName = UILocalNotificationDefaultSoundName
+ localNotification.applicationIconBadgeNumber += 1
+ application.presentLocalNotificationNow(localNotification)
+ }
+ }
+}