Add a debug page (#32)

Click the version in settings 3 times to open this new debug page which
displays internal information such as settings, server info, app info,
and database info. It will also show recently logcat messages.
This commit is contained in:
damontecres 2025-10-18 15:48:04 -04:00 committed by GitHub
parent f57fd25f8e
commit 405e170750
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 325 additions and 2 deletions

View file

@ -22,4 +22,7 @@ interface ItemPlaybackDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun saveItem(item: ItemPlayback): Long fun saveItem(item: ItemPlayback): Long
@Query("SELECT * from ItemPlayback WHERE userId=:userId")
fun getItems(userId: Int): List<ItemPlayback>
} }

View file

@ -13,6 +13,7 @@ import androidx.room.Query
import androidx.room.Relation import androidx.room.Relation
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import java.util.UUID import java.util.UUID
@Entity(tableName = "servers") @Entity(tableName = "servers")
@ -43,7 +44,10 @@ data class JellyfinUser(
@ColumnInfo(index = true) @ColumnInfo(index = true)
val serverId: UUID, val serverId: UUID,
val accessToken: String?, val accessToken: String?,
) ) {
override fun toString(): String =
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken=${accessToken.isNotNullOrBlank()})"
}
data class JellyfinServerUsers( data class JellyfinServerUsers(
@Embedded val server: JellyfinServer, @Embedded val server: JellyfinServer,

View file

@ -0,0 +1,310 @@
package com.github.damontecres.wholphin.ui.detail
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.BuildConfig
import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
import javax.inject.Inject
@HiltViewModel
class DebugViewModel
@Inject
constructor(
val serverRepository: ServerRepository,
val itemPlaybackDao: ItemPlaybackDao,
) : ViewModel() {
val itemPlaybacks = MutableLiveData<List<ItemPlayback>>(listOf())
val logcat = MutableLiveData<List<LogcatLine>>(listOf())
init {
viewModelScope.launchIO {
serverRepository.currentUser?.rowId?.let {
val results = itemPlaybackDao.getItems(it)
withContext(Dispatchers.Main) {
itemPlaybacks.value = results
}
val logcat = getLogCatLines()
withContext(Dispatchers.Main) {
this@DebugViewModel.logcat.value = logcat
}
}
}
}
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
}
count++
}
} finally {
process.destroy()
}
return logLines
}
}
data class LogcatLine(
val level: Int,
val text: String,
)
@Composable
fun DebugPage(
preferences: UserPreferences,
modifier: Modifier = Modifier,
viewModel: DebugViewModel = hiltViewModel(),
) {
val scrollAmount = 100f
val columnState = rememberLazyListState()
val scope = rememberCoroutineScope()
fun scroll(reverse: Boolean = false) {
scope.launch(ExceptionHandler()) {
columnState.scrollBy(if (reverse) -scrollAmount else scrollAmount)
}
}
val itemPlaybacks by viewModel.itemPlaybacks.observeAsState(listOf())
val logcat by viewModel.logcat.observeAsState(listOf())
LazyColumn(
state = columnState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(32.dp),
modifier =
modifier
.focusable()
.background(
MaterialTheme.colorScheme.surface,
).onKeyEvent {
if (it.type == KeyEventType.KeyUp) {
return@onKeyEvent false
}
if (it.key == Key.DirectionDown) {
scroll(false)
return@onKeyEvent true
}
if (it.key == Key.DirectionUp) {
scroll(true)
return@onKeyEvent true
}
return@onKeyEvent false
},
) {
item {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "AppPreferences",
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = preferences.appPreferences.toString(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "App Information",
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "Build type: ${BuildConfig.BUILD_TYPE}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "Debug enabled: ${BuildConfig.DEBUG}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "User Information",
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "Current server: ${viewModel.serverRepository.currentServer}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "Current user: ${viewModel.serverRepository.currentUser}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "User server settings: ${preferences.userConfig}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "Database",
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "ItemPlayback",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
itemPlaybacks.forEach {
Text(
text = it.toString(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "Logcat",
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
logcat.forEach { (level, line) ->
val color =
when (level) {
Log.VERBOSE -> MaterialTheme.colorScheme.onSurface
Log.DEBUG -> Color(0xff2bc4cf)
Log.INFO -> Color(0xff2bcf8b)
Log.WARN -> Color(0xffdde663)
Log.ERROR -> Color(0xffe67063)
else -> MaterialTheme.colorScheme.onSurface
}
Text(
text = line,
style = MaterialTheme.typography.bodySmall,
color = color,
)
}
}
}
}
}
private val THIRD_PARTY_TAGS =
listOf(
"libc:F",
"ExoPlayerImpl:W",
// FireTV
"Codec2Client:E",
"CCodecBuffers:E",
"CCodecConfig:E",
"okhttp.Http2:W",
"okhttp.TaskRunner:W",
"LruBitmapPool:W",
"FragmentManager:W",
"ConfigStore:W",
"GlideRequest:W",
"FactoryPools:W",
"ViewTarget:W",
"Engine:W",
"Downsampler:W",
"TransformationUtils:W",
"DecodeJob:W",
"BufferPoolAccessor2.0:W",
"ExifInterface:W",
"MediaCodec:W",
"SurfaceUtils:W",
"ByteArrayPool:W",
"HardwareConfig:W",
"DfltImageHeaderParser:W",
)

View file

@ -90,4 +90,7 @@ sealed class Destination(
@Serializable @Serializable
data object License : Destination(true) data object License : Destination(true)
@Serializable
data object Debug : Destination(true)
} }

View file

@ -8,6 +8,7 @@ import com.github.damontecres.wholphin.ui.components.LicenseInfo
import com.github.damontecres.wholphin.ui.detail.CollectionFolderGeneric import com.github.damontecres.wholphin.ui.detail.CollectionFolderGeneric
import com.github.damontecres.wholphin.ui.detail.CollectionFolderMovie import com.github.damontecres.wholphin.ui.detail.CollectionFolderMovie
import com.github.damontecres.wholphin.ui.detail.CollectionFolderTv import com.github.damontecres.wholphin.ui.detail.CollectionFolderTv
import com.github.damontecres.wholphin.ui.detail.DebugPage
import com.github.damontecres.wholphin.ui.detail.PlaylistDetails import com.github.damontecres.wholphin.ui.detail.PlaylistDetails
import com.github.damontecres.wholphin.ui.detail.SeriesDetails import com.github.damontecres.wholphin.ui.detail.SeriesDetails
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
@ -174,5 +175,7 @@ fun DestinationContent(
userPreferences = preferences, userPreferences = preferences,
modifier = modifier, modifier = modifier,
) )
Destination.Debug -> DebugPage(preferences, modifier)
} }
} }

View file

@ -192,7 +192,7 @@ fun PreferencesContent(
if (movementSounds) playOnClickSound(context) if (movementSounds) playOnClickSound(context)
if (clickCount++ >= 2) { if (clickCount++ >= 2) {
clickCount = 0 clickCount = 0
// navigationManager.navigateTo(Destination.Debug) viewModel.navigationManager.navigateTo(Destination.Debug)
} }
}, },
summary = installedVersion.toString(), summary = installedVersion.toString(),