Fix possible OOM's

This commit is contained in:
T8RIN
2025-04-14 02:00:53 +03:00
parent 8b0aa71bef
commit 4f79703cad
8 changed files with 126 additions and 129 deletions

View File

@ -56,11 +56,9 @@ class CrashComponent @AssistedInject internal constructor(
fun shareLogs() {
componentScope.launch {
shareProvider.shareData(
writeData = {
it.writeBytes(settingsManager.createLogsExport())
},
filename = settingsManager.createLogsFilename()
shareProvider.shareUri(
uri = settingsManager.createLogsExport(),
onComplete = {}
)
}
}

View File

@ -128,7 +128,7 @@ internal class AndroidShareProvider @Inject constructor(
)
onComplete()
}.onFailure {
val uri = cacheData(
val newUri = cacheData(
writeData = {
it.copyFrom(
UriReadable(
@ -143,7 +143,7 @@ internal class AndroidShareProvider @Inject constructor(
)
)
shareUriImpl(
uri = uri ?: return@onFailure,
uri = newUri ?: return@onFailure,
type = type
)
onComplete()

View File

@ -221,13 +221,11 @@ interface SettingsInteractor : SimpleSettingsInteractor {
suspend fun removeCustomFont(font: DomainFontFamily.Custom)
suspend fun createCustomFontsExport(): ByteArray
suspend fun createCustomFontsExport(): String?
suspend fun toggleEnableToolExitConfirmation()
suspend fun createLogsExport(): ByteArray
fun createLogsFilename(): String
suspend fun createLogsExport(): String
}
fun SettingsInteractor.toSimpleSettingsInteractor(): SimpleSettingsInteractor =

View File

@ -34,6 +34,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import oupson.apng.decoder.ApngDecoder
import oupson.apng.encoder.ApngEncoder
import ru.tech.imageresizershrinker.core.data.utils.outputStream
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.ImageGetter
import ru.tech.imageresizershrinker.core.domain.image.ImageScaler
@ -46,13 +47,12 @@ import ru.tech.imageresizershrinker.core.domain.model.IntegerSize
import ru.tech.imageresizershrinker.core.domain.utils.runSuspendCatching
import ru.tech.imageresizershrinker.feature.apng_tools.domain.ApngConverter
import ru.tech.imageresizershrinker.feature.apng_tools.domain.ApngParams
import java.io.ByteArrayOutputStream
import javax.inject.Inject
internal class AndroidApngConverter @Inject constructor(
private val imageGetter: ImageGetter<Bitmap>,
private val imageShareProvider: ShareProvider<Bitmap>,
private val shareProvider: ShareProvider<Bitmap>,
private val imageScaler: ImageScaler<Bitmap>,
@ApplicationContext private val context: Context,
dispatchersHolder: DispatchersHolder
@ -71,7 +71,7 @@ internal class AndroidApngConverter @Inject constructor(
currentCoroutineContext().cancel(null)
return@decodeAsync
}
imageShareProvider.cacheImage(
shareProvider.cacheImage(
image = frame,
imageInfo = ImageInfo(
width = frame.width,
@ -88,8 +88,7 @@ internal class AndroidApngConverter @Inject constructor(
params: ApngParams,
onFailure: (Throwable) -> Unit,
onProgress: () -> Unit
): ByteArray? = withContext(defaultDispatcher) {
val out = ByteArrayOutputStream()
): String? = withContext(defaultDispatcher) {
val size = params.size ?: imageGetter.getImage(data = imageUris[0])!!.run {
IntegerSize(width, height)
}
@ -99,43 +98,46 @@ internal class AndroidApngConverter @Inject constructor(
return@withContext null
}
val encoder = ApngEncoder(
outputStream = out,
width = size.width,
height = size.height,
numberOfFrames = imageUris.size
).apply {
setOptimiseApng(false)
setRepetitionCount(params.repeatCount)
setCompressionLevel(params.quality.qualityValue)
}
imageUris.forEach { uri ->
imageGetter.getImage(
data = uri,
size = size
)?.let {
encoder.writeFrame(
btm = imageScaler.scaleImage(
image = imageScaler.scaleImage(
image = it,
width = size.width,
height = size.height,
resizeType = ResizeType.Flexible
),
width = size.width,
height = size.height,
resizeType = ResizeType.CenterCrop(
canvasColor = Color.Transparent.toArgb()
shareProvider.cacheData(
writeData = { writeable ->
val encoder = ApngEncoder(
outputStream = writeable.outputStream(),
width = size.width,
height = size.height,
numberOfFrames = imageUris.size
).apply {
setOptimiseApng(false)
setRepetitionCount(params.repeatCount)
setCompressionLevel(params.quality.qualityValue)
}
imageUris.forEach { uri ->
imageGetter.getImage(
data = uri,
size = size
)?.let {
encoder.writeFrame(
btm = imageScaler.scaleImage(
image = imageScaler.scaleImage(
image = it,
width = size.width,
height = size.height,
resizeType = ResizeType.Flexible
),
width = size.width,
height = size.height,
resizeType = ResizeType.CenterCrop(
canvasColor = Color.Transparent.toArgb()
)
),
delay = params.delay.toFloat()
)
),
delay = params.delay.toFloat()
)
}
onProgress()
}
encoder.writeEnd()
out.toByteArray()
}
onProgress()
}
encoder.writeEnd()
},
filename = "temp_apng.png"
)
}
override suspend fun convertApngToJxl(

View File

@ -34,7 +34,7 @@ interface ApngConverter {
params: ApngParams,
onFailure: (Throwable) -> Unit,
onProgress: () -> Unit
): ByteArray?
): String?
suspend fun convertApngToJxl(
apngUris: List<String>,

View File

@ -108,7 +108,7 @@ class ApngToolsComponent @AssistedInject internal constructor(
private val _jxlQuality: MutableState<Quality.Jxl> = mutableStateOf(Quality.Jxl())
val jxlQuality by _jxlQuality
private var apngData: ByteArray? = null
private var _outputApngUri: String? = null
fun setType(type: Screen.ApngTools.Type) {
when (type) {
@ -166,7 +166,7 @@ class ApngToolsComponent @AssistedInject internal constructor(
collectionJob = null
_type.update { null }
_convertedImageUris.update { emptyList() }
apngData = null
_outputApngUri = null
savingJob = null
updateParams(ApngParams.Default)
registerChangesCleared()
@ -191,14 +191,14 @@ class ApngToolsComponent @AssistedInject internal constructor(
) {
savingJob = componentScope.launch {
_isSaving.value = true
apngData?.let { byteArray ->
fileController.writeBytes(
uri = uri.toString(),
block = { it.writeBytes(byteArray) }
_outputApngUri?.let { apngUri ->
fileController.transferBytes(
fromUri = apngUri,
toUri = uri.toString(),
).also(onResult).onSuccess(::registerSave)
}
_isSaving.value = false
apngData = null
_outputApngUri = null
}
}
@ -267,7 +267,7 @@ class ApngToolsComponent @AssistedInject internal constructor(
is Screen.ApngTools.Type.ImageToApng -> {
_left.value = type.imageUris?.size ?: -1
apngData = type.imageUris?.map { it.toString() }?.let { list ->
_outputApngUri = type.imageUris?.map { it.toString() }?.let { list ->
apngConverter.createApngFromImageUris(
imageUris = list,
params = params,
@ -441,13 +441,8 @@ class ApngToolsComponent @AssistedInject internal constructor(
_done.update { it + 1 }
},
onFailure = {}
)?.also { byteArray ->
shareProvider.cacheByteArray(
byteArray = byteArray,
filename = "APNG_${timestamp()}.png"
)?.let {
onComplete(listOf(it.toUri()))
}
)?.also { uri ->
onComplete(listOf(uri.toUri()))
}
}
}

View File

@ -18,6 +18,7 @@
package ru.tech.imageresizershrinker.feature.settings.data
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Typeface
import androidx.core.net.toFile
import androidx.core.net.toUri
@ -27,6 +28,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import com.t8rin.logger.Logger
import com.t8rin.logger.makeLog
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@ -37,9 +39,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ru.tech.imageresizershrinker.core.data.utils.getFilename
import ru.tech.imageresizershrinker.core.data.utils.isInstalledFromPlayStore
import ru.tech.imageresizershrinker.core.data.utils.outputStream
import ru.tech.imageresizershrinker.core.domain.BackupFileExtension
import ru.tech.imageresizershrinker.core.domain.GlobalStorageName
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.ShareProvider
import ru.tech.imageresizershrinker.core.domain.image.model.ImageScaleMode
import ru.tech.imageresizershrinker.core.domain.image.model.ResizeType
import ru.tech.imageresizershrinker.core.domain.model.ColorModel
@ -151,7 +155,6 @@ import ru.tech.imageresizershrinker.feature.settings.data.keys.USE_RANDOM_EMOJIS
import ru.tech.imageresizershrinker.feature.settings.data.keys.VIBRATION_STRENGTH
import ru.tech.imageresizershrinker.feature.settings.data.keys.toSettingsState
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.util.zip.ZipEntry
@ -162,6 +165,7 @@ import kotlin.random.Random
internal class AndroidSettingsManager @Inject constructor(
@ApplicationContext private val context: Context,
private val dataStore: DataStore<Preferences>,
private val shareProvider: Lazy<ShareProvider<Bitmap>>,
dispatchersHolder: DispatchersHolder,
) : DispatchersHolder by dispatchersHolder, SettingsManager {
@ -225,9 +229,9 @@ internal class AndroidSettingsManager @Inject constructor(
it[APP_COLOR_TUPLE] = colorTuple
}
override suspend fun setPresets(newPresets: List<Int>) = edit {
override suspend fun setPresets(newPresets: List<Int>) = edit { preferences ->
if (newPresets.size > 3) {
it[PRESETS] = newPresets
preferences[PRESETS] = newPresets
.map { it.coerceIn(10..500) }
.toSortedSet()
.toList()
@ -454,34 +458,35 @@ internal class AndroidSettingsManager @Inject constructor(
it[INITIAL_OCR_MODE] ?: 1
}
override suspend fun createLogsExport(): ByteArray = withContext(ioDispatcher) {
override suspend fun createLogsExport(): String = withContext(ioDispatcher) {
"Start Logs Export".makeLog("SettingsManager")
val logsFile = Logger.getLogsFile().toFile()
val settingsFile = createBackupFile()
val out = ByteArrayOutputStream()
shareProvider.get().cacheData(
writeData = { writeable ->
val out = writeable.outputStream()
ZipOutputStream(out).use { zipOut ->
FileInputStream(logsFile).use { fis ->
val zipEntry = ZipEntry(logsFile.name)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
zipOut.closeEntry()
}
ByteArrayInputStream(settingsFile).use { bis ->
val zipEntry = ZipEntry(createBackupFilename())
zipOut.putNextEntry(zipEntry)
bis.copyTo(zipOut)
zipOut.closeEntry()
}
}
out.toByteArray()
ZipOutputStream(out).use { zipOut ->
FileInputStream(logsFile).use { fis ->
val zipEntry = ZipEntry(logsFile.name)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
zipOut.closeEntry()
}
ByteArrayInputStream(settingsFile).use { bis ->
val zipEntry = ZipEntry(createBackupFilename())
zipOut.putNextEntry(zipEntry)
bis.copyTo(zipOut)
zipOut.closeEntry()
}
}
},
filename = "image_toolbox_logs_${timestamp()}.zip"
) ?: ""
}
override fun createLogsFilename(): String = "image_toolbox_logs_${timestamp()}.zip"
override suspend fun setScreensWithBrightnessEnforcement(data: String) = edit {
it[SCREENS_WITH_BRIGHTNESS_ENFORCEMENT] = data
}
@ -561,8 +566,8 @@ internal class AndroidSettingsManager @Inject constructor(
override suspend fun setOneTimeSaveLocations(
value: List<OneTimeSaveLocation>
) = edit {
it[ONE_TIME_SAVE_LOCATIONS] = value.filter {
) = edit { preferences ->
preferences[ONE_TIME_SAVE_LOCATIONS] = value.filter {
it.uri.isNotEmpty() && it.date != null
}.distinctBy { it.uri }.joinToString(", ")
}
@ -570,7 +575,7 @@ internal class AndroidSettingsManager @Inject constructor(
override suspend fun toggleRecentColor(
color: ColorModel,
forceExclude: Boolean,
) = edit {
) = edit { preferences ->
val current = currentSettings.recentColors
val newColors = if (color in current) {
if (forceExclude) {
@ -582,13 +587,13 @@ internal class AndroidSettingsManager @Inject constructor(
listOf(color) + current
}
it[RECENT_COLORS] = newColors.take(30).map { it.colorInt.toString() }.toSet()
preferences[RECENT_COLORS] = newColors.take(30).map { it.colorInt.toString() }.toSet()
}
override suspend fun toggleFavoriteColor(
color: ColorModel,
forceExclude: Boolean
) = edit {
) = edit { preferences ->
val current = currentSettings.favoriteColors
val newColors = if (color in current) {
if (forceExclude) {
@ -600,7 +605,7 @@ internal class AndroidSettingsManager @Inject constructor(
listOf(color) + current
}
it[FAVORITE_COLORS] = newColors.map { it.colorInt.toString() }.joinToString("/")
preferences[FAVORITE_COLORS] = newColors.joinToString("/") { it.colorInt.toString() }
}
override suspend fun toggleOpenEditInsteadOfPreview() = toggle(
@ -668,7 +673,7 @@ internal class AndroidSettingsManager @Inject constructor(
current + screenId
}
it[FAVORITE_SCREENS] = newScreens.joinToString("/") { it.toString() }
it[FAVORITE_SCREENS] = newScreens.joinToString("/")
}
override suspend fun toggleIsLinkPreviewEnabled() = toggle(
@ -698,8 +703,8 @@ internal class AndroidSettingsManager @Inject constructor(
it[IS_TELEGRAM_GROUP_OPENED] = true
}
override suspend fun setDefaultResizeType(resizeType: ResizeType) = edit {
it[DEFAULT_RESIZE_TYPE] = ResizeType.entries.indexOfFirst {
override suspend fun setDefaultResizeType(resizeType: ResizeType) = edit { preferences ->
preferences[DEFAULT_RESIZE_TYPE] = ResizeType.entries.indexOfFirst {
it::class.isInstance(resizeType)
}
}
@ -742,8 +747,8 @@ internal class AndroidSettingsManager @Inject constructor(
override suspend fun toggleSettingsGroupVisibility(
key: Int,
value: Boolean
) = edit {
it[SETTINGS_GROUP_VISIBILITY] =
) = edit { preferences ->
preferences[SETTINGS_GROUP_VISIBILITY] =
currentSettings.settingGroupsInitialVisibility.toMutableMap().run {
this[key] = value
map {
@ -758,8 +763,8 @@ internal class AndroidSettingsManager @Inject constructor(
override suspend fun updateFavoriteColors(
colors: List<ColorModel>
) = edit {
it[FAVORITE_COLORS] = colors.map { it.colorInt.toString() }.joinToString("/")
) = edit { preferences ->
preferences[FAVORITE_COLORS] = colors.joinToString("/") { it.colorInt.toString() }
}
override suspend fun setBackgroundColorForNoAlphaFormats(
@ -832,22 +837,23 @@ internal class AndroidSettingsManager @Inject constructor(
setCustomFonts(currentSettings.customFonts - font)
}
override suspend fun createCustomFontsExport(): ByteArray = withContext(ioDispatcher) {
val out = ByteArrayOutputStream()
ZipOutputStream(out).use { zipOut ->
val dir = File(context.filesDir, "customFonts")
dir.listFiles()?.forEach { file ->
FileInputStream(file).use { fis ->
val zipEntry = ZipEntry(file.name)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
zipOut.closeEntry()
override suspend fun createCustomFontsExport(): String? = withContext(ioDispatcher) {
shareProvider.get().cacheData(
writeData = { writeable ->
ZipOutputStream(writeable.outputStream()).use { zipOut ->
val dir = File(context.filesDir, "customFonts")
dir.listFiles()?.forEach { file ->
FileInputStream(file).use { fis ->
val zipEntry = ZipEntry(file.name)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
zipOut.closeEntry()
}
}
}
}
}
out.toByteArray()
},
filename = "fonts_export.zip"
)
}
override suspend fun toggleEnableToolExitConfirmation() = toggle(
@ -863,7 +869,7 @@ internal class AndroidSettingsManager @Inject constructor(
this[key] = !value
}
suspend fun toggle(
private suspend fun toggle(
key: Preferences.Key<Boolean>,
defaultValue: Boolean,
) = edit {
@ -873,7 +879,7 @@ internal class AndroidSettingsManager @Inject constructor(
)
}
suspend fun edit(
private suspend fun edit(
transform: suspend (MutablePreferences) -> Unit
) {
dataStore.edit(transform)

View File

@ -248,9 +248,9 @@ class SettingsComponent @AssistedInject internal constructor(
uri: Uri,
onResult: (SaveResult) -> Unit,
) = settingsScope {
fileController.writeBytes(
uri = uri.toString(),
block = { it.writeBytes(createCustomFontsExport()) }
fileController.transferBytes(
fromUri = createCustomFontsExport().toString(),
toUri = uri.toString()
).also(onResult)
}
@ -466,11 +466,9 @@ class SettingsComponent @AssistedInject internal constructor(
fun toggleEnableToolExitConfirmation() = settingsScope { toggleEnableToolExitConfirmation() }
fun shareLogs() = settingsScope {
shareProvider.shareData(
writeData = {
it.writeBytes(settingsManager.createLogsExport())
},
filename = settingsManager.createLogsFilename()
shareProvider.shareUri(
uri = settingsManager.createLogsExport(),
onComplete = {}
)
}