Improvements to debug page (#1538)

## Description
Makes some improvements to the debug page. Also adds some audio device
listing for debugging.

### Related issues
N/A

### Testing
Emulator & shield

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-06-08 13:45:48 -04:00 • committed by GitHub
parent b49926f80b
commit 93d601e7bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2,6 +2,10 @@ package com.github.damontecres.wholphin.ui.detail
import android.content.Context import android.content.Context
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioFormat
import android.media.AudioManager
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.Display import android.view.Display
@ -12,11 +16,14 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -25,7 +32,6 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -33,16 +39,18 @@ import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.BuildConfig import com.github.damontecres.wholphin.BuildConfig
import com.github.damontecres.wholphin.MainActivity
import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.util.versionCodeLong import org.acra.util.versionCodeLong
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -59,17 +67,18 @@ import javax.inject.Inject
class DebugViewModel class DebugViewModel
@Inject @Inject
constructor( constructor(
@param:ApplicationContext private val context: Context,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val itemPlaybackDao: ItemPlaybackDao, val itemPlaybackDao: ItemPlaybackDao,
val clientInfo: ClientInfo, val clientInfo: ClientInfo,
val deviceInfo: DeviceInfo, val deviceInfo: DeviceInfo,
) : ViewModel() { ) : ViewModel() {
val state = MutableStateFlow(DebugState())
val itemPlaybacks = MutableStateFlow<List<ItemPlayback>>(emptyList()) val itemPlaybacks = MutableStateFlow<List<ItemPlayback>>(emptyList())
val logcat = MutableStateFlow<List<LogcatLine>>(emptyList()) val logcat = MutableStateFlow<List<LogcatLine>>(emptyList())
val supportedModes by lazy { val supportedModes by lazy {
val displayManager = val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
MainActivity.instance.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
display.supportedModes.orEmpty() display.supportedModes.orEmpty()
} }
@ -103,6 +112,59 @@ class DebugViewModel
} }
init { init {
viewModelScope.launchDefault {
val buildTime = Date(BuildConfig.BUILD_TIME)
val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val installInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val installSource =
context.packageManager.getInstallSourceInfo(context.packageName)
buildList {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add("Install source: ${installSource.packageSource}")
}
add("Installer: ${installSource.installingPackageName}")
add("Initiator: ${installSource.initiatingPackageName}")
}
} else {
listOf(
context.packageManager
.getInstallerPackageName(context.packageName)
.toString(),
)
}
val appInfo =
listOf(
"Version Name: ${pkgInfo.versionName}",
"Version Code: ${pkgInfo.versionCodeLong}",
"ClientInfo: $clientInfo",
"Build type: ${BuildConfig.BUILD_TYPE}",
"Build flavor: ${BuildConfig.FLAVOR}",
"Build time: $buildTime",
"FFMPEG included: $ffmpegIncluded",
"AV1 included: $av1Included",
"libmpv loaded: $libMpvLoaded",
"Debug enabled: ${BuildConfig.DEBUG}",
"ABIs: ${Build.SUPPORTED_ABIS.toList()}",
) + installInfo
state.update { it.copy(appInfo = appInfo) }
}
viewModelScope.launchDefault {
val deviceInfoList =
listOf(
"DeviceInfo: $deviceInfo",
"Manufacturer: ${Build.MANUFACTURER}",
"Model: ${Build.MODEL}",
"API Level: ${Build.VERSION.SDK_INT}",
)
state.update {
it.copy(
deviceInfo = deviceInfoList,
displayModes = supportedModes.map { it.toString() },
)
}
}
viewModelScope.launchIO { viewModelScope.launchIO {
serverRepository.currentUser?.rowId?.let { serverRepository.currentUser?.rowId?.let {
val results = itemPlaybackDao.getItems(it) val results = itemPlaybackDao.getItems(it)
@ -113,6 +175,39 @@ class DebugViewModel
val logcat = getLogCatLines() val logcat = getLogCatLines()
this@DebugViewModel.logcat.value = logcat this@DebugViewModel.logcat.value = logcat
} }
viewModelScope.launchDefault {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val callback =
object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<AudioDeviceInfo>) {
state.update {
it.copy(
audioInfo =
it.audioInfo.toMutableList().apply {
addAll(
addedDevices
.filter { it.isSink }
.map { it.details },
)
},
)
}
}
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
state.update {
it.copy(
audioInfo =
it.audioInfo.toMutableList().apply {
removeAll(removedDevices.map { it.details })
},
)
}
}
}
audioManager.registerAudioDeviceCallback(callback, null)
addCloseable { audioManager.unregisterAudioDeviceCallback(callback) }
}
} }
companion object { companion object {
@ -189,18 +284,144 @@ data class LogcatLine(
val text: String, val text: String,
) )
data class DebugState(
val appInfo: List<String> = emptyList(),
val deviceInfo: List<String> = emptyList(),
val displayModes: List<String> = emptyList(),
val audioInfo: List<String> = emptyList(),
)
val AudioDeviceInfo.details: String
get() {
val typeName =
when (type) {
AudioDeviceInfo.TYPE_HDMI -> "HDMI"
AudioDeviceInfo.TYPE_HDMI_ARC -> "ARC"
AudioDeviceInfo.TYPE_HDMI_EARC -> "eARC"
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "Speaker"
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE -> "Speaker Safe"
else -> "N/A"
}
val addressStr =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
address
} else {
null
}
val encodings =
encodings.map { enc ->
when (enc) {
AudioFormat.ENCODING_INVALID -> "INVALID"
AudioFormat.ENCODING_PCM_16BIT -> "PCM_16BIT"
AudioFormat.ENCODING_PCM_8BIT -> "PCM_8BIT"
AudioFormat.ENCODING_PCM_FLOAT -> "PCM_FLOAT"
AudioFormat.ENCODING_AC3 -> "AC3"
AudioFormat.ENCODING_E_AC3 -> "E_AC3"
AudioFormat.ENCODING_DTS -> "DTS"
AudioFormat.ENCODING_DTS_HD -> "DTS_HD"
AudioFormat.ENCODING_MP3 -> "MP3"
AudioFormat.ENCODING_AAC_LC -> "AAC_LC"
AudioFormat.ENCODING_AAC_HE_V1 -> "AAC_HE_V1"
AudioFormat.ENCODING_AAC_HE_V2 -> "AAC_HE_V2"
AudioFormat.ENCODING_IEC61937 -> "IEC61937"
AudioFormat.ENCODING_DOLBY_TRUEHD -> "DOLBY_TRUEHD"
AudioFormat.ENCODING_AAC_ELD -> "AAC_ELD"
AudioFormat.ENCODING_AAC_XHE -> "AAC_XHE"
AudioFormat.ENCODING_AC4 -> "AC4"
// AudioFormat.ENCODING_AC4_L4->"AC4_L4"
AudioFormat.ENCODING_E_AC3_JOC -> "E_AC3_JOC"
AudioFormat.ENCODING_DOLBY_MAT -> "DOLBY_MAT"
AudioFormat.ENCODING_OPUS -> "OPUS"
AudioFormat.ENCODING_PCM_24BIT_PACKED -> "PCM_24BIT_PACKED"
AudioFormat.ENCODING_PCM_32BIT -> "PCM_32BIT"
AudioFormat.ENCODING_MPEGH_BL_L3 -> "MPEGH_BL_L3"
AudioFormat.ENCODING_MPEGH_BL_L4 -> "MPEGH_BL_L4"
AudioFormat.ENCODING_MPEGH_LC_L3 -> "MPEGH_LC_L3"
AudioFormat.ENCODING_MPEGH_LC_L4 -> "MPEGH_LC_L4"
AudioFormat.ENCODING_DTS_UHD_P1 -> "DTS_UHD_P1"
AudioFormat.ENCODING_DRA -> "DRA"
AudioFormat.ENCODING_DTS_HD_MA -> "DTS_HD_MA"
AudioFormat.ENCODING_DTS_UHD_P2 -> "DTS_UHD_P2"
AudioFormat.ENCODING_DSD -> "DSD"
AudioFormat.ENCODING_IAMF_BASE_ENHANCED_PROFILE_AAC -> "IAMF_BASE_ENHANCED_PROFILE_AAC"
AudioFormat.ENCODING_IAMF_BASE_ENHANCED_PROFILE_FLAC -> "IAMF_BASE_ENHANCED_PROFILE_FLAC"
AudioFormat.ENCODING_IAMF_BASE_ENHANCED_PROFILE_OPUS -> "IAMF_BASE_ENHANCED_PROFILE_OPUS"
AudioFormat.ENCODING_IAMF_BASE_ENHANCED_PROFILE_PCM -> "IAMF_BASE_ENHANCED_PROFILE_PCM"
AudioFormat.ENCODING_IAMF_BASE_PROFILE_AAC -> "IAMF_BASE_PROFILE_AAC"
AudioFormat.ENCODING_IAMF_BASE_PROFILE_FLAC -> "IAMF_BASE_PROFILE_FLAC"
AudioFormat.ENCODING_IAMF_BASE_PROFILE_OPUS -> "IAMF_BASE_PROFILE_OPUS"
AudioFormat.ENCODING_IAMF_BASE_PROFILE_PCM -> "IAMF_BASE_PROFILE_PCM"
AudioFormat.ENCODING_IAMF_SIMPLE_PROFILE_AAC -> "IAMF_SIMPLE_PROFILE_AAC"
AudioFormat.ENCODING_IAMF_SIMPLE_PROFILE_FLAC -> "IAMF_SIMPLE_PROFILE_FLAC"
AudioFormat.ENCODING_IAMF_SIMPLE_PROFILE_OPUS -> "IAMF_SIMPLE_PROFILE_OPUS"
AudioFormat.ENCODING_IAMF_SIMPLE_PROFILE_PCM -> "IAMF_SIMPLE_PROFILE_PCM"
else -> "invalid encoding $enc"
}
}
return "AudioDeviceInfo(id=$id, type=$type ($typeName), " +
"channelCounts=${channelCounts.contentToString()}, " +
"encodings=$encodings, " +
"productName=$productName, address=$addressStr)"
}
@Composable @Composable
fun DebugPage( fun DebugPage(
preferences: UserPreferences, preferences: UserPreferences,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: DebugViewModel = hiltViewModel(), viewModel: DebugViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current
val scrollAmount = 100f
val columnState = rememberLazyListState() val columnState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state by viewModel.state.collectAsState()
fun scroll(reverse: Boolean = false) { fun scroll(
reverse: Boolean = false,
scrollAmount: Float = 100f,
) {
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
columnState.scrollBy(if (reverse) -scrollAmount else scrollAmount) columnState.scrollBy(if (reverse) -scrollAmount else scrollAmount)
} }
@ -209,10 +430,12 @@ fun DebugPage(
val itemPlaybacks by viewModel.itemPlaybacks.collectAsState() val itemPlaybacks by viewModel.itemPlaybacks.collectAsState()
val logcat by viewModel.logcat.collectAsState() val logcat by viewModel.logcat.collectAsState()
val padding = remember { PaddingValues(bottom = 32.dp) }
LazyColumn( LazyColumn(
state = columnState, state = columnState,
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(32.dp), verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = modifier =
modifier modifier
.focusable() .focusable()
@ -230,150 +453,94 @@ fun DebugPage(
scroll(true) scroll(true)
return@onKeyEvent true return@onKeyEvent true
} }
if (it.key == Key.MediaFastForward || it.key == Key.PageDown) {
scroll(false, 300f)
return@onKeyEvent true
}
if (it.key == Key.MediaSkipBackward || it.key == Key.PageUp) {
scroll(true, 300f)
return@onKeyEvent true
}
return@onKeyEvent false return@onKeyEvent false
}, },
) { ) {
item { item {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.padding(padding),
) { ) {
Text( SectionTitle("App Information")
text = "App Information", state.appInfo.forEach {
style = MaterialTheme.typography.displaySmall, BodyText(it)
color = MaterialTheme.colorScheme.onSurface,
)
val buildTime = Date(BuildConfig.BUILD_TIME)
val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val installInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val installSource =
context.packageManager.getInstallSourceInfo(context.packageName)
buildList {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add("Install source: ${installSource.packageSource}")
}
add("Installer: ${installSource.installingPackageName}")
add("Initiator: ${installSource.initiatingPackageName}")
}
} else {
listOf(context.packageManager.getInstallerPackageName(context.packageName))
}
(
listOf(
"Version Name: ${pkgInfo.versionName}",
"Version Code: ${pkgInfo.versionCodeLong}",
"ClientInfo: ${viewModel.clientInfo}",
"Build type: ${BuildConfig.BUILD_TYPE}",
"Build flavor: ${BuildConfig.FLAVOR}",
"Build time: $buildTime",
"FFMPEG included: ${viewModel.ffmpegIncluded}",
"AV1 included: ${viewModel.av1Included}",
"libmpv loaded: ${viewModel.libMpvLoaded}",
"Debug enabled: ${BuildConfig.DEBUG}",
"ABIs: ${Build.SUPPORTED_ABIS.toList()}",
) + installInfo
).forEach {
Text(
text = it.toString(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
} }
} }
} }
item { item {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.padding(padding),
) { ) {
Text( SectionTitle("Device Information")
text = "Device Information", state.deviceInfo.forEach {
style = MaterialTheme.typography.displaySmall, BodyText(it)
color = MaterialTheme.colorScheme.onSurface, }
)
listOf( SubSectionTitle("Display Modes")
"DeviceInfo: ${viewModel.deviceInfo}", state.displayModes.forEach {
"Manufacturer: ${Build.MANUFACTURER}", BodyText(it)
"Model: ${Build.MODEL}", }
"API Level: ${Build.VERSION.SDK_INT}",
"Display Modes:", SubSectionTitle("Audio Devices")
*viewModel.supportedModes, state.audioInfo.forEach {
).forEach { BodyText(it)
Text(
text = it.toString(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
} }
} }
} }
item { item {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.padding(padding),
) { ) {
Text( SectionTitle("AppPreferences")
text = "AppPreferences", BodyText(preferences.appPreferences.toString())
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = preferences.appPreferences.toString(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
} }
} }
item { item {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.padding(padding),
) { ) {
Text( SectionTitle("User Information")
text = "User Information", BodyText("Current server: ${viewModel.serverRepository.currentServer}")
style = MaterialTheme.typography.displaySmall, BodyText("Current user: ${viewModel.serverRepository.currentUser}")
color = MaterialTheme.colorScheme.onSurface, BodyText("User server settings: ${viewModel.serverRepository.currentUserDto?.configuration}")
)
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: ${viewModel.serverRepository.currentUserDto?.configuration}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
} }
} }
item { item {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.padding(padding),
) { ) {
Text( SectionTitle("Database")
text = "Database",
style = MaterialTheme.typography.displaySmall, SubSectionTitle("ItemPlayback")
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "ItemPlayback",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
itemPlaybacks.forEach { itemPlaybacks.forEach {
Text( BodyText(it.toString())
text = it.toString(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
} }
} }
} }
@ -382,12 +549,10 @@ fun DebugPage(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Text( SectionTitle("Logcat")
text = "Logcat", }
style = MaterialTheme.typography.displaySmall, }
color = MaterialTheme.colorScheme.onSurface, items(logcat) { (level, line) ->
)
logcat.forEach { (level, line) ->
val color = val color =
when (level) { when (level) {
Log.VERBOSE -> MaterialTheme.colorScheme.onSurface Log.VERBOSE -> MaterialTheme.colorScheme.onSurface
@ -405,7 +570,32 @@ fun DebugPage(
} }
} }
} }
@Composable
private fun SectionTitle(text: String) {
Text(
text = text,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
)
} }
@Composable
private fun SubSectionTitle(text: String) {
Text(
text = text,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
@Composable
private fun BodyText(text: String) {
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
} }
private val THIRD_PARTY_TAGS = private val THIRD_PARTY_TAGS =