added iOS source code
[wl-app.git] / iOS / Pods / MZDownloadManager / MZDownloadManager / Classes / MZDownloadManager.swift
1 //
2 //  MZDownloadManager.swift
3 //  MZDownloadManager
4 //
5 //  Created by Muhammad Zeeshan on 19/04/2016.
6 //  Copyright © 2016 ideamakerz. All rights reserved.
7 //
8
9 import UIKit
10 fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
11   switch (lhs, rhs) {
12   case let (l?, r?):
13     return l < r
14   case (nil, _?):
15     return true
16   default:
17     return false
18   }
19 }
20
21 fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
22   switch (lhs, rhs) {
23   case let (l?, r?):
24     return l > r
25   default:
26     return rhs < lhs
27   }
28 }
29
30
31 @objc public protocol MZDownloadManagerDelegate: class {
32     /**A delegate method called each time whenever any download task's progress is updated
33      */
34     @objc func downloadRequestDidUpdateProgress(_ downloadModel: MZDownloadModel, index: Int)
35     /**A delegate method called when interrupted tasks are repopulated
36      */
37     @objc func downloadRequestDidPopulatedInterruptedTasks(_ downloadModel: [MZDownloadModel])
38     /**A delegate method called each time whenever new download task is start downloading
39      */
40     @objc optional func downloadRequestStarted(_ downloadModel: MZDownloadModel, index: Int)
41     /**A delegate method called each time whenever running download task is paused. If task is already paused the action will be ignored
42      */
43     @objc optional func downloadRequestDidPaused(_ downloadModel: MZDownloadModel, index: Int)
44     /**A delegate method called each time whenever any download task is resumed. If task is already downloading the action will be ignored
45      */
46     @objc optional func downloadRequestDidResumed(_ downloadModel: MZDownloadModel, index: Int)
47     /**A delegate method called each time whenever any download task is resumed. If task is already downloading the action will be ignored
48      */
49     @objc optional func downloadRequestDidRetry(_ downloadModel: MZDownloadModel, index: Int)
50     /**A delegate method called each time whenever any download task is cancelled by the user
51      */
52     @objc optional func downloadRequestCanceled(_ downloadModel: MZDownloadModel, index: Int)
53     /**A delegate method called each time whenever any download task is finished successfully
54      */
55     @objc optional func downloadRequestFinished(_ downloadModel: MZDownloadModel, index: Int)
56     /**A delegate method called each time whenever any download task is failed due to any reason
57      */
58     @objc optional func downloadRequestDidFailedWithError(_ error: NSError, downloadModel: MZDownloadModel, index: Int)
59     /**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
60      */
61     @objc optional func downloadRequestDestinationDoestNotExists(_ downloadModel: MZDownloadModel, index: Int, location: URL)
62     
63 }
64
65 open class MZDownloadManager: NSObject {
66     
67     fileprivate var sessionManager: URLSession!
68     
69     fileprivate var backgroundSessionCompletionHandler: (() -> Void)?
70     
71     fileprivate let TaskDescFileNameIndex = 0
72     fileprivate let TaskDescFileURLIndex = 1
73     fileprivate let TaskDescFileDestinationIndex = 2
74     
75     fileprivate weak var delegate: MZDownloadManagerDelegate?
76     
77     open var downloadingArray: [MZDownloadModel] = []
78     
79     public convenience init(session sessionIdentifer: String, delegate: MZDownloadManagerDelegate) {
80         self.init()
81         
82         self.delegate = delegate
83         self.sessionManager = backgroundSession(identifier: sessionIdentifer)
84         self.populateOtherDownloadTasks()
85     }
86     
87     public convenience init(session sessionIdentifer: String, delegate: MZDownloadManagerDelegate, completion: (() -> Void)?) {
88         self.init(session: sessionIdentifer, delegate: delegate)
89         self.backgroundSessionCompletionHandler = completion
90     }
91     
92     fileprivate func backgroundSession(identifier: String) -> URLSession {
93         let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: identifier)
94         let session = Foundation.URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil)
95         return session
96     }
97 }
98
99 // MARK: Private Helper functions
100
101 extension MZDownloadManager {
102     
103     fileprivate func downloadTasks() -> [URLSessionDownloadTask] {
104         var tasks: [URLSessionDownloadTask] = []
105         let semaphore : DispatchSemaphore = DispatchSemaphore(value: 0)
106         sessionManager.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
107             tasks = downloadTasks
108             semaphore.signal()
109         }
110         
111         let _ = semaphore.wait(timeout: DispatchTime.distantFuture)
112         
113         debugPrint("MZDownloadManager: pending tasks \(tasks)")
114         
115         return tasks
116     }
117     
118     fileprivate func populateOtherDownloadTasks() {
119         
120         let downloadTasks = self.downloadTasks()
121         
122         for downloadTask in downloadTasks {
123             let taskDescComponents: [String] = downloadTask.taskDescription!.components(separatedBy: ",")
124             let fileName = taskDescComponents[TaskDescFileNameIndex]
125             let fileURL = taskDescComponents[TaskDescFileURLIndex]
126             let destinationPath = taskDescComponents[TaskDescFileDestinationIndex]
127             
128             let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
129             downloadModel.task = downloadTask
130             downloadModel.startTime = Date()
131             
132             if downloadTask.state == .running {
133                 downloadModel.status = TaskStatus.downloading.description()
134                 downloadingArray.append(downloadModel)
135             } else if(downloadTask.state == .suspended) {
136                 downloadModel.status = TaskStatus.paused.description()
137                 downloadingArray.append(downloadModel)
138             } else {
139                 downloadModel.status = TaskStatus.failed.description()
140             }
141         }
142     }
143     
144     fileprivate func isValidResumeData(_ resumeData: Data?) -> Bool {
145         
146         guard resumeData != nil || resumeData?.count > 0 else {
147             return false
148         }
149         
150         do {
151             var resumeDictionary : AnyObject!
152             resumeDictionary = try PropertyListSerialization.propertyList(from: resumeData!, options: PropertyListSerialization.MutabilityOptions(), format: nil) as AnyObject!
153             var localFilePath = (resumeDictionary?["NSURLSessionResumeInfoLocalPath"] as? String)
154             
155             if localFilePath == nil || localFilePath?.count < 1 {
156                 localFilePath = (NSTemporaryDirectory() as String) + (resumeDictionary["NSURLSessionResumeInfoTempFileName"] as! String)
157             }
158             
159             let fileManager : FileManager! = FileManager.default
160             debugPrint("resume data file exists: \(fileManager.fileExists(atPath: localFilePath! as String))")
161             return fileManager.fileExists(atPath: localFilePath! as String)
162         } catch let error as NSError {
163             debugPrint("resume data is nil: \(error)")
164             return false
165         }
166     }
167 }
168
169 extension MZDownloadManager: URLSessionDownloadDelegate {
170     
171     public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
172         for (index, downloadModel) in self.downloadingArray.enumerated() {
173             if downloadTask.isEqual(downloadModel.task) {
174                 DispatchQueue.main.async(execute: { () -> Void in
175                     
176                     let receivedBytesCount = Double(downloadTask.countOfBytesReceived)
177                     let totalBytesCount = Double(downloadTask.countOfBytesExpectedToReceive)
178                     let progress = Float(receivedBytesCount / totalBytesCount)
179                     
180                     let taskStartedDate = downloadModel.startTime!
181                     let timeInterval = taskStartedDate.timeIntervalSinceNow
182                     let downloadTime = TimeInterval(-1 * timeInterval)
183                     
184                     let speed = Float(totalBytesWritten) / Float(downloadTime)
185                     
186                     let remainingContentLength = totalBytesExpectedToWrite - totalBytesWritten
187                     
188                     let remainingTime = remainingContentLength / Int64(speed)
189                     let hours = Int(remainingTime) / 3600
190                     let minutes = (Int(remainingTime) - hours * 3600) / 60
191                     let seconds = Int(remainingTime) - hours * 3600 - minutes * 60
192                     
193                     let totalFileSize = MZUtility.calculateFileSizeInUnit(totalBytesExpectedToWrite)
194                     let totalFileSizeUnit = MZUtility.calculateUnit(totalBytesExpectedToWrite)
195                     
196                     let downloadedFileSize = MZUtility.calculateFileSizeInUnit(totalBytesWritten)
197                     let downloadedSizeUnit = MZUtility.calculateUnit(totalBytesWritten)
198                     
199                     let speedSize = MZUtility.calculateFileSizeInUnit(Int64(speed))
200                     let speedUnit = MZUtility.calculateUnit(Int64(speed))
201                     
202                     downloadModel.remainingTime = (hours, minutes, seconds)
203                     downloadModel.file = (totalFileSize, totalFileSizeUnit as String)
204                     downloadModel.downloadedFile = (downloadedFileSize, downloadedSizeUnit as String)
205                     downloadModel.speed = (speedSize, speedUnit as String)
206                     downloadModel.progress = progress
207                     
208                     self.downloadingArray[index] = downloadModel
209                     
210                     self.delegate?.downloadRequestDidUpdateProgress(downloadModel, index: index)
211                 })
212                 break
213             }
214         }
215     }
216     
217     public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
218         for (index, downloadModel) in downloadingArray.enumerated() {
219             if downloadTask.isEqual(downloadModel.task) {
220                 let fileName = downloadModel.fileName as NSString
221                 let basePath = downloadModel.destinationPath == "" ? MZUtility.baseFilePath : downloadModel.destinationPath
222                 let destinationPath = (basePath as NSString).appendingPathComponent(fileName as String)
223                 
224                 let fileManager : FileManager = FileManager.default
225                 
226                 //If all set just move downloaded file to the destination
227                 if fileManager.fileExists(atPath: basePath) {
228                     let fileURL = URL(fileURLWithPath: destinationPath as String)
229                     debugPrint("directory path = \(destinationPath)")
230                     
231                     do {
232                         try fileManager.moveItem(at: location, to: fileURL)
233                     } catch let error as NSError {
234                         debugPrint("Error while moving downloaded file to destination path:\(error)")
235                         DispatchQueue.main.async(execute: { () -> Void in
236                             self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
237                         })
238                     }
239                 } else {
240                     //Opportunity to handle the folder doesnot exists error appropriately.
241                     //Move downloaded file to destination
242                     //Delegate will be called on the session queue
243                     //Otherwise blindly give error Destination folder does not exists
244                     
245                     if let _ = self.delegate?.downloadRequestDestinationDoestNotExists {
246                         self.delegate?.downloadRequestDestinationDoestNotExists?(downloadModel, index: index, location: location)
247                     } else {
248                         let error = NSError(domain: "FolderDoesNotExist", code: 404, userInfo: [NSLocalizedDescriptionKey : "Destination folder does not exists"])
249                         self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
250                     }
251                 }
252                 
253                 break
254             }
255         }
256     }
257     
258     public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
259         debugPrint("task id: \(task.taskIdentifier)")
260         /***** Any interrupted tasks due to any reason will be populated in failed state after init *****/
261         
262         DispatchQueue.main.async {
263             
264             let err = error as NSError?
265             
266             if (err?.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? NSNumber)?.intValue == NSURLErrorCancelledReasonUserForceQuitApplication || (err?.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? NSNumber)?.intValue == NSURLErrorCancelledReasonBackgroundUpdatesDisabled {
267                 
268                 let downloadTask = task as! URLSessionDownloadTask
269                 let taskDescComponents: [String] = downloadTask.taskDescription!.components(separatedBy: ",")
270                 let fileName = taskDescComponents[self.TaskDescFileNameIndex]
271                 let fileURL = taskDescComponents[self.TaskDescFileURLIndex]
272                 let destinationPath = taskDescComponents[self.TaskDescFileDestinationIndex]
273                 
274                 let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
275                 downloadModel.status = TaskStatus.failed.description()
276                 downloadModel.task = downloadTask
277                 
278                 let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
279                 
280                 var newTask = downloadTask
281                 if self.isValidResumeData(resumeData) == true {
282                     newTask = self.sessionManager.downloadTask(withResumeData: resumeData!)
283                 } else {
284                     newTask = self.sessionManager.downloadTask(with: URL(string: fileURL as String)!)
285                 }
286                 
287                 newTask.taskDescription = downloadTask.taskDescription
288                 downloadModel.task = newTask
289                 
290                 self.downloadingArray.append(downloadModel)
291                 
292                 self.delegate?.downloadRequestDidPopulatedInterruptedTasks(self.downloadingArray)
293                 
294             } else {
295                 for(index, object) in self.downloadingArray.enumerated() {
296                     let downloadModel = object
297                     if task.isEqual(downloadModel.task) {
298                         if err?.code == NSURLErrorCancelled || err == nil {
299                             self.downloadingArray.remove(at: index)
300                             
301                             if err == nil {
302                                 self.delegate?.downloadRequestFinished?(downloadModel, index: index)
303                             } else {
304                                 self.delegate?.downloadRequestCanceled?(downloadModel, index: index)
305                             }
306                             
307                         } else {
308                             let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
309                             var newTask = task
310                             if self.isValidResumeData(resumeData) == true {
311                                 newTask = self.sessionManager.downloadTask(withResumeData: resumeData!)
312                             } else {
313                                 newTask = self.sessionManager.downloadTask(with: URL(string: downloadModel.fileURL)!)
314                             }
315                             
316                             newTask.taskDescription = task.taskDescription
317                             downloadModel.status = TaskStatus.failed.description()
318                             downloadModel.task = newTask as? URLSessionDownloadTask
319                             
320                             self.downloadingArray[index] = downloadModel
321                             
322                             if let error = err {
323                                 self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
324                             } else {
325                                 let error: NSError = NSError(domain: "MZDownloadManagerDomain", code: 1000, userInfo: [NSLocalizedDescriptionKey : "Unknown error occurred"])
326                                 
327                                 self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
328                             }
329                         }
330                         break;
331                     }
332                 }
333             }
334         }
335     }
336     
337     public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
338         if let backgroundCompletion = self.backgroundSessionCompletionHandler {
339             DispatchQueue.main.async(execute: {
340                 backgroundCompletion()
341             })
342         }
343         debugPrint("All tasks are finished")
344     }
345 }
346
347 //MARK: Public Helper Functions
348
349 extension MZDownloadManager {
350     
351     @objc public func addDownloadTask(_ fileName: String, fileURL: String, destinationPath: String) {
352         
353         let url = URL(string: fileURL as String)!
354         let request = URLRequest(url: url)
355         
356         let downloadTask = sessionManager.downloadTask(with: request)
357         downloadTask.taskDescription = [fileName, fileURL, destinationPath].joined(separator: ",")
358         downloadTask.resume()
359         
360         debugPrint("session manager:\(sessionManager) url:\(url) request:\(request)")
361         
362         let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
363         downloadModel.startTime = Date()
364         downloadModel.status = TaskStatus.downloading.description()
365         downloadModel.task = downloadTask
366         
367         downloadingArray.append(downloadModel)
368         delegate?.downloadRequestStarted?(downloadModel, index: downloadingArray.count - 1)
369     }
370     
371     @objc public func addDownloadTask(_ fileName: String, fileURL: String) {
372         addDownloadTask(fileName, fileURL: fileURL, destinationPath: "")
373     }
374     
375     @objc public func pauseDownloadTaskAtIndex(_ index: Int) {
376         
377         let downloadModel = downloadingArray[index]
378         
379         guard downloadModel.status != TaskStatus.paused.description() else {
380             return
381         }
382         
383         let downloadTask = downloadModel.task
384         downloadTask!.suspend()
385         downloadModel.status = TaskStatus.paused.description()
386         downloadModel.startTime = Date()
387         
388         downloadingArray[index] = downloadModel
389         
390         delegate?.downloadRequestDidPaused?(downloadModel, index: index)
391     }
392     
393     @objc public func resumeDownloadTaskAtIndex(_ index: Int) {
394         
395         let downloadModel = downloadingArray[index]
396         
397         guard downloadModel.status != TaskStatus.downloading.description() else {
398             return
399         }
400         
401         let downloadTask = downloadModel.task
402         downloadTask!.resume()
403         downloadModel.status = TaskStatus.downloading.description()
404         
405         downloadingArray[index] = downloadModel
406         
407         delegate?.downloadRequestDidResumed?(downloadModel, index: index)
408     }
409     
410     @objc public func retryDownloadTaskAtIndex(_ index: Int) {
411         let downloadModel = downloadingArray[index]
412         
413         guard downloadModel.status != TaskStatus.downloading.description() else {
414             return
415         }
416         
417         let downloadTask = downloadModel.task
418         
419         downloadTask!.resume()
420         downloadModel.status = TaskStatus.downloading.description()
421         downloadModel.startTime = Date()
422         downloadModel.task = downloadTask
423         
424         downloadingArray[index] = downloadModel
425     }
426     
427     @objc public func cancelTaskAtIndex(_ index: Int) {
428         let downloadInfo = downloadingArray[index]
429         let downloadTask = downloadInfo.task
430         downloadTask!.cancel()
431     }
432     
433     @objc public func presentNotificationForDownload(_ notifAction: String, notifBody: String) {
434         let application = UIApplication.shared
435         let applicationState = application.applicationState
436         
437         if applicationState == UIApplicationState.background {
438             let localNotification = UILocalNotification()
439             localNotification.alertBody = notifBody
440             localNotification.alertAction = notifAction
441             localNotification.soundName = UILocalNotificationDefaultSoundName
442             localNotification.applicationIconBadgeNumber += 1
443             application.presentLocalNotificationNow(localNotification)
444         }
445     }
446 }