diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61e395d..e3c017b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,12 +72,14 @@ android { "proguard-rules.pro", ) signingConfig = signingConfigs.getByName(Constants.RELEASE) + resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"") } debug { applicationIdSuffix = ".debug" versionNameSuffix = "-debug" resValue("string", "app_name", "WG Tunnel - Debug") isDebuggable = true + resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"") } create(Constants.PRERELEASE) { @@ -85,6 +87,7 @@ android { applicationIdSuffix = ".prerelease" versionNameSuffix = "-pre" resValue("string", "app_name", "WG Tunnel - Pre") + resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"") } create(Constants.NIGHTLY) { @@ -92,6 +95,7 @@ android { applicationIdSuffix = ".nightly" versionNameSuffix = "-nightly" resValue("string", "app_name", "WG Tunnel - Nightly") + resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"") } applicationVariants.all { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 736070b..35e6d72 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,6 +98,16 @@ android:launchMode="singleInstance" android:theme="@android:style/Theme.NoDisplay" /> + + + + + if (currentScrollPosition < lastScrollPosition && isAutoScrolling) { + isAutoScrolling = false + } + val visible = lazyColumnListState.layoutInfo.visibleItemsInfo + if (visible.isNotEmpty()) { + if (visible.last().index + == lazyColumnListState.layoutInfo.totalItemsCount - 1 && !isAutoScrolling + ) { + isAutoScrolling = true + } + } + lastScrollPosition = currentScrollPosition + } + } + Scaffold( floatingActionButton = { FloatingActionButton( onClick = { - scope.launch { - viewModel.saveLogsToFile().onSuccess { - Toast.makeText( - context, - context.getString(R.string.logs_saved), - Toast.LENGTH_SHORT, - ).show() - } - } + viewModel.shareLogs(context) }, shape = RoundedCornerShape(16.dp), containerColor = MaterialTheme.colorScheme.primary, ) { - val icon = Icons.Filled.Save + val icon = Icons.Filled.Share Icon( imageVector = icon, contentDescription = icon.name, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt index e619f11..17df5fe 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsViewModel.kt @@ -1,19 +1,25 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs +import android.content.Context import androidx.compose.runtime.mutableStateListOf +import androidx.core.content.FileProvider import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.zaneschepke.logcatter.LocalLogCollector +import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.MainDispatcher import com.zaneschepke.wireguardautotunnel.util.Constants -import com.zaneschepke.wireguardautotunnel.util.FileUtils import com.zaneschepke.wireguardautotunnel.util.extensions.chunked +import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job +import com.zaneschepke.wireguardautotunnel.R import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File import java.time.Duration import java.time.Instant import javax.inject.Inject @@ -22,8 +28,7 @@ import javax.inject.Inject class LogsViewModel @Inject constructor( - private val localLogCollector: LocalLogCollector, - private val fileUtils: FileUtils, + private val localLogCollector: LogReader, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, @MainDispatcher private val mainDispatcher: CoroutineDispatcher, ) : ViewModel() { @@ -44,13 +49,19 @@ constructor( } } - suspend fun saveLogsToFile(): Result { - val file = - localLogCollector.getLogFile().getOrElse { - return Result.failure(it) - } - val fileContent = fileUtils.readBytesFromFile(file) - val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" - return fileUtils.saveByteArrayToDownloads(fileContent, fileName) + fun shareLogs(context: Context): Job = viewModelScope.launch(ioDispatcher) { + runCatching { + val sharePath = File(context.filesDir, "external_files") + if (sharePath.exists()) sharePath.delete() + sharePath.mkdir() + val file = File("${sharePath.path + "/" + Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip") + if (file.exists()) file.delete() + file.createNewFile() + localLogCollector.zipLogFiles(file.absolutePath) + val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), file) + context.launchShareFile(uri) + }.onFailure { + Timber.e(it) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt index 4c9add9..7089b8c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/ContextExtensions.kt @@ -26,6 +26,16 @@ fun Context.openWebUrl(url: String): Result { } } +fun Context.launchShareFile(file: Uri) { + val shareIntent = Intent().apply { + setAction(Intent.ACTION_SEND) + setType("*/*") + putExtra(Intent.EXTRA_STREAM, file) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + this.startActivity(Intent.createChooser(shareIntent, "")) +} + fun Context.showToast(resId: Int) { Toast.makeText( this, diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..be991f2 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt similarity index 67% rename from logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt rename to logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt index 83d17ec..56f44dd 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/LocalLogCollector.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt @@ -2,14 +2,12 @@ package com.zaneschepke.logcatter import com.zaneschepke.logcatter.model.LogMessage import kotlinx.coroutines.flow.Flow -import java.io.File -interface LocalLogCollector { +interface LogReader { suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null) - fun stop() - - suspend fun getLogFile(): Result - + fun zipLogFiles(path: String) + suspend fun deleteAndClearLogs() val bufferedLogs: Flow + val liveLogs: Flow } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatCollector.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatCollector.kt new file mode 100644 index 0000000..bbefde2 --- /dev/null +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatCollector.kt @@ -0,0 +1,274 @@ +package com.zaneschepke.logcatter + +import android.content.Context +import com.zaneschepke.logcatter.model.LogMessage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.BufferedOutputStream +import java.io.BufferedReader +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +object LogcatCollector { + + private const val MAX_FILE_SIZE = 2097152L // 2MB + private const val MAX_FOLDER_SIZE = 10485760L // 10MB + + private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex() + private val findIpv6AddressRegex = """^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}${'$'}""".toRegex() + private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex() + private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex() + + private val ioDispatcher = Dispatchers.IO + + private object LogcatHelperInit { + var maxFileSize: Long = MAX_FILE_SIZE + var maxFolderSize: Long = MAX_FOLDER_SIZE + var pID: Int = 0 + var publicAppDirectory = "" + var logcatPath = "" + } + + fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, context: Context): LogReader { + if (maxFileSize > maxFolderSize) { + throw IllegalStateException("maxFileSize must be less than maxFolderSize") + } + synchronized(LogcatHelperInit) { + LogcatHelperInit.maxFileSize = maxFileSize + LogcatHelperInit.maxFolderSize = maxFolderSize + LogcatHelperInit.pID = android.os.Process.myPid() + context.getExternalFilesDir(null)?.let { + LogcatHelperInit.publicAppDirectory = it.absolutePath + LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs" + val logDirectory = File(LogcatHelperInit.logcatPath) + if (!logDirectory.exists()) { + logDirectory.mkdir() + } + } + return Logcat + } + } + + internal object Logcat : LogReader { + + private var logcatReader: LogcatReader? = null + + private fun obfuscator(log: String): String { + return findKeyRegex.replace(log, "").let { first -> + findIpv6AddressRegex.replace(first, "").let { second -> + findTunnelNameRegex.replace(second, "") + } + }.let { last -> findIpv4AddressRegex.replace(last, "") } + } + + override suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) { + logcatReader ?: run { + logcatReader = LogcatReader(LogcatHelperInit.pID.toString(), LogcatHelperInit.logcatPath, onLogMessage) + } + logcatReader?.run() + } + + override fun stop() { + logcatReader?.stop() + logcatReader = null + } + + override fun zipLogFiles(path: String) { + logcatReader?.pause() + zipAll(path) + logcatReader?.resume() + } + + private fun zipAll(zipFilePath: String) { + val sourceFile = File(LogcatHelperInit.logcatPath) + val outputZipFile = File(zipFilePath) + ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> + sourceFile.walkTopDown().forEach { file -> + val zipFileName = file.absolutePath.removePrefix(sourceFile.absolutePath).removePrefix("/") + val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") + zos.putNextEntry(entry) + if (file.isFile) { + file.inputStream().use { + it.copyTo(zos) + } + } + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun deleteAndClearLogs() { + withContext(ioDispatcher) { + logcatReader?.pause() + _bufferedLogs.resetReplayCache() + logcatReader?.deleteAllFiles() + logcatReader?.resume() + } + } + + private val _bufferedLogs = MutableSharedFlow( + replay = 10_000, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + private val _liveLogs = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow() + + override val liveLogs: Flow = _liveLogs.asSharedFlow() + + private class LogcatReader( + pID: String, + private val logcatPath: String, + private val callback: ((input: LogMessage) -> Unit)?, + ) { + private var logcatProc: Process? = null + private var reader: BufferedReader? = null + + @get:Synchronized @set:Synchronized + private var paused = false + + @get:Synchronized @set:Synchronized + private var stopped = false + private var command = "" + private var clearLogCommand = "" + private var outputStream: FileOutputStream? = null + + init { + try { + outputStream = FileOutputStream(createLogFile(logcatPath)) + } catch (e: FileNotFoundException) { + Timber.e(e) + } + + command = "logcat -v epoch | grep \"($pID)\"" + clearLogCommand = "logcat -c" + } + + fun pause() { + paused = true + } + fun stop() { + stopped = true + } + + fun resume() { + paused = false + } + + fun clear() { + Runtime.getRuntime().exec(clearLogCommand) + } + + suspend fun run() { + withContext(ioDispatcher) { + paused = false + stopped = false + if (outputStream == null) return@withContext + try { + clear() + logcatProc = Runtime.getRuntime().exec(command) + reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024) + var line: String? = null + + while (!stopped) { + if (paused) continue + line = reader?.readLine() + if (line.isNullOrEmpty()) continue + outputStream?.let { + if (it.channel.size() >= LogcatHelperInit.maxFileSize) { + it.close() + outputStream = createNewLogFileStream() + } + if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) { + deleteOldestFile() + } + line.let { text -> + val obfuscated = obfuscator(text) + it.write((obfuscated + System.lineSeparator()).toByteArray()) + try { + val logMessage = LogMessage.from(text) + _bufferedLogs.tryEmit(logMessage) + _liveLogs.tryEmit(logMessage) + callback?.let { + it(logMessage) + } + } catch (e: Exception) { + Timber.e(e) + } + } + } + } + } catch (e: IOException) { + Timber.e(e) + } finally { + logcatProc?.destroy() + logcatProc = null + + try { + reader?.close() + outputStream?.close() + reader = null + outputStream = null + } catch (e: IOException) { + Timber.e(e) + } + } + } + } + + private fun getFolderSize(path: String): Long { + File(path).run { + var size = 0L + if (this.isDirectory && this.listFiles() != null) { + for (file in this.listFiles()!!) { + size += getFolderSize(file.absolutePath) + } + } else { + size = this.length() + } + return size + } + } + + private fun createLogFile(dir: String): File { + return File(dir, "logcat_" + System.currentTimeMillis() + ".txt") + } + + fun deleteOldestFile() { + val directory = File(logcatPath) + if (directory.isDirectory) { + directory.listFiles()?.toMutableList()?.run { + this.sortBy { it.lastModified() } + this.first().delete() + } + } + } + + private fun createNewLogFileStream(): FileOutputStream { + return FileOutputStream(createLogFile(logcatPath)) + } + + fun deleteAllFiles() { + val directory = File(logcatPath) + directory.listFiles()?.toMutableList()?.run { + this.forEach { it.delete() } + } + outputStream = createNewLogFileStream() + } + } + } +} diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt deleted file mode 100644 index 645b165..0000000 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/LogcatUtil.kt +++ /dev/null @@ -1,274 +0,0 @@ -package com.zaneschepke.logcatter - -import android.content.Context -import com.zaneschepke.logcatter.model.LogLevel -import com.zaneschepke.logcatter.model.LogMessage -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.io.BufferedReader -import java.io.File -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStreamReader -import java.nio.file.Files -import java.nio.file.Paths -import java.nio.file.StandardOpenOption - -object LogcatUtil { - private const val MAX_FILE_SIZE = 2097152L // 2MB - private const val MAX_FOLDER_SIZE = 10485760L // 10MB - private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex() - private val findIpv6AddressRegex = - """(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:) - |{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:) - |{1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F] - |{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:) - |fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]) - |{0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1 - |{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])) - """.trimMargin().toRegex() - private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex() - private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex() - private const val CHORE = "Choreographer" - - private object LogcatHelperInit { - var maxFileSize: Long = MAX_FILE_SIZE - var maxFolderSize: Long = MAX_FOLDER_SIZE - var pID: Int = 0 - var publicAppDirectory = "" - var logcatPath = "" - } - - fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, context: Context): LocalLogCollector { - if (maxFileSize > maxFolderSize) { - throw IllegalStateException("maxFileSize must be less than maxFolderSize") - } - synchronized(LogcatHelperInit) { - LogcatHelperInit.maxFileSize = maxFileSize - LogcatHelperInit.maxFolderSize = maxFolderSize - LogcatHelperInit.pID = android.os.Process.myPid() - context.getExternalFilesDir(null)?.let { - LogcatHelperInit.publicAppDirectory = it.absolutePath - LogcatHelperInit.logcatPath = - LogcatHelperInit.publicAppDirectory + File.separator + "logs" - val logDirectory = File(LogcatHelperInit.logcatPath) - if (!logDirectory.exists()) { - logDirectory.mkdir() - } - } - return Logcat - } - } - - internal object Logcat : LocalLogCollector { - private var logcatReader: LogcatReader? = null - - override suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) { - logcatReader ?: run { - logcatReader = - LogcatReader( - LogcatHelperInit.pID.toString(), - LogcatHelperInit.logcatPath, - onLogMessage, - ) - } - logcatReader?.run() - } - - override fun stop() { - logcatReader?.stopLogs() - logcatReader = null - } - - private fun mergeLogsApi26(sourceDir: String, outputFile: File) { - val outputFilePath = Paths.get(outputFile.absolutePath) - val logcatPath = Paths.get(sourceDir) - - Files.list(logcatPath).use { - it.sorted { o1, o2 -> - Files.getLastModifiedTime(o1).compareTo(Files.getLastModifiedTime(o2)) - } - .flatMap(Files::lines).use { lines -> - lines.forEach { line -> - Files.write( - outputFilePath, - (line + System.lineSeparator()).toByteArray(), - StandardOpenOption.CREATE, - StandardOpenOption.APPEND, - ) - } - } - } - } - - override suspend fun getLogFile(): Result { - stop() - return withContext(Dispatchers.IO) { - try { - val outputDir = - File(LogcatHelperInit.publicAppDirectory + File.separator + "output") - val outputFile = File(outputDir.absolutePath + File.separator + "logs.txt") - - if (!outputDir.exists()) outputDir.mkdir() - if (outputFile.exists()) outputFile.delete() - - mergeLogsApi26(LogcatHelperInit.logcatPath, outputFile) - Result.success(outputFile) - } catch (e: Exception) { - Result.failure(e) - } finally { - start() - } - } - } - - private val _bufferedLogs = - MutableSharedFlow( - replay = 10_000, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - override val bufferedLogs: Flow = _bufferedLogs.asSharedFlow() - - private class LogcatReader( - pID: String, - private val logcatPath: String, - private val callback: ((input: LogMessage) -> Unit)?, - ) { - private var logcatProc: Process? = null - private var reader: BufferedReader? = null - private var mRunning = true - private var command = "" - private var clearLogCommand = "" - private var outputStream: FileOutputStream? = null - - init { - try { - outputStream = FileOutputStream(createLogFile(logcatPath)) - } catch (e: FileNotFoundException) { - Timber.e(e) - } - - command = "logcat -v epoch | grep \"($pID)\"" - clearLogCommand = "logcat -c" - } - - fun stopLogs() { - mRunning = false - } - - fun clear() { - Runtime.getRuntime().exec(clearLogCommand) - } - - private fun obfuscator(log: String): String { - return findKeyRegex.replace(log, "").let { first -> - findIpv6AddressRegex.replace(first, "").let { second -> - findTunnelNameRegex.replace(second, "") - } - }.let { last -> findIpv4AddressRegex.replace(last, "") } - } - - fun run() { - if (outputStream == null) return - try { - clear() - logcatProc = Runtime.getRuntime().exec(command) - reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024) - var line: String? = null - - while (mRunning && run { - line = reader!!.readLine() - line - } != null - ) { - if (!mRunning) { - break - } - if (line!!.isEmpty()) { - continue - } - - if (outputStream!!.channel.size() >= LogcatHelperInit.maxFileSize) { - outputStream!!.close() - outputStream = FileOutputStream(createLogFile(logcatPath)) - } - if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) { - deleteOldestFile(logcatPath) - } - line?.let { text -> - val obfuscated = obfuscator(text) - outputStream!!.write( - (obfuscated + System.lineSeparator()).toByteArray(), - ) - try { - val logMessage = LogMessage.from(obfuscated) - when (logMessage.level) { - LogLevel.VERBOSE -> Unit - else -> { - if (!logMessage.tag.contains(CHORE)) { - _bufferedLogs.tryEmit(logMessage) - } - } - } - callback?.let { - it(logMessage) - } - } catch (e: Exception) { - Timber.e(e) - } - } - } - } catch (e: IOException) { - Timber.e(e) - } finally { - logcatProc?.destroy() - logcatProc = null - - try { - reader?.close() - outputStream?.close() - reader = null - outputStream = null - } catch (e: IOException) { - Timber.e(e) - } - } - } - - private fun getFolderSize(path: String): Long { - File(path).run { - var size = 0L - if (this.isDirectory && this.listFiles() != null) { - for (file in this.listFiles()!!) { - size += getFolderSize(file.absolutePath) - } - } else { - size = this.length() - } - return size - } - } - - private fun createLogFile(dir: String): File { - return File(dir, "logcat_" + System.currentTimeMillis() + ".txt") - } - - private fun deleteOldestFile(path: String) { - val directory = File(path) - if (directory.isDirectory) { - directory.listFiles()?.toMutableList()?.run { - this.sortBy { it.lastModified() } - this.first().delete() - } - } - } - } - } -}