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

293 lines
11 KiB
Swift
Raw Normal View History

//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
import ObjectiveC
@objc class GiphyAssetRequest: NSObject {
static let TAG = "[GiphyAssetRequest]"
let rendition: GiphyRendition
let success: ((GiphyAsset) -> Void)
let failure: (() -> Void)
var wasCancelled = false
var assetFilePath: String?
init(rendition: GiphyRendition,
success:@escaping ((GiphyAsset) -> Void),
failure:@escaping (() -> Void)
) {
self.rendition = rendition
self.success = success
self.failure = failure
}
public func cancel() {
wasCancelled = true
}
}
@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 {
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)")
}
}
}
}
private var URLSessionTask_GiphyAssetRequest: UInt8 = 0
extension URLSessionTask {
var assetRequest: GiphyAssetRequest {
get {
return objc_getAssociatedObject(self, &URLSessionTask_GiphyAssetRequest) as! GiphyAssetRequest
}
set {
objc_setAssociatedObject(self, &URLSessionTask_GiphyAssetRequest, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
@objc class GifDownloader: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {
// MARK: - Properties
static let TAG = "[GifDownloader]"
static let sharedInstance = GifDownloader()
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? {
// guard let baseUrl = NSURL(string:kGiphyBaseURL) else {
// Logger.error("\(GifDownloader.TAG) Invalid base URL.")
// return nil
// }
// TODO: Is this right?
let configuration = URLSessionConfiguration.ephemeral
// TODO: Is this right?
configuration.connectionProxyDictionary = [
kCFProxyHostNameKey as String: "giphy-proxy-production.whispersystems.org",
kCFProxyPortNumberKey as String: "80",
kCFProxyTypeKey as String: kCFProxyTypeHTTPS
]
configuration.urlCache = nil
configuration.requestCachePolicy = .reloadIgnoringCacheData
let session = URLSession(configuration:configuration, delegate:self, delegateQueue:operationQueue)
return session
// NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
//
// let sessionManager = AFHTTPSessionManager(baseURL:baseUrl as URL,
// sessionConfiguration:sessionConf)
// sessionManager.requestSerializer = AFJSONRequestSerializer()
// sessionManager.responseSerializer = AFJSONResponseSerializer()
//
// return sessionManager
}
// TODO: Use a proper cache.
private var assetMap = [NSURL: GiphyAsset]()
// TODO: We could use a proper queue.
private var assetRequestQueue = [GiphyAssetRequest]()
private var isDownloading = false
// The success and failure handlers are always called on main queue.
// The success and failure handlers may be called synchronously on cache hit.
public func downloadAssetAsync(rendition: GiphyRendition,
success:@escaping ((GiphyAsset) -> Void),
failure:@escaping (() -> Void)) -> GiphyAssetRequest? {
AssertIsOnMainThread()
if let asset = assetMap[rendition.url] {
success(asset)
return nil
}
var hasRequestCompleted = false
let assetRequest = GiphyAssetRequest(rendition:rendition,
success : { asset in
DispatchQueue.main.async {
// Ensure we call success or failure exactly once.
guard !hasRequestCompleted else {
return
}
hasRequestCompleted = true
self.assetMap[rendition.url] = asset
self.isDownloading = false
self.downloadIfNecessary()
success(asset)
}
},
failure : {
DispatchQueue.main.async {
// Ensure we call success or failure exactly once.
guard !hasRequestCompleted else {
return
}
hasRequestCompleted = true
self.isDownloading = false
self.downloadIfNecessary()
failure()
}
})
assetRequestQueue.append(assetRequest)
downloadIfNecessary()
return assetRequest
}
private func downloadIfNecessary() {
AssertIsOnMainThread()
DispatchQueue.main.async {
guard !self.isDownloading else {
return
}
guard self.assetRequestQueue.count > 0 else {
return
}
guard let assetRequest = self.assetRequestQueue.first else {
owsFail("\(GiphyAsset.TAG) could not pop asset requests")
return
}
self.assetRequestQueue.removeFirst()
guard !assetRequest.wasCancelled else {
DispatchQueue.main.async {
self.downloadIfNecessary()
}
return
}
self.isDownloading = true
if let asset = self.assetMap[assetRequest.rendition.url] {
// Deferred cache hit, avoids re-downloading assets already in the
// asset cache.
assetRequest.success(asset)
return
}
guard let downloadSession = self.giphyDownloadSession() else {
Logger.error("\(GifDownloader.TAG) Couldn't create session manager.")
assetRequest.failure()
return
}
let task = downloadSession.downloadTask(with:assetRequest.rendition.url as URL)
task.assetRequest = assetRequest
task.resume()
}
}
// MARK: URLSessionDataDelegate
@nonobjc
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
completionHandler(.allow)
}
// MARK: URLSessionTaskDelegate
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
let assetRequest = task.assetRequest
guard !assetRequest.wasCancelled else {
task.cancel()
assetRequest.failure()
return
}
if let error = error {
Logger.error("\(GifDownloader.TAG) download failed with error: \(error)")
assetRequest.failure()
return
}
guard let httpResponse = task.response as? HTTPURLResponse else {
Logger.error("\(GifDownloader.TAG) missing or unexpected response: \(task.response)")
assetRequest.failure()
return
}
let statusCode = httpResponse.statusCode
guard statusCode >= 200 && statusCode < 400 else {
Logger.error("\(GifDownloader.TAG) response has invalid status code: \(statusCode)")
assetRequest.failure()
return
}
guard let assetFilePath = assetRequest.assetFilePath else {
Logger.error("\(GifDownloader.TAG) task is missing asset file")
assetRequest.failure()
return
}
// Logger.verbose("\(GifDownloader.TAG) download succeeded: \(assetRequest.rendition.url)")
let asset = GiphyAsset(rendition: assetRequest.rendition, filePath : assetFilePath)
assetRequest.success(asset)
}
// MARK: URLSessionDownloadDelegate
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let assetRequest = downloadTask.assetRequest
guard !assetRequest.wasCancelled else {
downloadTask.cancel()
assetRequest.failure()
return
}
let dirPath = NSTemporaryDirectory()
let fileExtension = assetRequest.rendition.fileExtension()
let fileName = (NSUUID().uuidString as NSString).appendingPathExtension(fileExtension)!
let filePath = (dirPath as NSString).appendingPathComponent(fileName)
do {
try FileManager.default.moveItem(at: location, to: URL(fileURLWithPath:filePath))
assetRequest.assetFilePath = filePath
} catch let error as NSError {
owsFail("\(GiphyAsset.TAG) file move failed from: \(location), to: \(filePath), \(error)")
}
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let assetRequest = downloadTask.assetRequest
guard !assetRequest.wasCancelled else {
downloadTask.cancel()
assetRequest.failure()
return
}
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
let assetRequest = downloadTask.assetRequest
guard !assetRequest.wasCancelled else {
downloadTask.cancel()
assetRequest.failure()
return
}
}
}