session-ios/Signal/src/network/GiphyDownloader.swift

773 lines
28 KiB
Swift
Raw Normal View History

//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
import ObjectiveC
2017-10-01 00:22:08 +02:00
// Stills should be loaded before full GIFs.
enum GiphyRequestPriority {
case low, high
}
2017-10-27 21:16:02 +02:00
enum GiphyAssetSegmentState: UInt {
case waiting
case active
case complete
case failed
}
class GiphyAssetSegment {
2017-10-27 21:55:04 +02:00
let TAG = "[GiphyAssetSegment]"
2017-10-27 21:16:02 +02:00
public let index: UInt
public let segmentStart: UInt
public let segmentLength: UInt
2017-10-27 21:55:04 +02:00
public let redundantLength: UInt
2017-10-27 21:16:02 +02:00
public var state: GiphyAssetSegmentState = .waiting
private var datas = [Data]()
init(index: UInt,
segmentStart: UInt,
2017-10-27 21:55:04 +02:00
segmentLength: UInt,
redundantLength: UInt) {
2017-10-27 21:16:02 +02:00
self.index = index
self.segmentStart = segmentStart
self.segmentLength = segmentLength
2017-10-27 21:55:04 +02:00
self.redundantLength = redundantLength
2017-10-27 21:16:02 +02:00
}
public func totalDataSize() -> UInt {
var result: UInt = 0
for data in datas {
result += UInt(data.count)
}
return result
}
2017-10-27 21:55:04 +02:00
public func append(data: Data) {
datas.append(data)
}
2017-10-27 21:16:02 +02:00
public func mergeData(assetData: NSMutableData) {
2017-10-27 21:55:04 +02:00
// In some cases the last two segments will overlap.
// In that case, we only want to append the non-overlapping
// tail of the last segment.
var bytesToIgnore = Int(redundantLength)
2017-10-27 21:16:02 +02:00
for data in datas {
2017-10-27 21:55:04 +02:00
if data.count <= bytesToIgnore {
bytesToIgnore -= data.count
} else if bytesToIgnore > 0 {
let range = NSMakeRange(bytesToIgnore, data.count - bytesToIgnore)
Logger.verbose("\(TAG) bytesToIgnore: \(bytesToIgnore), data.count: \(data.count), range: \(range.location), \(range.length).")
let subdata = (data as NSData).subdata(with: range)
Logger.verbose("\(TAG) subdata: \(subdata.count).")
assetData.append(subdata)
bytesToIgnore = 0
} else {
assetData.append(data)
}
2017-10-27 21:16:02 +02:00
}
}
}
enum GiphyAssetRequestState: UInt {
case waiting
2017-10-27 22:35:30 +02:00
case requestingSize
2017-10-27 21:16:02 +02:00
case active
case complete
case failed
}
// Represents a request to download a GIF.
//
// Should be cancelled if no longer necessary.
@objc class GiphyAssetRequest: NSObject {
static let TAG = "[GiphyAssetRequest]"
2017-10-27 21:16:02 +02:00
let TAG = "[GiphyAssetRequest]"
let rendition: GiphyRendition
let priority: GiphyRequestPriority
// Exactly one of success or failure should be called once,
// on the main thread _unless_ this request is cancelled before
// the request succeeds or fails.
private var success: ((GiphyAssetRequest?, GiphyAsset) -> Void)?
private var failure: ((GiphyAssetRequest) -> Void)?
var wasCancelled = false
// This property is an internal implementation detail of the download process.
var assetFilePath: String?
2017-10-27 21:16:02 +02:00
private var segments = [GiphyAssetSegment]()
private var assetData = NSMutableData()
public var state: GiphyAssetRequestState = .waiting
2017-10-27 22:35:30 +02:00
public var contentLength: Int = 0 {
didSet {
AssertIsOnMainThread()
assert(oldValue == 0)
assert(contentLength > 0)
createSegments()
}
}
2017-10-27 21:16:02 +02:00
init(rendition: GiphyRendition,
priority: GiphyRequestPriority,
success:@escaping ((GiphyAssetRequest?, GiphyAsset) -> Void),
2017-10-27 21:16:02 +02:00
failure:@escaping ((GiphyAssetRequest) -> Void)) {
self.rendition = rendition
self.priority = priority
self.success = success
self.failure = failure
2017-10-27 21:16:02 +02:00
super.init()
}
private func segmentSize() -> UInt {
2017-10-27 22:35:30 +02:00
let fileSize = UInt(contentLength)
2017-10-27 21:16:02 +02:00
guard fileSize > 0 else {
owsFail("\(TAG) rendition missing filesize")
requestDidFail()
return 0
}
let k1MB: UInt = 1024 * 1024
let k500KB: UInt = 500 * 1024
let k100KB: UInt = 100 * 1024
let k50KB: UInt = 50 * 1024
let k10KB: UInt = 10 * 1024
let k1KB: UInt = 1 * 1024
for segmentSize in [k1MB, k500KB, k100KB, k50KB, k10KB, k1KB ] {
if fileSize >= segmentSize {
return segmentSize
}
}
return fileSize
}
private func createSegments() {
let segmentLength = segmentSize()
guard segmentLength > 0 else {
return
}
2017-10-27 22:35:30 +02:00
let fileSize = UInt(contentLength)
2017-10-27 21:16:02 +02:00
var nextSegmentStart: UInt = 0
var index: UInt = 0
while nextSegmentStart < fileSize {
var segmentStart: UInt = nextSegmentStart
2017-10-27 21:55:04 +02:00
var redundantLength: UInt = 0
2017-10-27 21:16:02 +02:00
// The last segment may overlap the penultimate segment
// in order to keep the segment sizes uniform.
if segmentStart + segmentLength > fileSize {
2017-10-27 21:55:04 +02:00
redundantLength = segmentStart + segmentLength - fileSize
2017-10-27 21:16:02 +02:00
segmentStart = fileSize - segmentLength
}
segments.append(GiphyAssetSegment(index:index,
segmentStart:segmentStart,
2017-10-27 21:55:04 +02:00
segmentLength:segmentLength,
redundantLength:redundantLength))
2017-10-27 21:16:02 +02:00
nextSegmentStart = segmentStart + segmentLength
index += 1
}
}
private func firstSegmentWithState(state: GiphyAssetSegmentState) -> GiphyAssetSegment? {
for segment in segments {
guard segment.state != .failed else {
owsFail("\(TAG) unexpected failed segment.")
continue
}
if segment.state == state {
return segment
}
}
return nil
}
public func firstWaitingSegment() -> GiphyAssetSegment? {
return firstSegmentWithState(state:.waiting)
}
public func firstActiveSegment() -> GiphyAssetSegment? {
return firstSegmentWithState(state:.active)
}
2017-10-27 21:55:04 +02:00
public func mergeSegmentData(segment: GiphyAssetSegment) {
2017-10-27 21:16:02 +02:00
guard segment.totalDataSize() > 0 else {
owsFail("\(TAG) could not merge empty segment.")
return
}
2017-10-27 21:55:04 +02:00
guard segment.state == .complete else {
owsFail("\(TAG) could not merge incomplete segment.")
return
}
Logger.verbose("\(TAG) merging segment: \(segment.index) \(segment.segmentStart) \(segment.segmentLength) \(segment.redundantLength) \(rendition.url).")
Logger.verbose("\(TAG) before merge: \(assetData.length) \(rendition.url).")
segment.mergeData(assetData: assetData)
Logger.verbose("\(TAG) after merge: \(assetData.length) \(rendition.url).")
2017-10-27 21:16:02 +02:00
}
public func writeAssetToFile() -> GiphyAsset? {
2017-10-27 22:35:30 +02:00
Logger.verbose("\(TAG) writeAssetToFile: \(rendition.url).")
Logger.verbose("\(TAG) expected length: \(rendition.fileSize) \(contentLength).")
Logger.verbose("\(TAG) actual length: \(assetData.length).")
Logger.flush()
guard assetData.length == contentLength else {
2017-10-27 21:55:04 +02:00
owsFail("\(TAG) asset data has unexpected length.")
return nil
}
2017-10-27 21:16:02 +02:00
guard assetData.length > 0 else {
owsFail("\(TAG) could not write empty asset to disk.")
return nil
}
// We write assets to the temporary directory so that iOS can clean them up.
// We try to eagerly clean up these assets when they are no longer in use.
let dirPath = NSTemporaryDirectory()
let fileExtension = rendition.fileExtension
let fileName = (NSUUID().uuidString as NSString).appendingPathExtension(fileExtension)!
let filePath = (dirPath as NSString).appendingPathComponent(fileName)
2017-10-27 22:35:30 +02:00
Logger.verbose("\(TAG) filePath: \(filePath).")
2017-10-27 21:16:02 +02:00
let success = assetData.write(toFile: filePath, atomically: true)
guard success else {
owsFail("\(TAG) could not write asset to disk.")
return nil
}
let asset = GiphyAsset(rendition: rendition, filePath : filePath)
return asset
}
public func cancel() {
AssertIsOnMainThread()
wasCancelled = true
// Don't call the callbacks if the request is cancelled.
clearCallbacks()
}
private func clearCallbacks() {
AssertIsOnMainThread()
success = nil
failure = nil
}
public func requestDidSucceed(asset: GiphyAsset) {
AssertIsOnMainThread()
success?(self, asset)
// Only one of the callbacks should be called, and only once.
clearCallbacks()
}
public func requestDidFail() {
AssertIsOnMainThread()
failure?(self)
// Only one of the callbacks should be called, and only once.
clearCallbacks()
}
}
// Represents a downloaded gif asset.
//
// The blob on disk is cleaned up when this instance is deallocated,
// so consumers of this resource should retain a strong reference to
// this instance as long as they are using the asset.
@objc class GiphyAsset: NSObject {
static let TAG = "[GiphyAsset]"
let rendition: GiphyRendition
let filePath: String
init(rendition: GiphyRendition,
filePath: String) {
self.rendition = rendition
self.filePath = filePath
}
deinit {
// Clean up on the asset on disk.
let filePathCopy = filePath
DispatchQueue.global().async {
do {
let fileManager = FileManager.default
try fileManager.removeItem(atPath:filePathCopy)
} catch let error as NSError {
owsFail("\(GiphyAsset.TAG) file cleanup failed: \(filePathCopy), \(error)")
}
}
}
}
// A simple LRU cache bounded by the number of entries.
class LRUCache<KeyType: Hashable & Equatable, ValueType> {
private var cacheMap = [KeyType: ValueType]()
private var cacheOrder = [KeyType]()
private let maxSize: Int
init(maxSize: Int) {
self.maxSize = maxSize
}
public func get(key: KeyType) -> ValueType? {
guard let value = cacheMap[key] else {
return nil
}
// Update cache order.
cacheOrder = cacheOrder.filter { $0 != key }
cacheOrder.append(key)
return value
}
public func set(key: KeyType, value: ValueType) {
cacheMap[key] = value
// Update cache order.
cacheOrder = cacheOrder.filter { $0 != key }
cacheOrder.append(key)
while cacheOrder.count > maxSize {
guard let staleKey = cacheOrder.first else {
owsFail("Cache ordering unexpectedly empty")
return
}
cacheOrder.removeFirst()
cacheMap.removeValue(forKey:staleKey)
}
}
}
2017-10-01 00:22:08 +02:00
private var URLSessionTaskGiphyAssetRequest: UInt8 = 0
2017-10-27 21:16:02 +02:00
private var URLSessionTaskGiphyAssetSegment: UInt8 = 0
// This extension is used to punch an asset request onto a download task.
extension URLSessionTask {
var assetRequest: GiphyAssetRequest {
get {
2017-10-01 00:22:08 +02:00
return objc_getAssociatedObject(self, &URLSessionTaskGiphyAssetRequest) as! GiphyAssetRequest
}
set {
2017-10-01 00:22:08 +02:00
objc_setAssociatedObject(self, &URLSessionTaskGiphyAssetRequest, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
2017-10-27 21:16:02 +02:00
var assetSegment: GiphyAssetSegment {
get {
return objc_getAssociatedObject(self, &URLSessionTaskGiphyAssetSegment) as! GiphyAssetSegment
}
set {
objc_setAssociatedObject(self, &URLSessionTaskGiphyAssetSegment, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
2017-10-27 21:16:02 +02:00
@objc class GiphyDownloader: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate {
// MARK: - Properties
let TAG = "[GiphyDownloader]"
2017-10-01 20:54:39 +02:00
static let sharedInstance = GiphyDownloader()
// A private queue used for download task callbacks.
private let operationQueue = OperationQueue()
// Force usage as a singleton
override private init() {}
deinit {
NotificationCenter.default.removeObserver(self)
}
private let kGiphyBaseURL = "https://api.giphy.com/"
private func giphyDownloadSession() -> URLSession? {
2017-10-20 22:21:52 +02:00
let configuration = GiphyAPI.giphySessionConfiguration()
configuration.urlCache = nil
configuration.requestCachePolicy = .reloadIgnoringCacheData
2017-10-20 22:21:52 +02:00
let session = URLSession(configuration:configuration,
delegate:self, delegateQueue:operationQueue)
return session
}
2017-10-01 00:22:08 +02:00
// 100 entries of which at least half will probably be stills.
// Actual animated GIFs will usually be less than 3 MB so the
// max size of the cache on disk should be ~150 MB. Bear in mind
// that assets are not always deleted on disk as soon as they are
2017-10-01 00:22:08 +02:00
// evacuated from the cache; if a cache consumer (e.g. view) is
// still using the asset, the asset won't be deleted on disk until
// it is no longer in use.
private var assetMap = LRUCache<NSURL, GiphyAsset>(maxSize:100)
// TODO: We could use a proper queue, e.g. implemented with a linked
// list.
private var assetRequestQueue = [GiphyAssetRequest]()
private let kMaxAssetRequestCount = 3
2017-10-27 21:16:02 +02:00
// private var activeAssetRequests = Set<GiphyAssetRequest>()
// The success and failure callbacks are always called on main queue.
//
2017-10-27 21:16:02 +02:00
// The success callbacks may be called synchronously on cache hit, in
// which case the GiphyAssetRequest parameter will be nil.
2017-10-01 00:22:08 +02:00
public func requestAsset(rendition: GiphyRendition,
priority: GiphyRequestPriority,
success:@escaping ((GiphyAssetRequest?, GiphyAsset) -> Void),
failure:@escaping ((GiphyAssetRequest) -> Void)) -> GiphyAssetRequest? {
AssertIsOnMainThread()
if let asset = assetMap.get(key:rendition.url) {
2017-10-01 00:22:08 +02:00
// Synchronous cache hit.
success(nil, asset)
return nil
}
2017-10-01 00:22:08 +02:00
// Cache miss.
//
// Asset requests are done queued and performed asynchronously.
let assetRequest = GiphyAssetRequest(rendition:rendition,
priority:priority,
2017-10-01 00:22:08 +02:00
success:success,
failure:failure)
assetRequestQueue.append(assetRequest)
2017-10-27 21:16:02 +02:00
processRequestQueue()
return assetRequest
}
public func cancelAllRequests() {
2017-10-27 21:16:02 +02:00
AssertIsOnMainThread()
self.assetRequestQueue.forEach { $0.cancel() }
2017-10-27 21:16:02 +02:00
self.assetRequestQueue = []
}
private func segmentRequestDidSucceed(assetRequest: GiphyAssetRequest, assetSegment: GiphyAssetSegment) {
Logger.verbose("\(self.TAG) segment request succeeded \(assetRequest.rendition.url), \(assetSegment.index), \(assetSegment.segmentStart), \(assetSegment.segmentLength)")
DispatchQueue.main.async {
assetSegment.state = .complete
// TODO: Should we move this merge off main thread?
2017-10-27 21:55:04 +02:00
assetRequest.mergeSegmentData(segment : assetSegment)
2017-10-27 21:16:02 +02:00
// If the asset request has completed all of its segments,
// try to write the asset to file.
if assetRequest.firstWaitingSegment() == nil {
assetRequest.state = .complete
// Move write off main thread.
DispatchQueue.global().async {
guard let asset = assetRequest.writeAssetToFile() else {
self.segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return
}
self.assetRequestDidSucceed(assetRequest: assetRequest, asset: asset)
}
} else {
self.processRequestQueue()
}
}
}
private func assetRequestDidSucceed(assetRequest: GiphyAssetRequest, asset: GiphyAsset) {
2017-10-27 21:16:02 +02:00
Logger.verbose("\(self.TAG) asset request succeeded \(assetRequest.rendition.url)")
DispatchQueue.main.async {
self.assetMap.set(key:assetRequest.rendition.url, value:asset)
2017-10-27 21:16:02 +02:00
self.removeAssetRequestFromQueue(assetRequest:assetRequest)
assetRequest.requestDidSucceed(asset:asset)
2017-10-27 21:16:02 +02:00
self.processRequestQueue()
}
}
// TODO: If we wanted to implement segment retry, we'll need to add
// a segmentRequestDidFail() method.
private func segmentRequestDidFail(assetRequest: GiphyAssetRequest, assetSegment: GiphyAssetSegment) {
Logger.verbose("\(self.TAG) segment request failed \(assetRequest.rendition.url), \(assetSegment.index), \(assetSegment.segmentStart), \(assetSegment.segmentLength)")
DispatchQueue.main.async {
assetSegment.state = .failed
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
}
}
private func assetRequestDidFail(assetRequest: GiphyAssetRequest) {
2017-10-27 21:16:02 +02:00
Logger.verbose("\(self.TAG) asset request failed \(assetRequest.rendition.url)")
DispatchQueue.main.async {
2017-10-27 21:16:02 +02:00
self.removeAssetRequestFromQueue(assetRequest:assetRequest)
assetRequest.requestDidFail()
2017-10-27 21:16:02 +02:00
self.processRequestQueue()
}
}
private func removeAssetRequestFromQueue(assetRequest: GiphyAssetRequest) {
AssertIsOnMainThread()
guard assetRequestQueue.contains(assetRequest) else {
Logger.warn("\(TAG) could not remove asset request from queue: \(assetRequest.rendition.url)")
return
}
2017-10-27 21:16:02 +02:00
assetRequestQueue = assetRequestQueue.filter { $0 != assetRequest }
}
2017-10-27 21:16:02 +02:00
// Start a request if necessary, complete asset requests if possible.
private func processRequestQueue() {
AssertIsOnMainThread()
DispatchQueue.main.async {
guard let assetRequest = self.popNextAssetRequest() else {
return
}
guard !assetRequest.wasCancelled else {
// Discard the cancelled asset request and try again.
2017-10-27 21:16:02 +02:00
self.processRequestQueue()
return
}
guard UIApplication.shared.applicationState == .active else {
// If app is not active, fail the asset request.
2017-10-27 21:16:02 +02:00
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
2017-10-27 21:16:02 +02:00
self.processRequestQueue()
return
}
if let asset = self.assetMap.get(key:assetRequest.rendition.url) {
// Deferred cache hit, avoids re-downloading assets that were
// downloaded while this request was queued.
2017-10-27 21:16:02 +02:00
assetRequest.state = .complete
self.assetRequestDidSucceed(assetRequest : assetRequest, asset: asset)
return
}
guard let downloadSession = self.giphyDownloadSession() else {
owsFail("\(self.TAG) Couldn't create session manager.")
2017-10-27 21:16:02 +02:00
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
return
}
2017-10-27 22:35:30 +02:00
if assetRequest.state == .waiting {
// If asset request hasn't yet determined the resource size,
// try to do so now.
assetRequest.state = .requestingSize
var request = URLRequest(url: assetRequest.rendition.url as URL)
// var request = NSMutableURLRequest(URL: NSURL(string: urlString)!)
request.httpMethod = "HEAD"
// var session = NSURLSession.sharedSession()
// var error: NSError?
var task = downloadSession.dataTask(with:request, completionHandler: { [weak self] _, response, error -> Void in
self?.handleAssetSizeResponse(assetRequest:assetRequest, response:response, error:error)
})
task.resume()
return
}
2017-10-01 00:22:08 +02:00
// Start a download task.
2017-10-27 21:16:02 +02:00
guard let assetSegment = assetRequest.firstWaitingSegment() else {
owsFail("\(self.TAG) queued asset request does not have a waiting segment.")
return
}
assetSegment.state = .active
assetRequest.state = .active
Logger.verbose("\(self.TAG) new segment request \(assetRequest.rendition.url), \(assetSegment.index), \(assetSegment.segmentStart), \(assetSegment.segmentLength)")
var request = URLRequest(url: assetRequest.rendition.url as URL)
let rangeHeaderValue = "bytes=\(assetSegment.segmentStart)-\(assetSegment.segmentStart + assetSegment.segmentLength - 1)"
Logger.verbose("\(self.TAG) rangeHeaderValue: \(rangeHeaderValue)")
request.addValue(rangeHeaderValue, forHTTPHeaderField: "Range")
let task = downloadSession.dataTask(with:request)
task.assetRequest = assetRequest
2017-10-27 21:16:02 +02:00
task.assetSegment = assetSegment
task.resume()
}
}
2017-10-27 22:35:30 +02:00
private func handleAssetSizeResponse(assetRequest: GiphyAssetRequest, response: URLResponse?, error: Error?) {
guard let httpResponse = response as? HTTPURLResponse else {
owsFail("\(self.TAG) Asset size response is invalid.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
return
}
guard let contentLengthString = httpResponse.allHeaderFields["Content-Length"] as? String else {
owsFail("\(self.TAG) Asset size response is missing content length.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
return
}
guard let contentLength = Int(contentLengthString) else {
owsFail("\(self.TAG) Asset size response has unparsable content length.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
return
}
guard contentLength > 0 else {
owsFail("\(self.TAG) Asset size response has invalid content length.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
return
}
DispatchQueue.main.async {
assetRequest.contentLength = contentLength
assetRequest.state = .active
self.processRequestQueue()
}
}
private func popNextAssetRequest() -> GiphyAssetRequest? {
AssertIsOnMainThread()
2017-10-01 20:54:39 +02:00
// Prefer the first "high" priority request;
2017-10-01 00:22:08 +02:00
// fall back to the first "low" priority request.
2017-10-27 21:16:02 +02:00
var activeAssetRequestsCount = 0
for priority in [GiphyRequestPriority.high, GiphyRequestPriority.low] {
2017-10-27 21:16:02 +02:00
for assetRequest in assetRequestQueue where assetRequest.priority == priority {
2017-10-27 22:35:30 +02:00
switch assetRequest.state {
case .waiting:
break
case .requestingSize:
activeAssetRequestsCount += 1
continue
case .active:
break
case .complete:
continue
case .failed:
2017-10-27 21:16:02 +02:00
continue
}
guard assetRequest.firstActiveSegment() == nil else {
activeAssetRequestsCount += 1
// Ensure that only N requests are active at a time.
guard activeAssetRequestsCount < self.kMaxAssetRequestCount else {
return nil
}
2017-10-01 20:54:39 +02:00
continue
}
2017-10-01 20:54:39 +02:00
return assetRequest
}
}
return nil
}
// MARK: URLSessionDataDelegate
@nonobjc
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
completionHandler(.allow)
}
2017-10-27 21:16:02 +02:00
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let assetRequest = dataTask.assetRequest
let assetSegment = dataTask.assetSegment
Logger.verbose("\(TAG) session dataTask didReceive: \(data.count) \(assetRequest.rendition.url)")
guard !assetRequest.wasCancelled else {
dataTask.cancel()
segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return
}
assetSegment.append(data:data)
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Swift.Void) {
completionHandler(nil)
}
// MARK: URLSessionTaskDelegate
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
2017-10-27 21:16:02 +02:00
// owsFail("\(TAG) session task didCompleteWithError \(error)")
Logger.verbose("\(TAG) session task didCompleteWithError \(error)")
let assetRequest = task.assetRequest
2017-10-27 21:16:02 +02:00
let assetSegment = task.assetSegment
guard !assetRequest.wasCancelled else {
task.cancel()
2017-10-27 21:16:02 +02:00
segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return
}
if let error = error {
Logger.error("\(TAG) download failed with error: \(error)")
2017-10-27 21:16:02 +02:00
segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return
}
guard let httpResponse = task.response as? HTTPURLResponse else {
Logger.error("\(TAG) missing or unexpected response: \(task.response)")
2017-10-27 21:16:02 +02:00
segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return
}
let statusCode = httpResponse.statusCode
guard statusCode >= 200 && statusCode < 400 else {
Logger.error("\(TAG) response has invalid status code: \(statusCode)")
2017-10-27 21:16:02 +02:00
segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return
}
2017-10-27 21:16:02 +02:00
guard assetSegment.totalDataSize() == assetSegment.segmentLength else {
Logger.error("\(TAG) segment is missing data: \(statusCode)")
segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return
}
2017-10-27 21:16:02 +02:00
segmentRequestDidSucceed(assetRequest : assetRequest, assetSegment: assetSegment)
}
2017-10-27 21:16:02 +02:00
// MARK: URLSessionDownloadDelegate
2017-10-27 21:16:02 +02:00
// var animatedDataCount = [URLSessionDownloadTask: Int64]()
// var stillDataCount = [URLSessionDownloadTask: Int64]()
// var totalDataCount = [URLSessionDownloadTask: Int64]()
// public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
//
// owsFail("\(TAG) session downloadTask didWriteData")
//
//// // Log accumulated data usage in debug
//// if _isDebugAssertConfiguration() {
//// let assetRequest = downloadTask.assetRequest
////
//// totalDataCount[downloadTask] = totalBytesWritten
//// if assetRequest.rendition.isStill {
//// stillDataCount[downloadTask] = totalBytesWritten
//// } else {
//// animatedDataCount[downloadTask] = totalBytesWritten
//// }
////
//// let megabyteCount = { (dataCountMap: [URLSessionDownloadTask: Int64]) -> String in
//// let sum = dataCountMap.values.reduce(0, +)
//// let megabyteCount = Float(sum) / 1000 / 1000
//// return String(format: "%06.2f MB", megabyteCount)
//// }
//// Logger.info("\(TAG) Still bytes written: \(megabyteCount(stillDataCount))")
//// Logger.info("\(TAG) Animated bytes written: \(megabyteCount(animatedDataCount))")
//// Logger.info("\(TAG) Total bytes written: \(megabyteCount(totalDataCount))")
//// }
////
//// let assetRequest = downloadTask.assetRequest
//// guard !assetRequest.wasCancelled else {
//// downloadTask.cancel()
//// assetRequestDidFail(assetRequest:assetRequest)
//// return
//// }
// }
}