mirror of
https://github.com/T8RIN/ImageToolbox.git
synced 2025-05-17 21:45:59 +08:00
Fix possible OOM's
This commit is contained in:
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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 =
|
||||
|
@ -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(
|
||||
|
@ -34,7 +34,7 @@ interface ApngConverter {
|
||||
params: ApngParams,
|
||||
onFailure: (Throwable) -> Unit,
|
||||
onProgress: () -> Unit
|
||||
): ByteArray?
|
||||
): String?
|
||||
|
||||
suspend fun convertApngToJxl(
|
||||
apngUris: List<String>,
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user