509 lines
18 KiB
Swift
509 lines
18 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import CoreGraphics
|
|
import LibSignalClient
|
|
import SignalServiceKit
|
|
|
|
#if USE_DEBUG_UI
|
|
|
|
enum DebugUIError: Error {
|
|
case downloadFailed
|
|
case imageGenerationFailed
|
|
case fileCreationFailed
|
|
case invalidMimeType
|
|
case unknownFileExtension
|
|
}
|
|
|
|
class DebugUIMessagesAssetLoader {
|
|
|
|
typealias Completion = (Result<Void, Error>) -> Void
|
|
|
|
let filename: String
|
|
let mimeType: String
|
|
|
|
private(set) var prepare: ((@escaping Completion) -> Void)?
|
|
|
|
private(set) var filePath: String?
|
|
|
|
// MARK: - Public
|
|
|
|
static let jpegInstance: DebugUIMessagesAssetLoader = .fakeAssetLoaderWithUrl(
|
|
"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG",
|
|
mimeType: MimeType.imageJpeg.rawValue
|
|
)
|
|
static let gifInstance: DebugUIMessagesAssetLoader = .fakeAssetLoaderWithUrl(
|
|
"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif",
|
|
mimeType: MimeType.imageGif.rawValue
|
|
)
|
|
static let largeGifInstance: DebugUIMessagesAssetLoader = .fakeAssetLoaderWithUrl(
|
|
"https://i.giphy.com/media/LTw0F3GAdaao8/source.gif",
|
|
mimeType: MimeType.imageGif.rawValue
|
|
)
|
|
static let mp3Instance: DebugUIMessagesAssetLoader = .fakeAssetLoaderWithUrl(
|
|
"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3",
|
|
mimeType: "audio/mp3"
|
|
)
|
|
static let mp4Instance: DebugUIMessagesAssetLoader = .fakeAssetLoaderWithUrl(
|
|
"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4",
|
|
mimeType: "video/mp4"
|
|
)
|
|
|
|
static let compactPortraitPngInstance: DebugUIMessagesAssetLoader = .fakePngAssetLoaderWithImageSize(
|
|
.init(width: 60, height: 100),
|
|
backgroundColor: .blue,
|
|
textColor: .white,
|
|
label: "P"
|
|
)
|
|
static let compactLandscapePngInstance: DebugUIMessagesAssetLoader = .fakePngAssetLoaderWithImageSize(
|
|
.init(width: 100, height: 60),
|
|
backgroundColor: .green,
|
|
textColor: .white,
|
|
label: "L"
|
|
)
|
|
static let tallPortraitPngInstance: DebugUIMessagesAssetLoader = .fakePngAssetLoaderWithImageSize(
|
|
.init(width: 10, height: 100),
|
|
backgroundColor: .yellow,
|
|
textColor: .white,
|
|
label: "P"
|
|
)
|
|
static let wideLandscapePngInstance: DebugUIMessagesAssetLoader = .fakePngAssetLoaderWithImageSize(
|
|
.init(width: 100, height: 10),
|
|
backgroundColor: .purple,
|
|
textColor: .white,
|
|
label: "L"
|
|
)
|
|
static let largePngInstance: DebugUIMessagesAssetLoader = .fakePngAssetLoaderWithImageSize(
|
|
.square(4000),
|
|
backgroundColor: .brown,
|
|
textColor: .white,
|
|
label: "B"
|
|
)
|
|
static let tinyPngInstance: DebugUIMessagesAssetLoader = .fakePngAssetLoaderWithImageSize(
|
|
.square(2),
|
|
backgroundColor: .cyan,
|
|
textColor: .white,
|
|
label: "T"
|
|
)
|
|
static func pngInstance(size: CGSize, backgroundColor: UIColor, textColor: UIColor, label: String) -> DebugUIMessagesAssetLoader {
|
|
return .fakePngAssetLoaderWithImageSize(size, backgroundColor: backgroundColor, textColor: textColor, label: label)
|
|
}
|
|
static let mediumFilesizePngInstance: DebugUIMessagesAssetLoader = .fakeNoisePngAssetLoaderWithImageSize(1000)
|
|
|
|
static let tinyPdfInstance: DebugUIMessagesAssetLoader = .fakeRandomAssetLoaderWithDataLength(256, mimeType: MimeType.applicationPdf.rawValue)!
|
|
static let largePdfInstance: DebugUIMessagesAssetLoader = .fakeRandomAssetLoaderWithDataLength(4 * 1024 * 1024, mimeType: MimeType.applicationPdf.rawValue)!
|
|
|
|
static let missingPngInstance: DebugUIMessagesAssetLoader = .fakeMissingAssetLoaderWithMimeType(MimeType.imagePng.rawValue)!
|
|
static let missingPdfInstance: DebugUIMessagesAssetLoader = .fakeMissingAssetLoaderWithMimeType(MimeType.applicationPdf.rawValue)!
|
|
static let oversizeTextInstance: DebugUIMessagesAssetLoader = .fakeOversizeTextAssetLoader(text: nil)
|
|
static func oversizeTextInstance(text: String) -> DebugUIMessagesAssetLoader {
|
|
return .fakeOversizeTextAssetLoader(text: text)
|
|
}
|
|
|
|
static func prepareAssetLoaders(
|
|
_ assetLoaders: [DebugUIMessagesAssetLoader],
|
|
completion: @escaping Completion
|
|
) {
|
|
var promises = [AnyPromise]()
|
|
|
|
assetLoaders.forEach { assetLoader in
|
|
// Use chained promises to make the code more readable.
|
|
let promise = AnyPromise { future in
|
|
assetLoader.prepare!({ result in
|
|
switch result {
|
|
case .success:
|
|
future.resolve(value: Void())
|
|
|
|
case .failure(let error):
|
|
future.reject(error: error)
|
|
}
|
|
})
|
|
}
|
|
promises.append(promise)
|
|
}
|
|
|
|
AnyPromise.when(resolved: promises)
|
|
.done { _ in
|
|
completion(.success(()))
|
|
}.catch { error in
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private init(filename: String, mimeType: String) {
|
|
self.filename = filename
|
|
self.mimeType = mimeType
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private static func fakeAssetLoaderWithUrl(_ urlString: String, mimeType: String) -> DebugUIMessagesAssetLoader! {
|
|
guard let url = URL(string: urlString), !mimeType.isEmpty else {
|
|
return nil
|
|
}
|
|
let assetLoader = DebugUIMessagesAssetLoader(filename: url.lastPathComponent, mimeType: mimeType)
|
|
assetLoader.prepare = { [weak assetLoader] completion in
|
|
assetLoader?.ensureURLAssetLoaded(url, completion: completion)
|
|
}
|
|
return assetLoader
|
|
}
|
|
|
|
private func ensureURLAssetLoaded(_ url: URL, completion: @escaping Completion) {
|
|
guard filePath == nil else {
|
|
completion(.success(()))
|
|
return
|
|
}
|
|
|
|
// Use a predictable file path so that we reuse the cache between app launches.
|
|
let temporaryDirectory = OWSTemporaryDirectory()
|
|
let cacheDirectory = temporaryDirectory.appendingPathComponent("cached_random_files")
|
|
OWSFileSystem.ensureDirectoryExists(cacheDirectory)
|
|
let filePath = cacheDirectory.appendingPathComponent(filename)
|
|
guard !FileManager.default.fileExists(atPath: filePath) else {
|
|
self.filePath = filePath
|
|
completion(.success(()))
|
|
return
|
|
}
|
|
|
|
let urlSession = OWSURLSession(securityPolicy: OWSURLSession.defaultSecurityPolicy, configuration: .ephemeral)
|
|
urlSession.dataTaskPromise(url.absoluteString, method: .get)
|
|
.done { response in
|
|
guard let data = response.responseBodyData, data.count > 0 else {
|
|
owsFailDebug("Error write url response [\(url)]: \(filePath)")
|
|
completion(.failure(DebugUIError.downloadFailed))
|
|
return
|
|
}
|
|
do {
|
|
let fileUrl = URL(fileURLWithPath: filePath)
|
|
try data.write(to: fileUrl, options: .atomic)
|
|
owsAssertDebug(FileManager.default.fileExists(atPath: filePath))
|
|
completion(.success(()))
|
|
} catch {
|
|
owsFailDebug("Error downloading [\(url)]: \(error)")
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
.catch { error in
|
|
owsFailDebug("Error downloading url[\(url)]: \(error)")
|
|
completion(.failure(error))
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private static func fakePngAssetLoaderWithImageSize(
|
|
_ imageSize: CGSize,
|
|
backgroundColor: UIColor,
|
|
textColor: UIColor,
|
|
label: String
|
|
) -> DebugUIMessagesAssetLoader {
|
|
owsAssertDebug(imageSize.isNonEmpty)
|
|
owsAssertDebug(!label.isEmpty)
|
|
|
|
let assetLoader = DebugUIMessagesAssetLoader(filename: "image.png", mimeType: MimeType.imagePng.rawValue)
|
|
assetLoader.prepare = { [weak assetLoader] completion in
|
|
assetLoader?.ensurePngAssetLoaded(
|
|
imageSize: imageSize,
|
|
backgroundColor: backgroundColor,
|
|
textColor: textColor,
|
|
label: label,
|
|
completion: completion
|
|
)
|
|
}
|
|
return assetLoader
|
|
}
|
|
|
|
private func ensurePngAssetLoaded(
|
|
imageSize: CGSize,
|
|
backgroundColor: UIColor,
|
|
textColor: UIColor,
|
|
label: String,
|
|
completion: Completion
|
|
) {
|
|
owsAssertDebug(imageSize.isNonEmpty)
|
|
owsAssertDebug(!label.isEmpty)
|
|
|
|
guard filePath == nil else {
|
|
completion(.success(()))
|
|
return
|
|
}
|
|
|
|
let filePath = OWSFileSystem.temporaryFilePath(fileExtension: "png")
|
|
guard
|
|
let image = DebugUIMessagesAssetLoader.createRandomPngWithSize(
|
|
imageSize,
|
|
backgroundColor: backgroundColor,
|
|
textColor: textColor,
|
|
label: label),
|
|
let pngData = image.pngData()
|
|
else {
|
|
completion(.failure(DebugUIError.imageGenerationFailed))
|
|
return
|
|
}
|
|
|
|
do {
|
|
try pngData.write(to: URL(fileURLWithPath: filePath), options: .atomic)
|
|
self.filePath = filePath
|
|
completion(.success(()))
|
|
} catch {
|
|
owsFailDebug("Error: \(error)")
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
|
|
private static func fakeNoisePngAssetLoaderWithImageSize(_ imageSize: UInt) -> DebugUIMessagesAssetLoader {
|
|
owsAssertDebug(imageSize > 0)
|
|
|
|
let assetLoader = DebugUIMessagesAssetLoader(filename: "image.png", mimeType: MimeType.imagePng.rawValue)
|
|
assetLoader.prepare = { [weak assetLoader] completion in
|
|
assetLoader?.ensureNoisePngAssetLoaded(imageSize: imageSize, completion: completion)
|
|
}
|
|
return assetLoader
|
|
}
|
|
|
|
private func ensureNoisePngAssetLoaded(imageSize: UInt, completion: Completion) {
|
|
owsAssertDebug(imageSize > 0)
|
|
|
|
guard filePath == nil else {
|
|
completion(.success(()))
|
|
return
|
|
}
|
|
|
|
let filePath = OWSFileSystem.temporaryFilePath(fileExtension: "png")
|
|
guard
|
|
let image = DebugUIMessagesAssetLoader.buildNoiseImageWithSize(imageSize),
|
|
let pngData = image.pngData()
|
|
else {
|
|
completion(.failure(DebugUIError.imageGenerationFailed))
|
|
return
|
|
}
|
|
|
|
do {
|
|
try pngData.write(to: URL(fileURLWithPath: filePath), options: .atomic)
|
|
self.filePath = filePath
|
|
completion(.success(()))
|
|
} catch {
|
|
owsFailDebug("Error: \(error)")
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
|
|
private static func buildNoiseImageWithSize(_ size: UInt) -> UIImage? {
|
|
let backgroundColor = UIColor(rgbHex: 0xaca6633)
|
|
return imageWithSize(size, backgroundColor: backgroundColor) { context in
|
|
for x in 0..<size {
|
|
for y in 0..<size {
|
|
let color = UIColor.ows_randomColor(isAlphaRandom: false)
|
|
context.setFillColor(color.cgColor)
|
|
let rect = CGRect(x: Int(x), y: Int(y), width: 1, height: 1)
|
|
context.fill([rect])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func imageWithSize(
|
|
_ imageSize: UInt,
|
|
backgroundColor: UIColor,
|
|
drawBlock: (CGContext) -> Void
|
|
) -> UIImage? {
|
|
owsAssertDebug(imageSize > 0)
|
|
|
|
let rect = CGRect(origin: .zero, size: .square(CGFloat(imageSize)))
|
|
|
|
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
|
|
guard let context = UIGraphicsGetCurrentContext() else {
|
|
return nil
|
|
}
|
|
|
|
context.setFillColor(backgroundColor.cgColor)
|
|
context.fill([rect])
|
|
|
|
context.saveGState()
|
|
drawBlock(context)
|
|
context.restoreGState()
|
|
|
|
defer {
|
|
UIGraphicsEndImageContext()
|
|
}
|
|
|
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
|
return image
|
|
}
|
|
|
|
private static func createRandomPngWithSize(
|
|
_ imageSize: CGSize,
|
|
backgroundColor: UIColor,
|
|
textColor: UIColor,
|
|
label: String
|
|
) -> UIImage? {
|
|
owsAssertDebug(imageSize.isNonEmpty)
|
|
owsAssertDebug(!label.isEmpty)
|
|
|
|
let size = imageSize.applying(.scale(1 / UIScreen.main.scale)).roundedForScreenScale()
|
|
|
|
UIGraphicsBeginImageContextWithOptions(size, false, 0)
|
|
guard let context = UIGraphicsGetCurrentContext() else {
|
|
return nil
|
|
}
|
|
|
|
let imageRect = CGRect(origin: .zero, size: size)
|
|
|
|
context.setFillColor(backgroundColor.cgColor)
|
|
context.fill([imageRect])
|
|
|
|
let labelAsNSString = label as NSString
|
|
let smallDimension = min(size.width, size.height)
|
|
let font = UIFont.boldSystemFont(ofSize: smallDimension * 0.5)
|
|
let textAttributes: [ NSAttributedString.Key: Any ] = [ .font: font, .foregroundColor: textColor ]
|
|
var textRect = labelAsNSString.boundingRect(
|
|
with: imageRect.size,
|
|
options: [ .usesLineFragmentOrigin, .usesFontLeading ],
|
|
attributes: textAttributes,
|
|
context: nil
|
|
)
|
|
textRect.origin.x = 0.5 * (imageRect.width - textRect.width)
|
|
textRect.origin.y = 0.5 * (imageRect.height - textRect.height)
|
|
|
|
labelAsNSString.draw(at: textRect.origin, withAttributes: textAttributes)
|
|
|
|
defer {
|
|
UIGraphicsEndImageContext()
|
|
}
|
|
|
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
|
return image
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private static func fakeRandomAssetLoaderWithDataLength(_ dataLength: UInt, mimeType: String) -> DebugUIMessagesAssetLoader? {
|
|
owsAssertDebug(dataLength > 0)
|
|
owsAssertDebug(!mimeType.isEmpty)
|
|
|
|
guard let fileExtension = MimeTypeUtil.fileExtensionForMimeType(mimeType) else {
|
|
owsFailDebug("Invalid mime type: \(mimeType)")
|
|
return nil
|
|
}
|
|
let assetLoader = DebugUIMessagesAssetLoader(filename: "attachment.\(fileExtension)", mimeType: mimeType)
|
|
assetLoader.prepare = { [weak assetLoader] completion in
|
|
assetLoader?.ensureRandomAssetLoaded(dataLength, completion: completion)
|
|
}
|
|
return assetLoader
|
|
}
|
|
|
|
private func ensureRandomAssetLoaded(_ dataLength: UInt, completion: Completion) {
|
|
owsAssertDebug(dataLength > 0)
|
|
|
|
guard filePath == nil else {
|
|
completion(.success(()))
|
|
return
|
|
}
|
|
|
|
guard let fileExtension = MimeTypeUtil.fileExtensionForMimeType(mimeType) else {
|
|
completion(.failure(DebugUIError.invalidMimeType))
|
|
return
|
|
}
|
|
|
|
let data = Randomness.generateRandomBytes(dataLength)
|
|
owsAssertDebug(data.count > 0)
|
|
|
|
let filePath = OWSFileSystem.temporaryFilePath(fileExtension: fileExtension)
|
|
do {
|
|
try data.write(to: URL(fileURLWithPath: filePath), options: .atomic)
|
|
self.filePath = filePath
|
|
completion(.success(()))
|
|
} catch {
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private static func fakeMissingAssetLoaderWithMimeType(_ mimeType: String) -> DebugUIMessagesAssetLoader? {
|
|
guard let fileExtension = MimeTypeUtil.fileExtensionForMimeType(mimeType) else {
|
|
owsFailDebug("Invalid mime type: \(mimeType)")
|
|
return nil
|
|
}
|
|
|
|
let assetLoader = DebugUIMessagesAssetLoader(filename: "attachment.\(fileExtension)", mimeType: mimeType)
|
|
assetLoader.prepare = { [weak assetLoader] completion in
|
|
assetLoader?.ensureMissingAssetLoaded(completion: completion)
|
|
}
|
|
return assetLoader
|
|
}
|
|
|
|
private func ensureMissingAssetLoaded(completion: Completion) {
|
|
guard filePath == nil else {
|
|
completion(.success(()))
|
|
return
|
|
}
|
|
|
|
guard let fileExtension = MimeTypeUtil.fileExtensionForMimeType(mimeType) else {
|
|
completion(.failure(DebugUIError.invalidMimeType))
|
|
return
|
|
}
|
|
|
|
let filePath = OWSFileSystem.temporaryFilePath(fileExtension: fileExtension)
|
|
guard FileManager.default.createFile(atPath: filePath, contents: nil) else {
|
|
completion(.failure(DebugUIError.fileCreationFailed))
|
|
return
|
|
}
|
|
self.filePath = filePath
|
|
completion(.success(()))
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private static var largeTextSnippet: String {
|
|
let snippet = """
|
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rutrum, nulla
|
|
vitae pretium hendrerit, tellus turpis pharetra libero, vitae sodales tortor ante vel
|
|
sem. Fusce sed nisl a lorem gravida tincidunt. Suspendisse efficitur non quam ac
|
|
sodales. Aenean ut velit maximus, posuere sem a, accumsan nunc. Donec ullamcorper
|
|
turpis lorem. Quisque dignissim purus eu placerat ultricies. Proin at urna eget mi
|
|
semper congue. Aenean non elementum ex. Praesent pharetra quam at sem vestibulum,
|
|
vestibulum ornare dolor elementum. Vestibulum massa tortor, scelerisque sit amet
|
|
pulvinar a, rhoncus vitae nisl. Sed mi nunc, tempus at varius in, malesuada vitae
|
|
dui. Vivamus efficitur pulvinar erat vitae congue. Proin vehicula turpis non felis
|
|
congue facilisis. Nullam aliquet dapibus ligula ac mollis. Etiam sit amet posuere
|
|
lorem, in rhoncus nisi.\n\n
|
|
"""
|
|
return (0..<32).reduce(into: "") { result, _ in result += snippet }
|
|
}
|
|
|
|
private static func fakeOversizeTextAssetLoader(text: String?) -> DebugUIMessagesAssetLoader {
|
|
let assetLoader = DebugUIMessagesAssetLoader(filename: "attachment.txt", mimeType: MimeType.textXSignalPlain.rawValue)
|
|
assetLoader.prepare = { [weak assetLoader] completion in
|
|
assetLoader?.ensureOversizeTextAssetLoaded(text: text, completion: completion)
|
|
}
|
|
return assetLoader
|
|
}
|
|
|
|
private func ensureOversizeTextAssetLoaded(text: String?, completion: Completion) {
|
|
guard filePath == nil else {
|
|
completion(.success(()))
|
|
return
|
|
}
|
|
|
|
let largeText = text ?? DebugUIMessagesAssetLoader.largeTextSnippet
|
|
let textData = largeText.data(using: .utf8)!
|
|
|
|
let filePath = OWSFileSystem.temporaryFilePath(fileExtension: "txt")
|
|
do {
|
|
try textData.write(to: URL(fileURLWithPath: filePath), options: .atomic)
|
|
self.filePath = filePath
|
|
completion(.success(()))
|
|
} catch {
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|