Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@
android:resource="@xml/file_paths" />
</provider>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_file_paths" />
</provider>

</application>

</manifest>
22 changes: 19 additions & 3 deletions app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.acra.ktx.initAcra
import org.acra.security.TLS
import org.acra.sender.HttpSender
import org.grakovne.lissen.common.RunningComponent
import org.grakovne.lissen.logging.LissenLogProvider
import timber.log.Timber
import javax.inject.Inject

Expand All @@ -20,6 +21,9 @@ class LissenApplication : Application() {
@Inject
lateinit var runningComponents: Set<@JvmSuppressWildcards RunningComponent>

@Inject
lateinit var lissenLogProvider: LissenLogProvider

override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)

Expand All @@ -32,10 +36,11 @@ class LissenApplication : Application() {
super.onCreate()
appContext = applicationContext

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
initLogging()
initRunningComponents()
}

private fun initRunningComponents() {
runningComponents.forEach {
try {
it.onCreate()
Expand All @@ -45,6 +50,17 @@ class LissenApplication : Application() {
}
}

private fun initLogging() {
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}

try {
Timber.plant(lissenLogProvider.provideLoggingTree())
} catch (_: Exception) {
}
}

private fun initCrashReporting() {
initAcra {
ACRA.DEV_LOGGING = true
Expand Down
124 changes: 124 additions & 0 deletions app/src/main/kotlin/org/grakovne/lissen/logging/FileLoggingTree.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.grakovne.lissen.logging

import android.util.Log
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

class FileLoggingTree(
private val logFile: File,
private val maxSizeBytes: Int = 1024 * 1024,
private val trimThresholdBytes: Int = 1280 * 1024,
) : Timber.DebugTree() {
private val lock = Any()

override fun log(
priority: Int,
tag: String?,
message: String,
t: Throwable?,
) {
val line = buildLogLine(priority, tag, message, t)

synchronized(lock) {
try {
logFile.parentFile?.mkdirs()
appendLine(line)

if (logFile.length() > trimThresholdBytes) {
trimToLastMegabyte()
}
} catch (_: IOException) {
}
}
}

private fun buildLogLine(
priority: Int,
tag: String?,
message: String,
t: Throwable?,
): String {
val timestamp = TIMESTAMP_FORMATTER.format(LocalDateTime.now())
val level = priority.toShortLevel()

return buildString(message.length + 128) {
append(timestamp)
append(' ')
append(level)
append('/')
append(tag ?: DEFAULT_TAG)
append(": ")
append(message)

if (t != null) {
append('\n')
append(Log.getStackTraceString(t))
}

append('\n')
}
}

@Throws(IOException::class)
private fun appendLine(line: String) {
logFile.appendText(line, StandardCharsets.UTF_8)
}

@Throws(IOException::class)
private fun trimToLastMegabyte() {
val fileLength = logFile.length()
if (fileLength <= maxSizeBytes) return

RandomAccessFile(logFile, "rw").use { raf ->
val startOffset = fileLength - maxSizeBytes.toLong()
raf.seek(startOffset)

val buffer = ByteArray(maxSizeBytes)
val bytesRead = raf.read(buffer)
if (bytesRead <= 0) return

val writeOffset = findTrimStartOffset(buffer, bytesRead)

raf.seek(0)
raf.write(buffer, writeOffset, bytesRead - writeOffset)
raf.setLength((bytesRead - writeOffset).toLong())
}
}

private fun findTrimStartOffset(
buffer: ByteArray,
bytesRead: Int,
): Int {
for (i in 0 until bytesRead) {
if (buffer[i] == '\n'.code.toByte()) {
val next = i + 1
if (next < bytesRead) return next
break
}
}

return 0
}

private fun Int.toShortLevel(): Char =
when (this) {
Log.VERBOSE -> 'V'
Log.DEBUG -> 'D'
Log.INFO -> 'I'
Log.WARN -> 'W'
Log.ERROR -> 'E'
Log.ASSERT -> 'A'
else -> '?'
}

private companion object {
const val DEFAULT_TAG = "TAG"
val TIMESTAMP_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.grakovne.lissen.logging

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class LissenLogProvider
@Inject
constructor(
@ApplicationContext private val context: Context,
) {
private val tree: FileLoggingTree by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
FileLoggingTree(profileLogFile())
}

fun profileLogFile(): File = File(context.cacheDir, FILE_LOG_NAME)

fun provideLoggingTree(): FileLoggingTree = tree

companion object {
private const val FILE_LOG_NAME = "lissen_log.txt"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.grakovne.lissen.ui.screens.settings.advanced

import android.content.Context
import android.content.Intent
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -34,6 +36,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import kotlinx.coroutines.launch
import org.grakovne.lissen.R
Expand All @@ -43,6 +46,8 @@ import org.grakovne.lissen.ui.screens.settings.composable.PlaybackVolumeBoostSet
import org.grakovne.lissen.ui.screens.settings.composable.SettingsToggleItem
import org.grakovne.lissen.viewmodel.CachingModelView
import org.grakovne.lissen.viewmodel.SettingsViewModel
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter

@OptIn(ExperimentalMaterial3Api::class)
@Composable
Expand Down Expand Up @@ -146,6 +151,12 @@ fun AdvancedSettingsComposable(
).show()
},
)

AdvancedSettingsSimpleItemComposable(
title = stringResource(R.string.export_logs_title),
description = stringResource(R.string.export_logs_description),
onclick = { shareLogs(context, viewModel) },
)
}

if (softwareCodecsEnabledOnStart != softwareCodecsEnabled) {
Expand Down Expand Up @@ -197,3 +208,53 @@ fun SoftwareCodecsPreferenceBanner(modifier: Modifier = Modifier) {
}
}
}

private fun shareLogs(
context: Context,
viewModel: SettingsViewModel,
) {
val logFile = viewModel.provideLogFileOrNull()

if (logFile == null) {
Toast.makeText(context, context.getString(R.string.export_logs_no_logs), Toast.LENGTH_SHORT).show()
return
}

val uri =
FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
logFile,
)

val exportTimestamp = OffsetDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XXX")

val formattedTimestamp = exportTimestamp.format(formatter)

val sizeKb = logFile.length() / 1024

val subject =
"${context.getString(R.string.app_name)} logs • $formattedTimestamp • $sizeKb KB"

val details =
buildString {
appendLine(context.getString(R.string.app_name))
appendLine(formattedTimestamp)
appendLine("$sizeKb KB")
}

val shareIntent =
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"

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"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import org.grakovne.lissen.lib.domain.connection.LocalUrl
import org.grakovne.lissen.lib.domain.connection.LocalUrl.Companion.clean
import org.grakovne.lissen.lib.domain.connection.ServerRequestHeader
import org.grakovne.lissen.lib.domain.connection.ServerRequestHeader.Companion.clean
import org.grakovne.lissen.logging.LissenLogProvider
import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences
import java.io.File
import javax.inject.Inject

@HiltViewModel
Expand All @@ -30,6 +32,7 @@ class SettingsViewModel
constructor(
private val mediaChannel: LissenMediaProvider,
private val preferences: LissenSharedPreferences,
private val logProvider: LissenLogProvider,
) : ViewModel() {
private val _host: MutableLiveData<Host> = MutableLiveData(preferences.getHost()?.let { Host.external(it) })
val host = _host
Expand Down Expand Up @@ -95,6 +98,12 @@ class SettingsViewModel
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 preferCrashReporting(value: Boolean) {
_crashReporting.postValue(value)
preferences.saveAcraEnabled(value)
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,7 @@
<string name="settings_screen_client_cert_remove_action">Отвязать сертификат</string>
<string name="settings_screen_client_cert_picker_cancelled_toast">mTLS не найден</string>
<string name="login_error_client_cert_error">mTLS не прошел проверку на сервере</string>
<string name="export_logs_title">Выгрузить логи</string>
<string name="export_logs_description">Поделиться данными приложения для отладки</string>
<string name="export_logs_no_logs">Файл с логами не найден</string>
</resources>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,7 @@
<string name="connection_settings_title">Connection preferences</string>
<string name="connection_settings_description">Control server address, proxy, and SSL settings</string>
<string name="disconnect_from_server_title">Disconnect from the server</string>
<string name="export_logs_title">Export logs</string>
<string name="export_logs_description">Share diagnostic data for troubleshooting</string>
<string name="export_logs_no_logs">No logs available</string>
</resources>
6 changes: 6 additions & 0 deletions app/src/main/res/xml/log_file_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path
name="logs"
path="." />
</paths>
Loading