Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add button to report device profile to server for troubleshooting #4482

Merged
merged 1 commit into from
Feb 26, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import org.acra.sender.ReportSenderFactory
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.TelemetryPreferences
import org.jellyfin.androidtv.util.appendCodeBlock
import org.jellyfin.androidtv.util.appendItem
import org.jellyfin.androidtv.util.appendSection
import org.jellyfin.androidtv.util.appendValue
import org.jellyfin.androidtv.util.buildMarkdown
import org.jellyfin.sdk.api.client.util.AuthorizationHeaderBuilder
import java.net.HttpURLConnection
import java.net.URL
Expand Down Expand Up @@ -85,31 +90,7 @@ object TelemetryService {
throw ReportSenderException("Unable to send crash report to server", e)
}

private fun StringBuilder.appendSection(name: String, content: StringBuilder.() -> Unit) {
appendLine("### $name")
appendLine()
content()
appendLine()
}

private fun StringBuilder.appendItem(name: String, value: StringBuilder.() -> Unit) {
append("***$name***: ")
value()
appendLine(" ")
}

private fun StringBuilder.appendCodeBlock(language: String, code: String?) {
appendLine()
appendLine("```$language")
appendLine(code ?: "<null>")
append("```")
}

private fun StringBuilder.appendValue(value: String?) {
append("`", value ?: "<null>", "`")
}

private fun CrashReportData.toReport(): String = buildString {
private fun CrashReportData.toReport(): String = buildMarkdown {
// Header
appendLine("---")
appendLine("client: Jellyfin for Android TV")
Expand All @@ -118,6 +99,7 @@ object TelemetryService {
appendLine("type: crash_report")
appendLine("format: markdown")
appendLine("---")
appendLine()

// Content
appendSection("Logs") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.jellyfin.androidtv.data.model.DataRefreshService;
import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.androidtv.preference.UserSettingPreferences;
import org.jellyfin.androidtv.preference.constant.AudioBehavior;
import org.jellyfin.androidtv.preference.constant.NextUpBehavior;
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior;
import org.jellyfin.androidtv.preference.constant.ZoomMode;
Expand Down Expand Up @@ -475,9 +474,7 @@ public void onClick(DialogInterface dialog, int which) {
// undo setting mSeekPosition for liveTV
if (isLiveTv) mSeekPosition = -1;

int maxBitrate = Utils.getMaxBitrate(userPreferences.getValue());
Timber.d("Max bitrate is: %d", maxBitrate);
VideoOptions internalOptions = buildExoPlayerOptions(forcedSubtitleIndex, item, maxBitrate);
VideoOptions internalOptions = buildExoPlayerOptions(forcedSubtitleIndex, item);

playInternal(getCurrentlyPlayingItem(), position, internalOptions);
mPlaybackState = PlaybackState.BUFFERING;
Expand All @@ -493,7 +490,7 @@ public void onClick(DialogInterface dialog, int which) {
}

@NonNull
private VideoOptions buildExoPlayerOptions(@Nullable Integer forcedSubtitleIndex, BaseItemDto item, int maxBitrate) {
private VideoOptions buildExoPlayerOptions(@Nullable Integer forcedSubtitleIndex, BaseItemDto item) {
VideoOptions internalOptions = new VideoOptions();
internalOptions.setItemId(item.getId());
internalOptions.setMediaSources(item.getMediaSources());
Expand All @@ -511,10 +508,8 @@ private VideoOptions buildExoPlayerOptions(@Nullable Integer forcedSubtitleIndex
internalOptions.setMediaSourceId(currentMediaSource.getId());
}
DeviceProfile internalProfile = DeviceProfileKt.createDeviceProfile(
maxBitrate,
!internalOptions.getEnableDirectStream(),
userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled()),
userPreferences.getValue().get(UserPreferences.Companion.getAudioBehaviour()) == AudioBehavior.DOWNMIX_TO_STEREO
userPreferences.getValue(),
!internalOptions.getEnableDirectStream()
);
internalOptions.setProfile(internalProfile);
return internalOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui.preference.screen

import android.text.format.Formatter
import coil3.ImageLoader
import coil3.annotation.ExperimentalCoilApi
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.SystemPreferences
Expand Down Expand Up @@ -64,7 +63,6 @@ class DeveloperPreferencesScreen : OptionsFragment() {
bind(userPreferences, UserPreferences.preferExoPlayerFfmpeg)
}

@OptIn(ExperimentalCoilApi::class)
action {
setTitle(R.string.clear_image_cache)
content = getString(R.string.clear_image_cache_content, Formatter.formatFileSize(context, imageLoader.diskCache?.size ?: 0))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
package org.jellyfin.androidtv.ui.preference.screen

import android.os.Build
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.constant.getQualityProfiles
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
import org.jellyfin.androidtv.preference.constant.ZoomMode
import org.jellyfin.androidtv.ui.preference.custom.DurationSeekBarPreference
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.action
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
import org.jellyfin.androidtv.ui.preference.dsl.enum
import org.jellyfin.androidtv.ui.preference.dsl.list
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
import org.jellyfin.androidtv.ui.preference.dsl.seekbar
import org.jellyfin.androidtv.util.TimeUtils
import org.jellyfin.androidtv.util.profile.createDeviceProfileReport
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.clientLogApi
import org.koin.android.ext.android.inject
import timber.log.Timber

class PlaybackAdvancedPreferencesScreen : OptionsFragment() {
private val api: ApiClient by inject()
private val userPreferences: UserPreferences by inject()
private var deviceProfileReported = false

override val screen by optionsScreen {
setTitle(R.string.pref_playback)
Expand Down Expand Up @@ -84,7 +94,7 @@ class PlaybackAdvancedPreferencesScreen : OptionsFragment() {
bind(userPreferences, UserPreferences.playerZoomMode)
}

checkbox{
checkbox {
setTitle(R.string.pref_external_player)
bind(userPreferences, UserPreferences.useExternalPlayer)
}
Expand Down Expand Up @@ -116,5 +126,39 @@ class PlaybackAdvancedPreferencesScreen : OptionsFragment() {
bind(userPreferences, UserPreferences.ac3Enabled)
}
}

category {
setTitle(R.string.pref_troubleshooting)

action {
setTitle(R.string.pref_report_device_profile_title)
setContent(R.string.pref_report_device_profile_summary)

depends { !deviceProfileReported }

onActivate = {
deviceProfileReported = true

lifecycleScope.launch {
runCatching {
api.clientLogApi.logFile(createDeviceProfileReport(context, userPreferences)).content
}.fold(
onSuccess = { result ->
Toast.makeText(
context,
getString(R.string.pref_report_device_profile_success, result.fileName),
Toast.LENGTH_LONG
).show()
},
onFailure = { error ->
Timber.e(error, "Failed to upload device profile")
Toast.makeText(context, R.string.pref_report_device_profile_failure, Toast.LENGTH_LONG).show()
deviceProfileReported = false
}
)
}
}
}
}
}
}
60 changes: 60 additions & 0 deletions app/src/main/java/org/jellyfin/androidtv/util/MarkdownBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.jellyfin.androidtv.util

class MarkdownBuilder : Appendable, CharSequence {
private val stringBuilder = StringBuilder()

override val length: Int get() = stringBuilder.length
override fun get(index: Int): Char = stringBuilder[index]
override fun subSequence(startIndex: Int, endIndex: Int) = stringBuilder.subSequence(startIndex, endIndex)

override fun append(value: CharSequence?): MarkdownBuilder {
stringBuilder.append(value)
return this
}

override fun append(value: CharSequence?, p1: Int, p2: Int): MarkdownBuilder {
stringBuilder.append(value)
return this
}

override fun append(value: Char): MarkdownBuilder {
stringBuilder.append(value)
return this
}

override fun toString(): String {
return stringBuilder.toString()
}
}

inline fun buildMarkdown(builderAction: MarkdownBuilder.() -> Unit): String {
return MarkdownBuilder().apply(builderAction).toString()
}

fun MarkdownBuilder.appendSection(name: String? = null, depth: Int = 3, content: MarkdownBuilder.() -> Unit) {
if (name != null) {
appendLine("${"#".repeat(depth)} $name")
}

if (last().category != CharCategory.LINE_SEPARATOR) appendLine()
content()
appendLine()
}

fun MarkdownBuilder.appendItem(name: String, value: MarkdownBuilder.() -> Unit) {
append("***$name***: ")
value()
appendLine(" ")
}

fun MarkdownBuilder.appendCodeBlock(language: String, code: String?) {
if (last().category != CharCategory.LINE_SEPARATOR) appendLine()

appendLine("```$language")
appendLine(code ?: "<null>")
appendLine("```")
}

fun MarkdownBuilder.appendValue(value: String?) {
append("`", value ?: "<null>", "`")
}
31 changes: 0 additions & 31 deletions app/src/main/java/org/jellyfin/androidtv/util/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.jellyfin.androidtv.util

import android.content.Context
import android.widget.Toast
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import org.koin.core.component.KoinComponent
Expand Down Expand Up @@ -49,42 +48,12 @@ object Utils : KoinComponent {
@JvmStatic
fun isTrue(value: Boolean?): Boolean = value == true

/**
* A null safe version of `String.equalsIgnoreCase`.
*/
@JvmStatic
fun equalsIgnoreCase(str1: String?, str2: String?): Boolean = when {
str1 == null && str2 == null -> true
str1 == null || str2 == null -> false
else -> str1.equals(str2, ignoreCase = true)
}

@JvmStatic
fun <T> getSafeValue(value: T?, defaultValue: T): T = value ?: defaultValue

@JvmStatic
fun isEmpty(value: String?): Boolean = value.isNullOrEmpty()

@JvmStatic
fun isNonEmpty(value: String?): Boolean = !value.isNullOrEmpty()

@JvmStatic
fun join(separator: String, items: Iterable<String?>): String = items.joinToString(separator = separator)

@JvmStatic
fun join(separator: String, vararg items: String?): String = join(separator, items.toList())

@JvmStatic
fun getMaxBitrate(userPreferences: UserPreferences): Int {
var maxRate = userPreferences[UserPreferences.maxBitrate]

// Use default when value is what was previously "auto"
if (maxRate == "0") maxRate = UserPreferences.maxBitrate.defaultValue

// Convert megabit to bit
return (maxRate.toFloat() * 1_000_000).toInt()
}

@JvmStatic
fun getThemeColor(context: Context, resourceId: Int): Int {
val styledAttributes = context.theme.obtainStyledAttributes(intArrayOf(resourceId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import androidx.media3.common.MimeTypes
import org.jellyfin.androidtv.constant.Codec
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.sdk.model.api.CodecType
import org.jellyfin.sdk.model.api.DlnaProfileType
import org.jellyfin.sdk.model.api.EncodingContext
Expand Down Expand Up @@ -39,6 +41,23 @@
Codec.Audio.VORBIS,
)

private fun UserPreferences.getMaxBitrate(): Int {
var maxBitrate = this[UserPreferences.maxBitrate].toIntOrNull()

// The value "0" was used in an older release, make sure we prevent that from being used to avoid video not playing
if (maxBitrate == null || maxBitrate < 1) maxBitrate = UserPreferences.maxBitrate.defaultValue.toInt()

// Convert megabit to bit
return maxBitrate * 1_000_000

Check warning

Code scanning / detekt

Report magic numbers. Magic number is a numeric literal that is not defined as a constant and hence it's unclear what the purpose of this number is. It's better to declare such numbers as constants and give them a proper name. By default, -1, 0, 1, and 2 are not considered to be magic numbers. Warning

This expression contains a magic number. Consider defining it to a well named constant.
}

fun createDeviceProfile(userPreferences: UserPreferences, disableDirectPlay: Boolean) = createDeviceProfile(
maxBitrate = userPreferences.getMaxBitrate(),
disableDirectPlay = disableDirectPlay,
isAC3Enabled = userPreferences[UserPreferences.ac3Enabled],
downMixAudio = userPreferences[UserPreferences.audioBehaviour] == AudioBehavior.DOWNMIX_TO_STEREO,
)

fun createDeviceProfile(
maxBitrate: Int,
disableDirectPlay: Boolean,
Expand Down
Loading
Loading