Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,12 @@
plugins {
id(ThunderbirdPlugins.Library.android)
}
android {
namespace = "net.thunderbird.core.android.logging"
}
dependencies {
implementation(libs.timber)
implementation(libs.commons.io)
}

View file

@ -0,0 +1,13 @@
package net.thunderbird.core.android.logging
import org.koin.dsl.module
val loggingModule = module {
factory<ProcessExecutor> { RealProcessExecutor() }
factory<LogFileWriter> {
LogcatLogFileWriter(
contentResolver = get(),
processExecutor = get(),
)
}
}

View file

@ -0,0 +1,31 @@
package net.thunderbird.core.android.logging
import android.content.ContentResolver
import android.net.Uri
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.commons.io.IOUtils
interface LogFileWriter {
suspend fun writeLogTo(contentUri: Uri)
}
class LogcatLogFileWriter(
private val contentResolver: ContentResolver,
private val processExecutor: ProcessExecutor,
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : LogFileWriter {
override suspend fun writeLogTo(contentUri: Uri) {
return withContext(coroutineDispatcher) {
val outputStream = contentResolver.openOutputStream(contentUri, "wt")
?: error("Error opening contentUri for writing")
outputStream.use {
processExecutor.exec("logcat -d").use { inputStream ->
IOUtils.copy(inputStream, outputStream)
}
}
}
}
}

View file

@ -0,0 +1,14 @@
package net.thunderbird.core.android.logging
import java.io.InputStream
interface ProcessExecutor {
fun exec(command: String): InputStream
}
class RealProcessExecutor : ProcessExecutor {
override fun exec(command: String): InputStream {
val process = Runtime.getRuntime().exec(command)
return process.inputStream
}
}

View file

@ -0,0 +1,86 @@
package net.thunderbird.core.android.logging
import android.content.ContentResolver
import android.net.Uri
import assertk.assertThat
import assertk.assertions.isEqualTo
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
class LogcatLogFileWriterTest {
private val contentUri = mock<Uri>()
private val outputStream = ByteArrayOutputStream()
@Test
fun `write log to contentUri`() = runBlocking {
val logData = "a".repeat(10_000)
val logFileWriter = LogcatLogFileWriter(
contentResolver = createContentResolver(),
processExecutor = createProcessExecutor(logData),
coroutineDispatcher = Dispatchers.Unconfined,
)
logFileWriter.writeLogTo(contentUri)
assertThat(outputStream.toByteArray().decodeToString()).isEqualTo(logData)
}
@Test(expected = FileNotFoundException::class)
fun `contentResolver throws`() = runBlocking {
val logFileWriter = LogcatLogFileWriter(
contentResolver = createThrowingContentResolver(FileNotFoundException()),
processExecutor = createProcessExecutor("irrelevant"),
coroutineDispatcher = Dispatchers.Unconfined,
)
logFileWriter.writeLogTo(contentUri)
}
@Test(expected = IOException::class)
fun `processExecutor throws`() = runBlocking {
val logFileWriter = LogcatLogFileWriter(
contentResolver = createContentResolver(),
processExecutor = ThrowingProcessExecutor(IOException()),
coroutineDispatcher = Dispatchers.Unconfined,
)
logFileWriter.writeLogTo(contentUri)
}
private fun createContentResolver(): ContentResolver {
return mock {
on { openOutputStream(contentUri, "wt") } doReturn outputStream
}
}
private fun createThrowingContentResolver(exception: Exception): ContentResolver {
return mock {
on { openOutputStream(contentUri, "wt") } doAnswer { throw exception }
}
}
private fun createProcessExecutor(logData: String): DataProcessExecutor {
return DataProcessExecutor(logData.toByteArray(charset = Charsets.US_ASCII))
}
}
private class DataProcessExecutor(val data: ByteArray) : ProcessExecutor {
override fun exec(command: String): InputStream {
return ByteArrayInputStream(data)
}
}
private class ThrowingProcessExecutor(val exception: Exception) : ProcessExecutor {
override fun exec(command: String): InputStream {
throw exception
}
}