Merge branch 'charlesmchen/splitGifs' into release/2.18.0

pull/1/head
Michael Kirk 8 years ago
commit 1a6cb7da13

@ -148,6 +148,10 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
view.backgroundColor = UIColor.white view.backgroundColor = UIColor.white
// Block UIKit from adjust insets of collection view which screws up
// min/max scroll positions.
self.automaticallyAdjustsScrollViewInsets = false
// Search // Search
searchBar.searchBarStyle = .minimal searchBar.searchBarStyle = .minimal
searchBar.delegate = self searchBar.delegate = self

@ -10,11 +10,106 @@ enum GiphyRequestPriority {
case low, high case low, high
} }
enum GiphyAssetSegmentState: UInt {
case waiting
case downloading
case complete
case failed
}
class GiphyAssetSegment {
let TAG = "[GiphyAssetSegment]"
public let index: UInt
public let segmentStart: UInt
public let segmentLength: UInt
// The amount of the segment that is overlap.
// The overlap lies in the _first_ n bytes of the segment data.
public let redundantLength: UInt
// This state should only be accessed on the main thread.
public var state: GiphyAssetSegmentState = .waiting {
didSet {
AssertIsOnMainThread()
}
}
// This state is accessed off the main thread.
//
// * During downloads it will be accessed on the task delegate queue.
// * After downloads it will be accessed on a worker queue.
private var segmentData = Data()
// This state should only be accessed on the main thread.
public weak var task: URLSessionDataTask?
init(index: UInt,
segmentStart: UInt,
segmentLength: UInt,
redundantLength: UInt) {
self.index = index
self.segmentStart = segmentStart
self.segmentLength = segmentLength
self.redundantLength = redundantLength
}
public func totalDataSize() -> UInt {
return UInt(segmentData.count)
}
public func append(data: Data) {
guard state == .downloading else {
owsFail("\(TAG) appending data in invalid state: \(state)")
return
}
segmentData.append(data)
}
public func mergeData(assetData: inout Data) -> Bool {
guard state == .complete else {
owsFail("\(TAG) merging data in invalid state: \(state)")
return false
}
guard UInt(segmentData.count) == segmentLength else {
owsFail("\(TAG) segment data length: \(segmentData.count) doesn't match expected length: \(segmentLength)")
return false
}
// In some cases the last two segments will overlap.
// In that case, we only want to append the non-overlapping
// tail of the segment data.
let bytesToIgnore = Int(redundantLength)
if bytesToIgnore > 0 {
let range = NSMakeRange(bytesToIgnore, segmentData.count - bytesToIgnore)
let subdata = segmentData.subdata(in: range.location..<range.location + range.length)
assetData.append(subdata)
} else {
assetData.append(segmentData)
}
return true
}
}
enum GiphyAssetRequestState: UInt {
// Does not yet have content length.
case waiting
// Getting content length.
case requestingSize
// Has content length, ready for downloads or downloads in flight.
case active
// Success
case complete
// Failure
case failed
}
// Represents a request to download a GIF. // Represents a request to download a GIF.
// //
// Should be cancelled if no longer necessary. // Should be cancelled if no longer necessary.
@objc class GiphyAssetRequest: NSObject { @objc class GiphyAssetRequest: NSObject {
static let TAG = "[GiphyAssetRequest]" static let TAG = "[GiphyAssetRequest]"
let TAG = "[GiphyAssetRequest]"
let rendition: GiphyRendition let rendition: GiphyRendition
let priority: GiphyRequestPriority let priority: GiphyRequestPriority
@ -28,21 +123,188 @@ enum GiphyRequestPriority {
// This property is an internal implementation detail of the download process. // This property is an internal implementation detail of the download process.
var assetFilePath: String? var assetFilePath: String?
// This state should only be accessed on the main thread.
private var segments = [GiphyAssetSegment]()
public var state: GiphyAssetRequestState = .waiting
public var contentLength: Int = 0 {
didSet {
AssertIsOnMainThread()
assert(oldValue == 0)
assert(contentLength > 0)
createSegments()
}
}
public weak var contentLengthTask: URLSessionDataTask?
init(rendition: GiphyRendition, init(rendition: GiphyRendition,
priority: GiphyRequestPriority, priority: GiphyRequestPriority,
success:@escaping ((GiphyAssetRequest?, GiphyAsset) -> Void), success:@escaping ((GiphyAssetRequest?, GiphyAsset) -> Void),
failure:@escaping ((GiphyAssetRequest) -> Void) failure:@escaping ((GiphyAssetRequest) -> Void)) {
) {
self.rendition = rendition self.rendition = rendition
self.priority = priority self.priority = priority
self.success = success self.success = success
self.failure = failure self.failure = failure
super.init()
}
private func segmentSize() -> UInt {
AssertIsOnMainThread()
let contentLength = UInt(self.contentLength)
guard contentLength > 0 else {
owsFail("\(TAG) rendition missing contentLength")
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 contentLength >= segmentSize {
return segmentSize
}
}
return contentLength
}
private func createSegments() {
AssertIsOnMainThread()
let segmentLength = segmentSize()
guard segmentLength > 0 else {
return
}
let contentLength = UInt(self.contentLength)
var nextSegmentStart: UInt = 0
var index: UInt = 0
while nextSegmentStart < contentLength {
var segmentStart: UInt = nextSegmentStart
var redundantLength: UInt = 0
// The last segment may overlap the penultimate segment
// in order to keep the segment sizes uniform.
if segmentStart + segmentLength > contentLength {
redundantLength = segmentStart + segmentLength - contentLength
segmentStart = contentLength - segmentLength
}
let assetSegment = GiphyAssetSegment(index:index,
segmentStart:segmentStart,
segmentLength:segmentLength,
redundantLength:redundantLength)
segments.append(assetSegment)
nextSegmentStart = segmentStart + segmentLength
index += 1
}
}
private func firstSegmentWithState(state: GiphyAssetSegmentState) -> GiphyAssetSegment? {
AssertIsOnMainThread()
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? {
AssertIsOnMainThread()
return firstSegmentWithState(state:.waiting)
}
public func downloadingSegmentsCount() -> UInt {
AssertIsOnMainThread()
var result: UInt = 0
for segment in segments {
guard segment.state != .failed else {
owsFail("\(TAG) unexpected failed segment.")
continue
}
if segment.state == .downloading {
result += 1
}
}
return result
}
public func areAllSegmentsComplete() -> Bool {
AssertIsOnMainThread()
for segment in segments {
guard segment.state == .complete else {
return false
}
}
return true
}
public func writeAssetToFile(gifFolderPath: String) -> GiphyAsset? {
var assetData = Data()
for segment in segments {
guard segment.state == .complete else {
owsFail("\(TAG) unexpected incomplete segment.")
return nil
}
guard segment.totalDataSize() > 0 else {
owsFail("\(TAG) could not merge empty segment.")
return nil
}
guard segment.mergeData(assetData: &assetData) else {
owsFail("\(TAG) failed to merge segment data.")
return nil
}
}
guard assetData.count == contentLength else {
owsFail("\(TAG) asset data has unexpected length.")
return nil
}
guard assetData.count > 0 else {
owsFail("\(TAG) could not write empty asset to disk.")
return nil
}
let fileExtension = rendition.fileExtension
let fileName = (NSUUID().uuidString as NSString).appendingPathExtension(fileExtension)!
let filePath = (gifFolderPath as NSString).appendingPathComponent(fileName)
Logger.verbose("\(TAG) filePath: \(filePath).")
do {
try assetData.write(to: NSURL.fileURL(withPath:filePath), options: .atomicWrite)
let asset = GiphyAsset(rendition: rendition, filePath : filePath)
return asset
} catch let error as NSError {
owsFail("\(GiphyAsset.TAG) file write failed: \(filePath), \(error)")
return nil
}
} }
public func cancel() { public func cancel() {
AssertIsOnMainThread() AssertIsOnMainThread()
wasCancelled = true wasCancelled = true
contentLengthTask?.cancel()
contentLengthTask = nil
for segment in segments {
segment.task?.cancel()
segment.task = nil
}
// Don't call the callbacks if the request is cancelled. // Don't call the callbacks if the request is cancelled.
clearCallbacks() clearCallbacks()
@ -147,6 +409,7 @@ class LRUCache<KeyType: Hashable & Equatable, ValueType> {
} }
private var URLSessionTaskGiphyAssetRequest: UInt8 = 0 private var URLSessionTaskGiphyAssetRequest: UInt8 = 0
private var URLSessionTaskGiphyAssetSegment: UInt8 = 0
// This extension is used to punch an asset request onto a download task. // This extension is used to punch an asset request onto a download task.
extension URLSessionTask { extension URLSessionTask {
@ -158,9 +421,17 @@ extension URLSessionTask {
objc_setAssociatedObject(self, &URLSessionTaskGiphyAssetRequest, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_setAssociatedObject(self, &URLSessionTaskGiphyAssetRequest, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
} }
} }
var assetSegment: GiphyAssetSegment {
get {
return objc_getAssociatedObject(self, &URLSessionTaskGiphyAssetSegment) as! GiphyAssetSegment
}
set {
objc_setAssociatedObject(self, &URLSessionTaskGiphyAssetSegment, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
} }
@objc class GiphyDownloader: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate { @objc class GiphyDownloader: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate {
// MARK: - Properties // MARK: - Properties
@ -168,11 +439,16 @@ extension URLSessionTask {
static let sharedInstance = GiphyDownloader() static let sharedInstance = GiphyDownloader()
// A private queue used for download task callbacks. var gifFolderPath = ""
private let operationQueue = OperationQueue()
// Force usage as a singleton // Force usage as a singleton
override private init() {} override private init() {
AssertIsOnMainThread()
super.init()
ensureGifFolder()
}
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
@ -180,14 +456,18 @@ extension URLSessionTask {
private let kGiphyBaseURL = "https://api.giphy.com/" private let kGiphyBaseURL = "https://api.giphy.com/"
private func giphyDownloadSession() -> URLSession? { private lazy var giphyDownloadSession: URLSession = {
AssertIsOnMainThread()
let configuration = GiphyAPI.giphySessionConfiguration() let configuration = GiphyAPI.giphySessionConfiguration()
configuration.urlCache = nil configuration.urlCache = nil
configuration.requestCachePolicy = .reloadIgnoringCacheData configuration.requestCachePolicy = .reloadIgnoringCacheData
configuration.httpMaximumConnectionsPerHost = 10
let session = URLSession(configuration:configuration, let session = URLSession(configuration:configuration,
delegate:self, delegateQueue:operationQueue) delegate:self,
delegateQueue:nil)
return session return session
} }()
// 100 entries of which at least half will probably be stills. // 100 entries of which at least half will probably be stills.
// Actual animated GIFs will usually be less than 3 MB so the // Actual animated GIFs will usually be less than 3 MB so the
@ -200,12 +480,10 @@ extension URLSessionTask {
// TODO: We could use a proper queue, e.g. implemented with a linked // TODO: We could use a proper queue, e.g. implemented with a linked
// list. // list.
private var assetRequestQueue = [GiphyAssetRequest]() private var assetRequestQueue = [GiphyAssetRequest]()
private let kMaxAssetRequestCount = 3
private var activeAssetRequests = Set<GiphyAssetRequest>()
// The success and failure callbacks are always called on main queue. // The success and failure callbacks are always called on main queue.
// //
// The success callbacks may be called synchronously on cache hit, in // The success callbacks may be called synchronously on cache hit, in
// which case the GiphyAssetRequest parameter will be nil. // which case the GiphyAssetRequest parameter will be nil.
public func requestAsset(rendition: GiphyRendition, public func requestAsset(rendition: GiphyRendition,
priority: GiphyRequestPriority, priority: GiphyRequestPriority,
@ -215,6 +493,7 @@ extension URLSessionTask {
if let asset = assetMap.get(key:rendition.url) { if let asset = assetMap.get(key:rendition.url) {
// Synchronous cache hit. // Synchronous cache hit.
Logger.verbose("\(self.TAG) asset cache hit: \(rendition.url)")
success(nil, asset) success(nil, asset)
return nil return nil
} }
@ -222,98 +501,253 @@ extension URLSessionTask {
// Cache miss. // Cache miss.
// //
// Asset requests are done queued and performed asynchronously. // Asset requests are done queued and performed asynchronously.
Logger.verbose("\(self.TAG) asset cache miss: \(rendition.url)")
let assetRequest = GiphyAssetRequest(rendition:rendition, let assetRequest = GiphyAssetRequest(rendition:rendition,
priority:priority, priority:priority,
success:success, success:success,
failure:failure) failure:failure)
assetRequestQueue.append(assetRequest) assetRequestQueue.append(assetRequest)
startRequestIfNecessary() // Process the queue (which may start this request)
// asynchronously so that the caller has time to store
// a reference to the asset request returned by this
// method before its success/failure handler is called.
DispatchQueue.main.async {
self.processRequestQueue()
}
return assetRequest return assetRequest
} }
public func cancelAllRequests() { public func cancelAllRequests() {
AssertIsOnMainThread()
Logger.verbose("\(self.TAG) cancelAllRequests")
self.assetRequestQueue.forEach { $0.cancel() } self.assetRequestQueue.forEach { $0.cancel() }
self.assetRequestQueue = []
}
private func segmentRequestDidSucceed(assetRequest: GiphyAssetRequest, assetSegment: GiphyAssetSegment) {
DispatchQueue.main.async {
assetSegment.state = .complete
if assetRequest.areAllSegmentsComplete() {
// If the asset request has completed all of its segments,
// try to write the asset to file.
assetRequest.state = .complete
// Move write off main thread.
DispatchQueue.global().async {
guard let asset = assetRequest.writeAssetToFile(gifFolderPath:self.gifFolderPath) else {
self.segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return
}
self.assetRequestDidSucceed(assetRequest: assetRequest, asset: asset)
}
} else {
self.processRequestQueue()
}
}
} }
private func assetRequestDidSucceed(assetRequest: GiphyAssetRequest, asset: GiphyAsset) { private func assetRequestDidSucceed(assetRequest: GiphyAssetRequest, asset: GiphyAsset) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.assetMap.set(key:assetRequest.rendition.url, value:asset) self.assetMap.set(key:assetRequest.rendition.url, value:asset)
self.activeAssetRequests.remove(assetRequest) self.removeAssetRequestFromQueue(assetRequest:assetRequest)
assetRequest.requestDidSucceed(asset:asset) assetRequest.requestDidSucceed(asset:asset)
self.startRequestIfNecessary() }
}
// TODO: If we wanted to implement segment retry, we'll need to add
// a segmentRequestDidFail() method.
private func segmentRequestDidFail(assetRequest: GiphyAssetRequest, assetSegment: GiphyAssetSegment) {
DispatchQueue.main.async {
assetSegment.state = .failed
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
} }
} }
private func assetRequestDidFail(assetRequest: GiphyAssetRequest) { private func assetRequestDidFail(assetRequest: GiphyAssetRequest) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.activeAssetRequests.remove(assetRequest) self.removeAssetRequestFromQueue(assetRequest:assetRequest)
assetRequest.requestDidFail() assetRequest.requestDidFail()
self.startRequestIfNecessary()
} }
} }
private func startRequestIfNecessary() { private func removeAssetRequestFromQueue(assetRequest: GiphyAssetRequest) {
AssertIsOnMainThread() AssertIsOnMainThread()
DispatchQueue.main.async { guard assetRequestQueue.contains(assetRequest) else {
guard self.activeAssetRequests.count < self.kMaxAssetRequestCount else { Logger.warn("\(TAG) could not remove asset request from queue: \(assetRequest.rendition.url)")
return return
} }
guard let assetRequest = self.popNextAssetRequest() else {
return
}
guard !assetRequest.wasCancelled else {
// Discard the cancelled asset request and try again.
self.startRequestIfNecessary()
return
}
guard UIApplication.shared.applicationState == .active else {
// If app is not active, fail the asset request.
self.assetRequestDidFail(assetRequest:assetRequest)
self.startRequestIfNecessary()
return
}
self.activeAssetRequests.insert(assetRequest) assetRequestQueue = assetRequestQueue.filter { $0 != assetRequest }
processRequestQueue()
}
if let asset = self.assetMap.get(key:assetRequest.rendition.url) { // * Start a segment request or content length request if possible.
// Deferred cache hit, avoids re-downloading assets that were // * Complete/cancel asset requests if possible.
// downloaded while this request was queued. //
private func processRequestQueue() {
AssertIsOnMainThread()
self.assetRequestDidSucceed(assetRequest : assetRequest, asset: asset) guard let assetRequest = popNextAssetRequest() else {
return return
} }
guard !assetRequest.wasCancelled else {
// Discard the cancelled asset request and try again.
removeAssetRequestFromQueue(assetRequest: assetRequest)
return
}
guard UIApplication.shared.applicationState == .active else {
// If app is not active, fail the asset request.
assetRequest.state = .failed
assetRequestDidFail(assetRequest:assetRequest)
processRequestQueue()
return
}
if let asset = assetMap.get(key:assetRequest.rendition.url) {
// Deferred cache hit, avoids re-downloading assets that were
// downloaded while this request was queued.
assetRequest.state = .complete
assetRequestDidSucceed(assetRequest : assetRequest, asset: asset)
return
}
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)
request.httpMethod = "HEAD"
request.httpShouldUsePipelining = true
guard let downloadSession = self.giphyDownloadSession() else { let task = giphyDownloadSession.dataTask(with:request, completionHandler: { [weak self] data, response, error -> Void in
owsFail("\(self.TAG) Couldn't create session manager.") if let data = data, data.count > 0 {
self.assetRequestDidFail(assetRequest:assetRequest) owsFail("\(self?.TAG) HEAD request has unexpected body: \(data.count).")
}
self?.handleAssetSizeResponse(assetRequest:assetRequest, response:response, error:error)
})
assetRequest.contentLengthTask = task
task.resume()
} else {
// Start a download task.
guard let assetSegment = assetRequest.firstWaitingSegment() else {
owsFail("\(TAG) queued asset request does not have a waiting segment.")
return return
} }
assetSegment.state = .downloading
// Start a download task. var request = URLRequest(url: assetRequest.rendition.url as URL)
let task = downloadSession.downloadTask(with:assetRequest.rendition.url as URL) request.httpShouldUsePipelining = true
let rangeHeaderValue = "bytes=\(assetSegment.segmentStart)-\(assetSegment.segmentStart + assetSegment.segmentLength - 1)"
request.addValue(rangeHeaderValue, forHTTPHeaderField: "Range")
let task = giphyDownloadSession.dataTask(with:request)
task.assetRequest = assetRequest task.assetRequest = assetRequest
task.assetSegment = assetSegment
assetSegment.task = task
task.resume() task.resume()
} }
// Recurse; we may be able to start multiple downloads.
processRequestQueue()
} }
private func handleAssetSizeResponse(assetRequest: GiphyAssetRequest, response: URLResponse?, error: Error?) {
guard error == nil else {
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest:assetRequest)
return
}
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()
}
}
// Return the first asset request for which we either:
//
// * Need to download the content length.
// * Need to download at least one of its segments.
private func popNextAssetRequest() -> GiphyAssetRequest? { private func popNextAssetRequest() -> GiphyAssetRequest? {
AssertIsOnMainThread() AssertIsOnMainThread()
var activeAssetRequestURLs = Set<NSURL>() let kMaxAssetRequestCount: UInt = 3
for assetRequest in activeAssetRequests { let kMaxAssetRequestsPerAssetCount: UInt = kMaxAssetRequestCount - 1
activeAssetRequestURLs.insert(assetRequest.rendition.url)
}
// Prefer the first "high" priority request; // Prefer the first "high" priority request;
// fall back to the first "low" priority request. // fall back to the first "low" priority request.
var activeAssetRequestsCount: UInt = 0
for priority in [GiphyRequestPriority.high, GiphyRequestPriority.low] { for priority in [GiphyRequestPriority.high, GiphyRequestPriority.low] {
for (assetRequestIndex, assetRequest) in assetRequestQueue.enumerated() where assetRequest.priority == priority { for assetRequest in assetRequestQueue where assetRequest.priority == priority {
guard !activeAssetRequestURLs.contains(assetRequest.rendition.url) else { switch assetRequest.state {
// Defer requests if there is already an active asset request with the same URL. case .waiting:
// This asset request needs its content length.
return assetRequest
case .requestingSize:
activeAssetRequestsCount += 1
// Ensure that only N requests are active at a time.
guard activeAssetRequestsCount < kMaxAssetRequestCount else {
return nil
}
continue
case .active:
break
case .complete:
continue
case .failed:
continue
}
let downloadingSegmentsCount = assetRequest.downloadingSegmentsCount()
activeAssetRequestsCount += downloadingSegmentsCount
// Ensure that only N segment requests are active per asset at a time.
guard downloadingSegmentsCount < kMaxAssetRequestsPerAssetCount else {
continue
}
// Ensure that only N requests are active at a time.
guard activeAssetRequestsCount < kMaxAssetRequestCount else {
return nil
}
guard assetRequest.firstWaitingSegment() != nil else {
/// Asset request does not have a waiting segment.
continue continue
} }
assetRequestQueue.remove(at:assetRequestIndex)
return assetRequest return assetRequest
} }
} }
@ -329,105 +763,83 @@ extension URLSessionTask {
completionHandler(.allow) completionHandler(.allow)
} }
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let assetRequest = dataTask.assetRequest
let assetSegment = dataTask.assetSegment
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 // MARK: URLSessionTaskDelegate
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
let assetRequest = task.assetRequest let assetRequest = task.assetRequest
let assetSegment = task.assetSegment
guard !assetRequest.wasCancelled else { guard !assetRequest.wasCancelled else {
task.cancel() task.cancel()
assetRequestDidFail(assetRequest:assetRequest) segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return return
} }
if let error = error { if let error = error {
Logger.error("\(TAG) download failed with error: \(error)") Logger.error("\(TAG) download failed with error: \(error)")
assetRequestDidFail(assetRequest:assetRequest) segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return return
} }
guard let httpResponse = task.response as? HTTPURLResponse else { guard let httpResponse = task.response as? HTTPURLResponse else {
Logger.error("\(TAG) missing or unexpected response: \(task.response)") Logger.error("\(TAG) missing or unexpected response: \(task.response)")
assetRequestDidFail(assetRequest:assetRequest) segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return return
} }
let statusCode = httpResponse.statusCode let statusCode = httpResponse.statusCode
guard statusCode >= 200 && statusCode < 400 else { guard statusCode >= 200 && statusCode < 400 else {
Logger.error("\(TAG) response has invalid status code: \(statusCode)") Logger.error("\(TAG) response has invalid status code: \(statusCode)")
assetRequestDidFail(assetRequest:assetRequest) segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return return
} }
guard let assetFilePath = assetRequest.assetFilePath else { guard assetSegment.totalDataSize() == assetSegment.segmentLength else {
Logger.error("\(TAG) task is missing asset file") Logger.error("\(TAG) segment is missing data: \(statusCode)")
assetRequestDidFail(assetRequest:assetRequest) segmentRequestDidFail(assetRequest:assetRequest, assetSegment:assetSegment)
return return
} }
let asset = GiphyAsset(rendition: assetRequest.rendition, filePath : assetFilePath)
assetRequestDidSucceed(assetRequest : assetRequest, asset: asset)
}
// MARK: URLSessionDownloadDelegate segmentRequestDidSucceed(assetRequest : assetRequest, assetSegment: assetSegment)
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { // MARK: Temp Directory
let assetRequest = downloadTask.assetRequest
guard !assetRequest.wasCancelled else {
downloadTask.cancel()
assetRequestDidFail(assetRequest:assetRequest)
return
}
public func ensureGifFolder() {
// We write assets to the temporary directory so that iOS can clean them up. // 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. // We try to eagerly clean up these assets when they are no longer in use.
let dirPath = NSTemporaryDirectory()
let fileExtension = assetRequest.rendition.fileExtension
let fileName = (NSUUID().uuidString as NSString).appendingPathExtension(fileExtension)!
let filePath = (dirPath as NSString).appendingPathComponent(fileName)
let tempDirPath = NSTemporaryDirectory()
let dirPath = (tempDirPath as NSString).appendingPathComponent("GIFs")
do { do {
try FileManager.default.moveItem(at: location, to: URL(fileURLWithPath:filePath)) let fileManager = FileManager.default
assetRequest.assetFilePath = filePath
} catch let error as NSError {
owsFail("\(GiphyAsset.TAG) file move failed from: \(location), to: \(filePath), \(error)")
}
}
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) {
// Log accumulated data usage in debug
if _isDebugAssertConfiguration() {
let assetRequest = downloadTask.assetRequest
totalDataCount[downloadTask] = totalBytesWritten // Try to delete existing folder if necessary.
if assetRequest.rendition.isStill { if fileManager.fileExists(atPath:dirPath) {
stillDataCount[downloadTask] = totalBytesWritten try fileManager.removeItem(atPath:dirPath)
} else { gifFolderPath = dirPath
animatedDataCount[downloadTask] = totalBytesWritten
} }
// Try to create folder if necessary.
let megabyteCount = { (dataCountMap: [URLSessionDownloadTask: Int64]) -> String in if !fileManager.fileExists(atPath:dirPath) {
let sum = dataCountMap.values.reduce(0, +) try fileManager.createDirectory(atPath:dirPath,
let megabyteCount = Float(sum) / 1000 / 1000 withIntermediateDirectories:true,
return String(format: "%06.2f MB", megabyteCount) attributes:nil)
gifFolderPath = dirPath
} }
Logger.info("\(TAG) Still bytes written: \(megabyteCount(stillDataCount))") } catch let error as NSError {
Logger.info("\(TAG) Animated bytes written: \(megabyteCount(animatedDataCount))") owsFail("\(GiphyAsset.TAG) ensureTempFolder failed: \(dirPath), \(error)")
Logger.info("\(TAG) Total bytes written: \(megabyteCount(totalDataCount))") gifFolderPath = tempDirPath
}
let assetRequest = downloadTask.assetRequest
guard !assetRequest.wasCancelled else {
downloadTask.cancel()
assetRequestDidFail(assetRequest:assetRequest)
return
}
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
let assetRequest = downloadTask.assetRequest
guard !assetRequest.wasCancelled else {
downloadTask.cancel()
assetRequestDidFail(assetRequest:assetRequest)
return
} }
} }
} }

Loading…
Cancel
Save