mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 01:19:33 +00:00
Address review
This commit is contained in:
+5
-6
@@ -36,17 +36,16 @@ internal class CoilMediaFetcher(
|
|||||||
return when (val kind = mediaData.kind) {
|
return when (val kind = mediaData.kind) {
|
||||||
is MediaRequestData.Kind.Content -> fetchContent(mediaSource)
|
is MediaRequestData.Kind.Content -> fetchContent(mediaSource)
|
||||||
is MediaRequestData.Kind.Thumbnail -> {
|
is MediaRequestData.Kind.Thumbnail -> {
|
||||||
val thumbnailResult = fetchThumbnail(mediaSource, kind)
|
fetchThumbnail(mediaSource, kind).fold(
|
||||||
if (thumbnailResult.isSuccess) {
|
onSuccess = { it },
|
||||||
thumbnailResult.getOrThrow()
|
onFailure = { error ->
|
||||||
} else {
|
if (error.isNetworkError()) {
|
||||||
val error = thumbnailResult.exceptionOrNull()
|
|
||||||
if (error?.isNetworkError() == true) {
|
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
fetchContent(mediaSource)
|
fetchContent(mediaSource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is MediaRequestData.Kind.File -> fetchFile(mediaSource, kind)
|
is MediaRequestData.Kind.File -> fetchFile(mediaSource, kind)
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -42,7 +42,6 @@ class DefaultImageLoaderFactory(
|
|||||||
return ImageLoader.Builder(context)
|
return ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
add(okHttpNetworkFetcherFactory)
|
add(okHttpNetworkFetcherFactory)
|
||||||
add(SvgDecoder.Factory())
|
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-61
@@ -12,7 +12,6 @@ import android.content.Context
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Xml
|
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
@@ -43,7 +42,6 @@ import kotlinx.coroutines.flow.filterIsInstance
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.xmlpull.v1.XmlPullParser
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -69,10 +67,7 @@ class AndroidMediaPreProcessor(
|
|||||||
*/
|
*/
|
||||||
private const val IMAGE_SCALE_REF_SIZE = 640
|
private const val IMAGE_SCALE_REF_SIZE = 640
|
||||||
|
|
||||||
private const val SVG_DEFAULT_WIDTH = 640L
|
private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP)
|
||||||
private const val SVG_DEFAULT_HEIGHT = 480L
|
|
||||||
|
|
||||||
private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP, MimeTypes.Svg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val contentResolver = context.contentResolver
|
private val contentResolver = context.contentResolver
|
||||||
@@ -88,6 +83,7 @@ class AndroidMediaPreProcessor(
|
|||||||
): Result<MediaUploadInfo> = withContext(coroutineDispatchers.computation) {
|
): Result<MediaUploadInfo> = withContext(coroutineDispatchers.computation) {
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
val result = when {
|
val result = when {
|
||||||
|
mimeType == MimeTypes.Svg -> processSvgImage(uri, mimeType)
|
||||||
mimeType.isMimeTypeImage() -> {
|
mimeType.isMimeTypeImage() -> {
|
||||||
val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes
|
val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes
|
||||||
processImage(uri, mimeType, shouldBeCompressed)
|
processImage(uri, mimeType, shouldBeCompressed)
|
||||||
@@ -162,10 +158,6 @@ class AndroidMediaPreProcessor(
|
|||||||
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
|
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
|
||||||
Timber.d("Processing image ${uri.path.orEmpty().hash()}")
|
Timber.d("Processing image ${uri.path.orEmpty().hash()}")
|
||||||
|
|
||||||
if (mimeType == MimeTypes.Svg) {
|
|
||||||
return processSvgImage(uri, mimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun processImageWithCompression(): MediaUploadInfo {
|
suspend fun processImageWithCompression(): MediaUploadInfo {
|
||||||
// Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail.
|
// Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail.
|
||||||
val orientation = contentResolver.openInputStream(uri).use { input ->
|
val orientation = contentResolver.openInputStream(uri).use { input ->
|
||||||
@@ -226,10 +218,12 @@ class AndroidMediaPreProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val svgDimensionExtractor = SvgDimensionExtractor()
|
||||||
|
|
||||||
private suspend fun processSvgImage(uri: Uri, mimeType: String): MediaUploadInfo {
|
private suspend fun processSvgImage(uri: Uri, mimeType: String): MediaUploadInfo {
|
||||||
Timber.d("Processing SVG image ${uri.path.orEmpty().hash()}")
|
Timber.d("Processing SVG image ${uri.path.orEmpty().hash()}")
|
||||||
val file = copyToTmpFile(uri)
|
val file = copyToTmpFile(uri)
|
||||||
val (width, height) = extractSvgDimensions(file)
|
val (width, height) = svgDimensionExtractor.extractDimensions(file)
|
||||||
val imageInfo = ImageInfo(
|
val imageInfo = ImageInfo(
|
||||||
width = width,
|
width = width,
|
||||||
height = height,
|
height = height,
|
||||||
@@ -246,56 +240,6 @@ class AndroidMediaPreProcessor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractSvgDimensions(file: File): Pair<Long, Long> {
|
|
||||||
return file.inputStream().use { inputStream ->
|
|
||||||
try {
|
|
||||||
val parser = Xml.newPullParser()
|
|
||||||
parser.setInput(inputStream, null)
|
|
||||||
var eventType = parser.eventType
|
|
||||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
|
||||||
if (eventType == XmlPullParser.START_TAG && parser.name.equals("svg", ignoreCase = true)) {
|
|
||||||
val width = parser.getAttributeValue(null, "width")
|
|
||||||
val height = parser.getAttributeValue(null, "height")
|
|
||||||
val viewBox = parser.getAttributeValue(null, "viewBox")
|
|
||||||
|
|
||||||
val parsedWidth = width?.let { parseSvgLength(it) }
|
|
||||||
val parsedHeight = height?.let { parseSvgLength(it) }
|
|
||||||
|
|
||||||
if (parsedWidth != null && parsedHeight != null) {
|
|
||||||
return parsedWidth to parsedHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewBox != null) {
|
|
||||||
val parts = viewBox.trim().split("\\s+".toRegex()).map { it.toFloatOrNull() }
|
|
||||||
if (parts.size == 4 && parts[2] != null && parts[3] != null) {
|
|
||||||
val vbWidth = parts[2]!!.toLong().coerceAtLeast(1)
|
|
||||||
val vbHeight = parts[3]!!.toLong().coerceAtLeast(1)
|
|
||||||
return vbWidth to vbHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (parsedWidth ?: SVG_DEFAULT_WIDTH) to (parsedHeight ?: SVG_DEFAULT_HEIGHT)
|
|
||||||
}
|
|
||||||
eventType = parser.next()
|
|
||||||
}
|
|
||||||
SVG_DEFAULT_WIDTH to SVG_DEFAULT_HEIGHT
|
|
||||||
} catch (_: Exception) {
|
|
||||||
SVG_DEFAULT_WIDTH to SVG_DEFAULT_HEIGHT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseSvgLength(value: String): Long? {
|
|
||||||
val trimmed = value.trim()
|
|
||||||
if (trimmed.isEmpty()) return null
|
|
||||||
return try {
|
|
||||||
val numericPart = trimmed.replace(Regex("[^\\d.]"), "")
|
|
||||||
if (numericPart.isEmpty()) null else numericPart.toFloat().toLong().coerceAtLeast(1)
|
|
||||||
} catch (_: NumberFormatException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun processVideo(uri: Uri, mimeType: String?, videoCompressionPreset: VideoCompressionPreset): MediaUploadInfo {
|
private suspend fun processVideo(uri: Uri, mimeType: String?, videoCompressionPreset: VideoCompressionPreset): MediaUploadInfo {
|
||||||
Timber.d("Processing video ${uri.path.orEmpty().hash()}")
|
Timber.d("Processing video ${uri.path.orEmpty().hash()}")
|
||||||
val resultFile = runCatchingExceptions {
|
val resultFile = runCatchingExceptions {
|
||||||
|
|||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
* Copyright 2023-2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.mediaupload.impl
|
||||||
|
|
||||||
|
import android.util.Xml
|
||||||
|
import org.xmlpull.v1.XmlPullParser
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class SvgDimensionExtractor(
|
||||||
|
private val defaultWidth: Long = 640L,
|
||||||
|
private val defaultHeight: Long = 480L,
|
||||||
|
) {
|
||||||
|
fun extractDimensions(file: File): Pair<Long, Long> {
|
||||||
|
return file.inputStream().use { inputStream ->
|
||||||
|
try {
|
||||||
|
val parser = Xml.newPullParser()
|
||||||
|
parser.setInput(inputStream, null)
|
||||||
|
var eventType = parser.eventType
|
||||||
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||||
|
if (eventType == XmlPullParser.START_TAG && parser.name.equals("svg", ignoreCase = true)) {
|
||||||
|
val width = parser.getAttributeValue(null, "width")
|
||||||
|
val height = parser.getAttributeValue(null, "height")
|
||||||
|
val viewBox = parser.getAttributeValue(null, "viewBox")
|
||||||
|
|
||||||
|
val parsedWidth = width?.let { parseLength(it) }
|
||||||
|
val parsedHeight = height?.let { parseLength(it) }
|
||||||
|
|
||||||
|
if (parsedWidth != null && parsedHeight != null) {
|
||||||
|
return parsedWidth to parsedHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewBox != null) {
|
||||||
|
val parts = viewBox.trim().split("\\s+".toRegex()).map { it.toFloatOrNull() }
|
||||||
|
if (parts.size == 4 && parts[2] != null && parts[3] != null) {
|
||||||
|
val vbWidth = parts[2]!!.toLong().coerceAtLeast(1)
|
||||||
|
val vbHeight = parts[3]!!.toLong().coerceAtLeast(1)
|
||||||
|
return vbWidth to vbHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (parsedWidth ?: defaultWidth) to (parsedHeight ?: defaultHeight)
|
||||||
|
}
|
||||||
|
eventType = parser.next()
|
||||||
|
}
|
||||||
|
defaultWidth to defaultHeight
|
||||||
|
} catch (_: Exception) {
|
||||||
|
defaultWidth to defaultHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLength(value: String): Long? {
|
||||||
|
val trimmed = value.trim()
|
||||||
|
if (trimmed.isEmpty()) return null
|
||||||
|
return try {
|
||||||
|
val numericPart = trimmed.replace(Regex("[^\\d.]"), "")
|
||||||
|
if (numericPart.isEmpty()) null else numericPart.toFloat().toLong().coerceAtLeast(1)
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user