|  |  |  | // | 
					
						
							|  |  |  | //  Copyright (c) 2019 Open Whisper Systems. All rights reserved. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import Foundation | 
					
						
							|  |  |  | import AVFoundation | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | public enum OWSThumbnailError: Error { | 
					
						
							|  |  |  |     case failure(description: String) | 
					
						
							|  |  |  |     case assertionFailure(description: String) | 
					
						
							|  |  |  |     case externalError(description: String, underlyingError:Error) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @objc public class OWSLoadedThumbnail: NSObject { | 
					
						
							|  |  |  |     public typealias DataSourceBlock = () throws -> Data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @objc | 
					
						
							|  |  |  |     public let image: UIImage | 
					
						
							|  |  |  |     let dataSourceBlock: DataSourceBlock | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @objc | 
					
						
							|  |  |  |     public init(image: UIImage, filePath: String) { | 
					
						
							|  |  |  |         self.image = image | 
					
						
							|  |  |  |         self.dataSourceBlock = { | 
					
						
							|  |  |  |             return try Data(contentsOf: URL(fileURLWithPath: filePath)) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @objc | 
					
						
							|  |  |  |     public init(image: UIImage, data: Data) { | 
					
						
							|  |  |  |         self.image = image | 
					
						
							|  |  |  |         self.dataSourceBlock = { | 
					
						
							|  |  |  |             return data | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @objc | 
					
						
							|  |  |  |     public func data() throws -> Data { | 
					
						
							|  |  |  |         return try dataSourceBlock() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | private struct OWSThumbnailRequest { | 
					
						
							|  |  |  |     public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void | 
					
						
							|  |  |  |     public typealias FailureBlock = (Error) -> Void | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     let attachment: TSAttachmentStream | 
					
						
							|  |  |  |     let thumbnailDimensionPoints: UInt | 
					
						
							|  |  |  |     let success: SuccessBlock | 
					
						
							|  |  |  |     let failure: FailureBlock | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     init(attachment: TSAttachmentStream, thumbnailDimensionPoints: UInt, success: @escaping SuccessBlock, failure: @escaping FailureBlock) { | 
					
						
							|  |  |  |         self.attachment = attachment | 
					
						
							|  |  |  |         self.thumbnailDimensionPoints = thumbnailDimensionPoints | 
					
						
							|  |  |  |         self.success = success | 
					
						
							|  |  |  |         self.failure = failure | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @objc public class OWSThumbnailService: NSObject { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // MARK: - Singleton class | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @objc(shared) | 
					
						
							|  |  |  |     public static let shared = OWSThumbnailService() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void | 
					
						
							|  |  |  |     public typealias FailureBlock = (Error) -> Void | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private let serialQueue = DispatchQueue(label: "OWSThumbnailService") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // This property should only be accessed on the serialQueue. | 
					
						
							|  |  |  |     // | 
					
						
							|  |  |  |     // We want to process requests in _reverse_ order in which they | 
					
						
							|  |  |  |     // arrive so that we prioritize the most recent view state. | 
					
						
							|  |  |  |     private var thumbnailRequestStack = [OWSThumbnailRequest]() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private func canThumbnailAttachment(attachment: TSAttachmentStream) -> Bool { | 
					
						
							|  |  |  |         return attachment.isImage || attachment.isAnimated || attachment.isVideo | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // success and failure will be called async _off_ the main thread. | 
					
						
							|  |  |  |     @objc | 
					
						
							|  |  |  |     public func ensureThumbnail(forAttachment attachment: TSAttachmentStream, | 
					
						
							|  |  |  |                                                      thumbnailDimensionPoints: UInt, | 
					
						
							|  |  |  |                                                      success: @escaping SuccessBlock, | 
					
						
							|  |  |  |                                                      failure: @escaping FailureBlock) { | 
					
						
							|  |  |  |         serialQueue.async { | 
					
						
							|  |  |  |             let thumbnailRequest = OWSThumbnailRequest(attachment: attachment, thumbnailDimensionPoints: thumbnailDimensionPoints, success: success, failure: failure) | 
					
						
							|  |  |  |             self.thumbnailRequestStack.append(thumbnailRequest) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.processNextRequestSync() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private func processNextRequestAsync() { | 
					
						
							|  |  |  |         serialQueue.async { | 
					
						
							|  |  |  |             self.processNextRequestSync() | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // This should only be called on the serialQueue. | 
					
						
							|  |  |  |     private func processNextRequestSync() { | 
					
						
							|  |  |  |         guard let thumbnailRequest = thumbnailRequestStack.popLast() else { | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         do { | 
					
						
							|  |  |  |             let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest) | 
					
						
							|  |  |  |             DispatchQueue.global().async { | 
					
						
							|  |  |  |                 thumbnailRequest.success(loadedThumbnail) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } catch { | 
					
						
							|  |  |  |             DispatchQueue.global().async { | 
					
						
							|  |  |  |                 thumbnailRequest.failure(error) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // This should only be called on the serialQueue. | 
					
						
							|  |  |  |     // | 
					
						
							|  |  |  |     // It should be safe to assume that an attachment will never end up with two thumbnails of | 
					
						
							|  |  |  |     // the same size since: | 
					
						
							|  |  |  |     // | 
					
						
							|  |  |  |     // * Thumbnails are only added by this method. | 
					
						
							|  |  |  |     // * This method checks for an existing thumbnail using the same connection. | 
					
						
							|  |  |  |     // * This method is performed on the serial queue. | 
					
						
							|  |  |  |     private func process(thumbnailRequest: OWSThumbnailRequest) throws -> OWSLoadedThumbnail { | 
					
						
							|  |  |  |         let attachment = thumbnailRequest.attachment | 
					
						
							|  |  |  |         guard canThumbnailAttachment(attachment: attachment) else { | 
					
						
							|  |  |  |             throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.") | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         let thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints) | 
					
						
							|  |  |  |         if FileManager.default.fileExists(atPath: thumbnailPath) { | 
					
						
							|  |  |  |             guard let image = UIImage(contentsOfFile: thumbnailPath) else { | 
					
						
							|  |  |  |                 throw OWSThumbnailError.failure(description: "Could not load thumbnail.") | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return OWSLoadedThumbnail(image: image, filePath: thumbnailPath) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent | 
					
						
							|  |  |  |         guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else { | 
					
						
							|  |  |  |             throw OWSThumbnailError.failure(description: "Could not create attachment's thumbnail directory.") | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         guard let originalFilePath = attachment.originalFilePath else { | 
					
						
							|  |  |  |             throw OWSThumbnailError.failure(description: "Missing original file path.") | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         let maxDimension = CGFloat(thumbnailRequest.thumbnailDimensionPoints) | 
					
						
							|  |  |  |         let thumbnailImage: UIImage | 
					
						
							|  |  |  |         if attachment.isImage || attachment.isAnimated { | 
					
						
							|  |  |  |             thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension) | 
					
						
							|  |  |  |         } else if attachment.isVideo { | 
					
						
							|  |  |  |             thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension) | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.") | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else { | 
					
						
							|  |  |  |             throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.") | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         do { | 
					
						
							|  |  |  |             try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath, isDirectory: false), options: .atomic) | 
					
						
							|  |  |  |         } catch let error as NSError { | 
					
						
							|  |  |  |             throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath) | 
					
						
							|  |  |  |         return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |