Send crash reports to the server (#65)

Adds ability to send app crash reports to the last connected server. The
app prompts you before sending. There is an advanced settings to disable
this as well.

Additionally, there is an advanced settings button to send the app logs
to the current server.

The resulting reports can be found on the server's web UI under
Dashboard->Logs

If sharing the logs, make sure there's no private information present!
This commit is contained in:
damontecres 2025-10-26 07:35:42 -04:00 committed by GitHub
parent 05f3811149
commit 2667090485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 278 additions and 41 deletions

View file

@ -68,46 +68,48 @@ class DebugViewModel
}
}
fun getLogCatLines(): List<LogcatLine> {
val lineCount = 500
val args =
buildList {
add("logcat")
add("-d")
add("-t")
add(lineCount.toString())
addAll(THIRD_PARTY_TAGS)
add("*:V")
}
val process = ProcessBuilder().command(args).redirectErrorStream(true).start()
val logLines = mutableListOf<LogcatLine>()
try {
val reader = BufferedReader(InputStreamReader(process.inputStream))
var count = 0
while (count < lineCount) {
val line = reader.readLine()
if (line != null) {
val level = line.split(" ").getOrNull(4)
val logLevel =
when (level?.uppercase()) {
"V" -> Log.VERBOSE
"D" -> Log.DEBUG
"I" -> Log.INFO
"W" -> Log.WARN
"E" -> Log.ERROR
else -> Log.VERBOSE
}
logLines.add(LogcatLine(logLevel, line))
} else {
break
companion object {
fun getLogCatLines(): List<LogcatLine> {
val lineCount = 500
val args =
buildList {
add("logcat")
add("-d")
add("-t")
add(lineCount.toString())
addAll(THIRD_PARTY_TAGS)
add("*:V")
}
count++
val process = ProcessBuilder().command(args).redirectErrorStream(true).start()
val logLines = mutableListOf<LogcatLine>()
try {
val reader = BufferedReader(InputStreamReader(process.inputStream))
var count = 0
while (count < lineCount) {
val line = reader.readLine()
if (line != null) {
val level = line.split(" ").getOrNull(4)
val logLevel =
when (level?.uppercase()) {
"V" -> Log.VERBOSE
"D" -> Log.DEBUG
"I" -> Log.INFO
"W" -> Log.WARN
"E" -> Log.ERROR
else -> Log.VERBOSE
}
logLines.add(LogcatLine(logLevel, line))
} else {
break
}
count++
}
} finally {
process.destroy()
}
} finally {
process.destroy()
return logLines
}
return logLines
}
}

View file

@ -283,6 +283,19 @@ fun PreferencesContent(
}
}
AppPreference.SendAppLogs -> {
ClickPreference(
title = stringResource(pref.title),
onClick = {
viewModel.sendAppLogs()
},
modifier = Modifier,
summary = pref.summary(context, null),
onLongClick = {},
interactionSource = interactionSource,
)
}
else -> {
val value = pref.getter.invoke(preferences)
ComposablePreference(

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.preferences
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@ -11,25 +12,36 @@ import com.github.damontecres.wholphin.data.isPinned
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
import com.github.damontecres.wholphin.data.model.NavPinType
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.detail.DebugViewModel
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.RememberTabManager
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.clientLogApi
import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo
import javax.inject.Inject
@HiltViewModel
class PreferencesViewModel
@Inject
constructor(
@param:ApplicationContext private val context: Context,
private val api: ApiClient,
val preferenceDataStore: DataStore<AppPreferences>,
val navigationManager: NavigationManager,
val rememberTabManager: RememberTabManager,
val serverRepository: ServerRepository,
val navDrawerItemRepository: NavDrawerItemRepository,
val serverPreferencesDao: ServerPreferencesDao,
private val rememberTabManager: RememberTabManager,
private val serverRepository: ServerRepository,
private val navDrawerItemRepository: NavDrawerItemRepository,
private val serverPreferencesDao: ServerPreferencesDao,
private val deviceInfo: DeviceInfo,
private val clientInfo: ClientInfo,
) : ViewModel(),
RememberTabManager by rememberTabManager {
private lateinit var allNavDrawerItems: List<NavDrawerItem>
@ -77,4 +89,19 @@ class PreferencesViewModel
}
}
}
fun sendAppLogs() {
viewModelScope.launchIO(ExceptionHandler(true)) {
val logcat = DebugViewModel.getLogCatLines().joinToString("\n") { it.text }
val body =
"""
Send App Logs
$clientInfo
$deviceInfo
""".trimIndent() + logcat
val response by api.clientLogApi.logFile(body)
showToast(context, "Sent! Filename=${response.fileName}")
}
}
}