From fad66ebbcc93e09ee60cd35384fc9268304f5f4d Mon Sep 17 00:00:00 2001 From: Max Grakov Date: Tue, 26 May 2026 23:25:27 +0300 Subject: [PATCH] Feature/optional logging (#416) --- app/build.gradle.kts | 2 + .../org/grakovne/lissen/LissenApplication.kt | 12 +- .../lissen/content/cache/common/ImageBlur.kt | 2 + .../cache/temporary/CachedBookmarkProvider.kt | 2 +- .../lissen/logging/LissenLogProvider.kt | 35 ++++++ .../preferences/LissenSharedPreferences.kt | 8 ++ .../advanced/AdvancedSettingsComposable.kt | 82 ++++++------- .../AdvancedSettingsSimpleItemComposable.kt | 6 +- .../advanced/LocalUrlSettingsScreen.kt | 52 ++------- .../lissen/viewmodel/SettingsViewModel.kt | 17 ++- app/src/main/res/values-ru/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + .../lissen/logging/LissenLogProviderTest.kt | 110 ++++++++++++++++++ gradle/libs.versions.toml | 3 + 14 files changed, 244 insertions(+), 95 deletions(-) create mode 100644 app/src/test/kotlin/org/grakovne/lissen/logging/LissenLogProviderTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f270bac6..0ab70147 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -185,11 +185,13 @@ dependencies { implementation(libs.converter.moshi) implementation(libs.moshi) + implementation(libs.zip4j) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) testImplementation(libs.junit.jupiter) + testImplementation(libs.mockk) testRuntimeOnly(libs.junit.platform.launcher) androidTestImplementation(libs.androidx.test.ext.junit) diff --git a/app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt b/app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt index 3e828df7..cb2443a6 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt @@ -13,6 +13,7 @@ import org.acra.security.TLS import org.acra.sender.HttpSender import org.grakovne.lissen.common.RunningComponent import org.grakovne.lissen.logging.LissenLogProvider +import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences import timber.log.Timber import javax.inject.Inject @@ -24,6 +25,9 @@ class LissenApplication : Application() { @Inject lateinit var lissenLogProvider: LissenLogProvider + @Inject + lateinit var preferences: LissenSharedPreferences + override fun attachBaseContext(base: Context) { super.attachBaseContext(base) @@ -55,9 +59,11 @@ class LissenApplication : Application() { Timber.plant(Timber.DebugTree()) } - try { - Timber.plant(lissenLogProvider.provideLoggingTree()) - } catch (_: Exception) { + if (preferences.isActivityLoggingEnabled()) { + try { + Timber.plant(lissenLogProvider.provideLoggingTree()) + } catch (_: Exception) { + } } } diff --git a/app/src/main/kotlin/org/grakovne/lissen/content/cache/common/ImageBlur.kt b/app/src/main/kotlin/org/grakovne/lissen/content/cache/common/ImageBlur.kt index 5db87c70..046b1fe3 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/content/cache/common/ImageBlur.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/content/cache/common/ImageBlur.kt @@ -49,6 +49,8 @@ private fun sourceWithBackdropBlur( scaled.recycle() + scaled.recycle() + val backdrop = Bitmap.createBitmap(blurredPadded, padding / 2, padding / 2, size, size) blurredPadded.recycle() diff --git a/app/src/main/kotlin/org/grakovne/lissen/content/cache/temporary/CachedBookmarkProvider.kt b/app/src/main/kotlin/org/grakovne/lissen/content/cache/temporary/CachedBookmarkProvider.kt index 8309a395..4cfd46a5 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/content/cache/temporary/CachedBookmarkProvider.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/content/cache/temporary/CachedBookmarkProvider.kt @@ -19,7 +19,7 @@ class CachedBookmarkProvider @Inject constructor( private val channelProvider: AudiobookshelfChannelProvider, - private val localCacheRepository: LocalCacheRepository + private val localCacheRepository: LocalCacheRepository, ) { val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) diff --git a/app/src/main/kotlin/org/grakovne/lissen/logging/LissenLogProvider.kt b/app/src/main/kotlin/org/grakovne/lissen/logging/LissenLogProvider.kt index cf01620a..2321c184 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/logging/LissenLogProvider.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/logging/LissenLogProvider.kt @@ -2,6 +2,11 @@ package org.grakovne.lissen.logging import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import net.lingala.zip4j.ZipFile +import net.lingala.zip4j.model.ZipParameters +import net.lingala.zip4j.model.enums.CompressionLevel +import net.lingala.zip4j.model.enums.CompressionMethod +import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences import java.io.File import javax.inject.Inject import javax.inject.Singleton @@ -11,6 +16,7 @@ class LissenLogProvider @Inject constructor( @param:ApplicationContext private val context: Context, + private val preferences: LissenSharedPreferences, ) { private val tree: FileLoggingTree by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { FileLoggingTree(profileLogFile()) @@ -20,7 +26,36 @@ class LissenLogProvider fun provideLoggingTree(): FileLoggingTree = tree + fun enableLogging() { + preferences.saveActivityLoggingEnabled(true) + } + + fun disableLogging() { + preferences.saveActivityLoggingEnabled(false) + } + + fun archiveLogFile(): File? { + val logFile = profileLogFile() + if (!logFile.exists() || logFile.length() == 0L) return null + + val archiveFile = File(context.cacheDir, FILE_LOG_ARCHIVE_NAME) + archiveFile.delete() + + val parameters = + ZipParameters().apply { + compressionMethod = CompressionMethod.DEFLATE + compressionLevel = CompressionLevel.ULTRA + } + + ZipFile(archiveFile).use { zip -> + zip.addFile(logFile, parameters) + } + + return archiveFile + } + companion object { private const val FILE_LOG_NAME = "lissen_log.txt" + private const val FILE_LOG_ARCHIVE_NAME = "lissen_logs.zip" } } diff --git a/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt b/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt index af32d80e..e6b6fccf 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt @@ -499,6 +499,13 @@ class LissenSharedPreferences putBoolean(KEY_SOFTWARE_CODECS, value) } + fun isActivityLoggingEnabled(): Boolean = sharedPreferences.getBoolean(KEY_ACTIVITY_LOGGING, true) + + fun saveActivityLoggingEnabled(value: Boolean) = + sharedPreferences.edit { + putBoolean(KEY_ACTIVITY_LOGGING, value) + } + fun getHideCompleted(): Boolean = sharedPreferences.getBoolean(KEY_HIDE_COMPLETED, false) fun saveHideCompleted(value: Boolean) = @@ -534,6 +541,7 @@ class LissenSharedPreferences private const val KEY_AUTO_DOWNLOAD_DELAYED = "auto_download_delayed" private const val KEY_PREFERRED_LIBRARY_ORDERING = "preferred_library_ordering" private const val KEY_SOFTWARE_CODECS = "software_codecs" + private const val KEY_ACTIVITY_LOGGING = "activity_logging_enabled" private const val KEY_HIDE_COMPLETED = "hide_completed" private const val KEY_CUSTOM_HEADERS = "custom_headers" diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt index 1d568b37..9f141d3c 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt @@ -6,7 +6,6 @@ import android.os.Build import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -16,6 +15,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Description import androidx.compose.material.icons.outlined.Memory import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -24,7 +24,6 @@ import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -43,6 +42,7 @@ import org.grakovne.lissen.R import org.grakovne.lissen.common.restartApplication import org.grakovne.lissen.ui.navigation.AppNavigationService import org.grakovne.lissen.ui.screens.settings.composable.PlaybackVolumeBoostSettingsComposable +import org.grakovne.lissen.ui.screens.settings.composable.SettingsInfoBanner import org.grakovne.lissen.ui.screens.settings.composable.SettingsToggleItem import org.grakovne.lissen.viewmodel.CachingModelView import org.grakovne.lissen.viewmodel.SettingsViewModel @@ -63,6 +63,8 @@ fun AdvancedSettingsComposable( val materialYouColorsEnabled by viewModel.materialYouEnabled.observeAsState(false) val softwareCodecsEnabled by viewModel.softwareCodecsEnabled.observeAsState(false) val softwareCodecsEnabledOnStart = viewModel.softwareCodecsEnabledOnStart + val activityLoggingEnabled by viewModel.activityLoggingEnabled.observeAsState(true) + val activityLoggingEnabledOnStart = viewModel.activityLoggingEnabledOnStart val context = LocalContext.current val scope = rememberCoroutineScope() @@ -152,9 +154,16 @@ fun AdvancedSettingsComposable( }, ) + SettingsToggleItem( + title = stringResource(R.string.settings_screen_activity_logging_title), + description = stringResource(R.string.settings_screen_activity_logging_description), + initialState = activityLoggingEnabled, + ) { viewModel.preferActivityLoggingEnabled(it) } + AdvancedSettingsSimpleItemComposable( title = stringResource(R.string.export_logs_title), description = stringResource(R.string.export_logs_description), + enabled = activityLoggingEnabledOnStart, onclick = { shareLogs(context, viewModel) }, ) } @@ -162,6 +171,10 @@ fun AdvancedSettingsComposable( if (softwareCodecsEnabledOnStart != softwareCodecsEnabled) { SoftwareCodecsPreferenceBanner() } + + if (activityLoggingEnabledOnStart != activityLoggingEnabled) { + ActivityLoggingPreferenceBanner() + } } }, ) @@ -170,52 +183,34 @@ fun AdvancedSettingsComposable( @Composable fun SoftwareCodecsPreferenceBanner(modifier: Modifier = Modifier) { val context = LocalContext.current + SettingsInfoBanner( + icon = Icons.Outlined.Memory, + text = stringResource(R.string.restart_the_app_to_start_using_the_new_codecs_title), + ctaText = stringResource(R.string.restart_the_app_to_start_using_the_new_codecs_cta), + onAction = { context.restartApplication() }, + modifier = modifier, + ) +} - Row( - modifier = - modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Outlined.Memory, - contentDescription = null, - tint = colorScheme.primary, - modifier = Modifier.padding(end = 12.dp), - ) - - Text( - text = stringResource(R.string.restart_the_app_to_start_using_the_new_codecs_title), - style = - typography.bodyMedium.copy( - color = colorScheme.onSurface, - ), - modifier = Modifier.weight(1f), - ) - - TextButton( - onClick = { context.restartApplication() }, - ) { - Text( - text = stringResource(R.string.restart_the_app_to_start_using_the_new_codecs_cta), - style = - typography.bodyMedium.copy( - color = colorScheme.primary, - fontWeight = FontWeight.SemiBold, - ), - ) - } - } +@Composable +fun ActivityLoggingPreferenceBanner(modifier: Modifier = Modifier) { + val context = LocalContext.current + SettingsInfoBanner( + icon = Icons.Outlined.Description, + text = stringResource(R.string.restart_the_app_to_apply_logging_changes_title), + ctaText = stringResource(R.string.restart_the_app_to_apply_logging_changes_cta), + onAction = { context.restartApplication() }, + modifier = modifier, + ) } private fun shareLogs( context: Context, viewModel: SettingsViewModel, ) { - val logFile = viewModel.provideLogFileOrNull() + val archiveFile = viewModel.provideLogArchiveOrNull() - if (logFile == null) { + if (archiveFile == null) { Toast.makeText(context, context.getString(R.string.export_logs_no_logs), Toast.LENGTH_SHORT).show() return } @@ -224,7 +219,7 @@ private fun shareLogs( FileProvider.getUriForFile( context, "${context.packageName}.fileprovider", - logFile, + archiveFile, ) val exportTimestamp = OffsetDateTime.now() @@ -232,7 +227,7 @@ private fun shareLogs( val formattedTimestamp = exportTimestamp.format(formatter) - val sizeKb = logFile.length() / 1024 + val sizeKb = archiveFile.length() / 1024 val subject = "${context.getString(R.string.app_name)} logs • $formattedTimestamp • $sizeKb KB" @@ -246,14 +241,13 @@ private fun shareLogs( val shareIntent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" + type = "application/zip" putExtra(Intent.EXTRA_STREAM, uri) putExtra(Intent.EXTRA_SUBJECT, subject) putExtra(Intent.EXTRA_TEXT, details) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } context.startActivity(Intent.createChooser(shareIntent, "Export logs")) diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsSimpleItemComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsSimpleItemComposable.kt index 155476b6..135ea7d3 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsSimpleItemComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsSimpleItemComposable.kt @@ -19,13 +19,14 @@ import androidx.compose.ui.unit.dp fun AdvancedSettingsSimpleItemComposable( title: String, description: String, + enabled: Boolean = true, onclick: () -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() - .clickable { onclick() } + .let { if (enabled) it.clickable { onclick() } else it } .padding(start = 24.dp, end = 12.dp, top = 12.dp, bottom = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -38,11 +39,12 @@ fun AdvancedSettingsSimpleItemComposable( modifier = Modifier.padding(bottom = 4.dp), maxLines = 1, overflow = TextOverflow.Ellipsis, + color = if (enabled) colorScheme.onBackground else colorScheme.onBackground.copy(alpha = 0.4f), ) Text( text = description, style = typography.bodyMedium, - color = colorScheme.onSurfaceVariant, + color = if (enabled) colorScheme.onSurfaceVariant else colorScheme.onSurfaceVariant.copy(alpha = 0.4f), maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/LocalUrlSettingsScreen.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/LocalUrlSettingsScreen.kt index 5589793b..12528816 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/LocalUrlSettingsScreen.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/LocalUrlSettingsScreen.kt @@ -49,6 +49,7 @@ import kotlinx.coroutines.launch import org.grakovne.lissen.R import org.grakovne.lissen.lib.domain.connection.LocalUrl import org.grakovne.lissen.ui.screens.common.hasLocationPermission +import org.grakovne.lissen.ui.screens.settings.composable.SettingsInfoBanner import org.grakovne.lissen.viewmodel.SettingsViewModel import kotlin.math.max @@ -182,46 +183,17 @@ fun LocationPermissionBanner( modifier: Modifier = Modifier, onResult: (Boolean) -> Unit, ) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Default.LocationOn, - contentDescription = null, - tint = colorScheme.primary, - modifier = Modifier.padding(end = 12.dp), + val permissionRequestLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = onResult, ) - val permissionRequestLauncher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - onResult = onResult, - ) - - Text( - text = stringResource(R.string.location_permission_request_hint), - style = - typography.bodyMedium.copy( - color = colorScheme.onSurface, - ), - modifier = Modifier.weight(1f), - ) - - TextButton( - onClick = { permissionRequestLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }, - ) { - Text( - text = stringResource(R.string.permission_request_grant_button), - style = - typography.bodyMedium.copy( - color = colorScheme.primary, - fontWeight = FontWeight.SemiBold, - ), - ) - } - } + SettingsInfoBanner( + icon = Icons.Default.LocationOn, + text = stringResource(R.string.location_permission_request_hint), + ctaText = stringResource(R.string.permission_request_grant_button), + onAction = { permissionRequestLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }, + modifier = modifier, + ) } diff --git a/app/src/main/kotlin/org/grakovne/lissen/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/org/grakovne/lissen/viewmodel/SettingsViewModel.kt index 0b2bcf1e..8f353d73 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/viewmodel/SettingsViewModel.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/viewmodel/SettingsViewModel.kt @@ -93,17 +93,18 @@ class SettingsViewModel val softwareCodecsEnabled: LiveData = _softwareCodecsEnabled val softwareCodecsEnabledOnStart: Boolean = preferences.getSoftwareCodecsEnabled() + private val _activityLoggingEnabled = MutableLiveData(preferences.isActivityLoggingEnabled()) + + val activityLoggingEnabled: LiveData = _activityLoggingEnabled + val activityLoggingEnabledOnStart: Boolean = preferences.isActivityLoggingEnabled() + private val _hideCompleted = preferences.hideCompletedFlow val hideCompleted = _hideCompleted private val _autoDownloadDelayed = MutableLiveData(preferences.getAutoDownloadDelayed()) val autoDownloadDelayed = _autoDownloadDelayed - fun provideLogFileOrNull(): File? { - val logFile = logProvider.profileLogFile() - - return logFile.takeIf { it.exists() && it.length() > 0L } - } + fun provideLogArchiveOrNull(): File? = logProvider.archiveLogFile() fun preferCrashReporting(value: Boolean) { Timber.d("User action: preferCrashReporting $value") @@ -249,6 +250,12 @@ class SettingsViewModel preferences.saveSoftwareCodecsEnabled(value) } + fun preferActivityLoggingEnabled(value: Boolean) { + Timber.d("User action: preferActivityLoggingEnabled $value") + _activityLoggingEnabled.postValue(value) + if (value) logProvider.enableLogging() else logProvider.disableLogging() + } + fun preferAutoDownloadOption(option: DownloadOption?) { Timber.d("User action: preferAutoDownloadOption $option") _preferredAutoDownloadOption.postValue(option) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9b7df5e5..994c7120 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -190,6 +190,10 @@ Отвязать сертификат mTLS не найден mTLS не прошел проверку на сервере + Записывать логи + Сохранять диагностические данные в файл + Перезапустите приложение, чтобы применить настройки логирования + Перезапустить Экспортировать логи Выгрузить данные приложения для отладки Файл с логами не найден diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f81a657a..e0e3c214 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -188,6 +188,10 @@ Connection preferences Control server address, proxy, and SSL settings Disconnect from the server + Activity Logging + Record diagnostic events to a local file + Restart the app to apply logging changes + Restart Export logs Share diagnostic data for troubleshooting No logs available diff --git a/app/src/test/kotlin/org/grakovne/lissen/logging/LissenLogProviderTest.kt b/app/src/test/kotlin/org/grakovne/lissen/logging/LissenLogProviderTest.kt new file mode 100644 index 00000000..a28a9f79 --- /dev/null +++ b/app/src/test/kotlin/org/grakovne/lissen/logging/LissenLogProviderTest.kt @@ -0,0 +1,110 @@ +package org.grakovne.lissen.logging + +import android.content.Context +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import net.lingala.zip4j.ZipFile +import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +class LissenLogProviderTest { + @TempDir + lateinit var tempDir: File + + private lateinit var context: Context + private lateinit var preferences: LissenSharedPreferences + private lateinit var provider: LissenLogProvider + + @BeforeEach + fun setUp() { + context = mockk() + preferences = mockk() + every { context.cacheDir } returns tempDir + provider = LissenLogProvider(context, preferences) + } + + @Test + fun `archiveLogFile returns null when log file does not exist`() { + assertNull(provider.archiveLogFile()) + } + + @Test + fun `archiveLogFile returns null when log file is empty`() { + provider.profileLogFile().createNewFile() + + assertNull(provider.archiveLogFile()) + } + + @Test + fun `archiveLogFile returns zip file when log file has content`() { + provider.profileLogFile().writeText("some log content") + + val archive = provider.archiveLogFile() + + assertNotNull(archive) + assertTrue(archive!!.exists()) + assertTrue(archive.length() > 0) + } + + @Test + fun `archiveLogFile produces valid zip containing the log file`() { + val logContent = "line1\nline2\nline3" + provider.profileLogFile().writeText(logContent) + + val archive = provider.archiveLogFile()!! + + val zip = ZipFile(archive) + assertTrue(zip.isValidZipFile) + + val entry = zip.fileHeaders.single() + assertEquals(provider.profileLogFile().name, entry.fileName) + } + + @Test + fun `archiveLogFile overwrites previous archive on repeated calls`() { + provider.profileLogFile().writeText("first run") + val first = provider.archiveLogFile()!! + + provider.profileLogFile().writeText("second run with more content to make it larger") + val second = provider.archiveLogFile()!! + + assertEquals(first.absolutePath, second.absolutePath) + } + + @Test + fun `disableLogging saves preference as false`() { + provider.profileLogFile().writeText("logs") + justRun { preferences.saveActivityLoggingEnabled(false) } + + provider.disableLogging() + + verify { preferences.saveActivityLoggingEnabled(false) } + } + + @Test + fun `disableLogging is safe when log file does not exist`() { + justRun { preferences.saveActivityLoggingEnabled(false) } + + provider.disableLogging() + + assertTrue(!provider.profileLogFile().exists()) + } + + @Test + fun `enableLogging saves preference as true`() { + justRun { preferences.saveActivityLoggingEnabled(true) } + + provider.enableLogging() + + verify { preferences.saveActivityLoggingEnabled(true) } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3c59bc85..6d649538 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,7 @@ junitJupiter = "6.1.0" mockk = "1.14.9" androidxTestExtJunit = "1.3.0" androidxTestRunner = "1.7.0" +zip4j = "2.11.5" [libraries] acra-core = { module = "ch.acra:acra-core", version.ref = "acraCore" } @@ -89,9 +90,11 @@ moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } androidx-media3-ffmpeg-decoder = { module = "org.jellyfin.media3:media3-ffmpeg-decoder", version.ref = "media3Ffmpeg" } process-phoenix = { module = "com.jakewharton:process-phoenix", version.ref = "processPhoenix" } +zip4j = { module = "net.lingala.zip4j:zip4j", version.ref = "zip4j" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junitJupiter" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitJupiter" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExtJunit" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" }