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() { fun shareLogs() {
componentScope.launch { componentScope.launch {
shareProvider.shareData( shareProvider.shareUri(
writeData = { uri = settingsManager.createLogsExport(),
it.writeBytes(settingsManager.createLogsExport()) onComplete = {}
},
filename = settingsManager.createLogsFilename()
) )
} }
} }

View File

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

View File

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

View File

@ -34,6 +34,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import oupson.apng.decoder.ApngDecoder import oupson.apng.decoder.ApngDecoder
import oupson.apng.encoder.ApngEncoder 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.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.ImageGetter import ru.tech.imageresizershrinker.core.domain.image.ImageGetter
import ru.tech.imageresizershrinker.core.domain.image.ImageScaler 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.core.domain.utils.runSuspendCatching
import ru.tech.imageresizershrinker.feature.apng_tools.domain.ApngConverter import ru.tech.imageresizershrinker.feature.apng_tools.domain.ApngConverter
import ru.tech.imageresizershrinker.feature.apng_tools.domain.ApngParams import ru.tech.imageresizershrinker.feature.apng_tools.domain.ApngParams
import java.io.ByteArrayOutputStream
import javax.inject.Inject import javax.inject.Inject
internal class AndroidApngConverter @Inject constructor( internal class AndroidApngConverter @Inject constructor(
private val imageGetter: ImageGetter<Bitmap>, private val imageGetter: ImageGetter<Bitmap>,
private val imageShareProvider: ShareProvider<Bitmap>, private val shareProvider: ShareProvider<Bitmap>,
private val imageScaler: ImageScaler<Bitmap>, private val imageScaler: ImageScaler<Bitmap>,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
dispatchersHolder: DispatchersHolder dispatchersHolder: DispatchersHolder
@ -71,7 +71,7 @@ internal class AndroidApngConverter @Inject constructor(
currentCoroutineContext().cancel(null) currentCoroutineContext().cancel(null)
return@decodeAsync return@decodeAsync
} }
imageShareProvider.cacheImage( shareProvider.cacheImage(
image = frame, image = frame,
imageInfo = ImageInfo( imageInfo = ImageInfo(
width = frame.width, width = frame.width,
@ -88,8 +88,7 @@ internal class AndroidApngConverter @Inject constructor(
params: ApngParams, params: ApngParams,
onFailure: (Throwable) -> Unit, onFailure: (Throwable) -> Unit,
onProgress: () -> Unit onProgress: () -> Unit
): ByteArray? = withContext(defaultDispatcher) { ): String? = withContext(defaultDispatcher) {
val out = ByteArrayOutputStream()
val size = params.size ?: imageGetter.getImage(data = imageUris[0])!!.run { val size = params.size ?: imageGetter.getImage(data = imageUris[0])!!.run {
IntegerSize(width, height) IntegerSize(width, height)
} }
@ -99,8 +98,10 @@ internal class AndroidApngConverter @Inject constructor(
return@withContext null return@withContext null
} }
shareProvider.cacheData(
writeData = { writeable ->
val encoder = ApngEncoder( val encoder = ApngEncoder(
outputStream = out, outputStream = writeable.outputStream(),
width = size.width, width = size.width,
height = size.height, height = size.height,
numberOfFrames = imageUris.size numberOfFrames = imageUris.size
@ -134,8 +135,9 @@ internal class AndroidApngConverter @Inject constructor(
onProgress() onProgress()
} }
encoder.writeEnd() encoder.writeEnd()
},
out.toByteArray() filename = "temp_apng.png"
)
} }
override suspend fun convertApngToJxl( override suspend fun convertApngToJxl(

View File

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

View File

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

View File

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

View File

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