Simplify how display mode is changed (#823)

## Description
Simplifies the refresh rate/display mode change code by removing the
live data go between.

Also, instead of a `CountDownLatch` that blocks the thread, use a
`Deferred` to suspend the coroutine instead.

### Related issues
Might help with #769

### Testing
Tested on nvidia shield
This commit is contained in:
Ray 2026-02-03 15:30:24 -05:00 committed by GitHub
parent 045b689994
commit 733213d6a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 27 deletions

View file

@ -66,9 +66,12 @@ import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
import com.github.damontecres.wholphin.util.DebugLogTree import com.github.damontecres.wholphin.util.DebugLogTree
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -126,19 +129,12 @@ class MainActivity : AppCompatActivity() {
@OptIn(ExperimentalTvMaterial3Api::class) @OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
instance = this
Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}") Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}")
lifecycle.addObserver(playbackLifecycleObserver) lifecycle.addObserver(playbackLifecycleObserver)
if (savedInstanceState == null) { if (savedInstanceState == null) {
appUpgradeHandler.copySubfont(false) appUpgradeHandler.copySubfont(false)
} }
refreshRateService.refreshRateMode.observe(this) { modeId ->
// Listen for refresh rate changes
val attrs = window.attributes
if (attrs.preferredDisplayModeId != modeId) {
Timber.d("Switch preferredDisplayModeId to %s", modeId)
window.attributes = attrs.apply { preferredDisplayModeId = modeId }
}
}
viewModel.serverRepository.currentUser.observe(this) { user -> viewModel.serverRepository.currentUser.observe(this) { user ->
if (user?.hasPin == true) { if (user?.hasPin == true) {
window?.setFlags( window?.setFlags(
@ -384,6 +380,16 @@ class MainActivity : AppCompatActivity() {
} }
} }
fun changeDisplayMode(modeId: Int) {
lifecycleScope.launch(Dispatchers.Main + ExceptionHandler()) {
val attrs = window.attributes
if (attrs.preferredDisplayModeId != modeId) {
Timber.d("Switch preferredDisplayModeId to %s", modeId)
window.attributes = attrs.apply { preferredDisplayModeId = modeId }
}
}
}
companion object { companion object {
const val INTENT_ITEM_ID = "itemId" const val INTENT_ITEM_ID = "itemId"
const val INTENT_ITEM_TYPE = "itemType" const val INTENT_ITEM_TYPE = "itemType"
@ -391,6 +397,9 @@ class MainActivity : AppCompatActivity() {
const val INTENT_EPISODE_NUMBER = "epNum" const val INTENT_EPISODE_NUMBER = "epNum"
const val INTENT_SEASON_NUMBER = "seaNum" const val INTENT_SEASON_NUMBER = "seaNum"
const val INTENT_SEASON_ID = "seaId" const val INTENT_SEASON_ID = "seaId"
lateinit var instance: MainActivity
private set
} }
} }

View file

@ -6,17 +6,17 @@ import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.Display import android.view.Display
import androidx.lifecycle.LiveData import com.github.damontecres.wholphin.MainActivity
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.EqualityMutableLiveData
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.jellyfin.sdk.model.api.MediaStream import org.jellyfin.sdk.model.api.MediaStream
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -30,7 +30,6 @@ class RefreshRateService
) { ) {
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) private val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
private val originalMode = display.mode
val supportedDisplayModes get() = display.supportedModes.orEmpty() val supportedDisplayModes get() = display.supportedModes.orEmpty()
@ -44,9 +43,6 @@ class RefreshRateService
) )
} }
private val _refreshRateMode = EqualityMutableLiveData<Int>(originalMode.modeId)
val refreshRateMode: LiveData<Int> = _refreshRateMode
/** /**
* Find the best display mode for the given stream and signal to change to it * Find the best display mode for the given stream and signal to change to it
*/ */
@ -54,10 +50,10 @@ class RefreshRateService
stream: MediaStream, stream: MediaStream,
switchRefreshRate: Boolean, switchRefreshRate: Boolean,
switchResolution: Boolean, switchResolution: Boolean,
) { ) = withContext(Dispatchers.IO) {
if (!switchRefreshRate && !switchResolution) { if (!switchRefreshRate && !switchResolution) {
Timber.v("Not switching either refresh rate nor resolution") Timber.v("Not switching either refresh rate nor resolution")
return return@withContext
} }
val currentDisplayMode = display.mode val currentDisplayMode = display.mode
require(stream.type == MediaStreamType.VIDEO) { "Stream is not video" } require(stream.type == MediaStreamType.VIDEO) { "Stream is not video" }
@ -67,7 +63,7 @@ class RefreshRateService
if (switchRefreshRate) stream.realFrameRate else currentDisplayMode.refreshRate if (switchRefreshRate) stream.realFrameRate else currentDisplayMode.refreshRate
if (width == null || height == null || frameRate == null) { if (width == null || height == null || frameRate == null) {
Timber.w("Video stream missing required info: width=%s, height=%s, frameRate=%s", width, height, frameRate) Timber.w("Video stream missing required info: width=%s, height=%s, frameRate=%s", width, height, frameRate)
return return@withContext
} }
Timber.d("Getting refresh rate for: width=%s, height=%s, frameRate=%s", width, height, frameRate) Timber.d("Getting refresh rate for: width=%s, height=%s, frameRate=%s", width, height, frameRate)
val targetMode = val targetMode =
@ -86,14 +82,20 @@ class RefreshRateService
listener, listener,
Handler(Looper.myLooper() ?: Looper.getMainLooper()), Handler(Looper.myLooper() ?: Looper.getMainLooper()),
) )
_refreshRateMode.setValueOnMain(targetMode.modeId)
try { try {
if (!listener.latch.await(5, TimeUnit.SECONDS)) { MainActivity.instance.changeDisplayMode(targetMode.modeId)
val result =
withTimeoutOrNull(5.seconds) {
listener.deferred.await()
}
if (result == null) {
Timber.w("Timed out waiting for display change") Timber.w("Timed out waiting for display change")
showToast(context, "Refresh rate switch is taking a long time") showToast(context, "Refresh rate switch is taking a long time")
} }
} catch (ex: InterruptedException) { } catch (ex: Exception) {
Timber.w(ex, "Exception waiting for refresh rate switch") Timber.w(ex, "Exception waiting for refresh rate switch")
} finally {
displayManager.unregisterDisplayListener(listener)
} }
val targetRate = (targetMode.refreshRate * 1000).roundToInt() val targetRate = (targetMode.refreshRate * 1000).roundToInt()
val isSeamless = val isSeamless =
@ -110,7 +112,6 @@ class RefreshRateService
// Wait the recommended 2 seconds (https://developer.android.com/media/optimize/performance/frame-rate) // Wait the recommended 2 seconds (https://developer.android.com/media/optimize/performance/frame-rate)
delay(2.seconds) delay(2.seconds)
} }
displayManager.unregisterDisplayListener(listener)
} }
} }
@ -118,13 +119,13 @@ class RefreshRateService
* Reset the display mode to the original * Reset the display mode to the original
*/ */
fun resetRefreshRate() { fun resetRefreshRate() {
_refreshRateMode.value = originalMode.modeId MainActivity.instance.changeDisplayMode(0)
} }
private class Listener( private class Listener(
val displayId: Int, val displayId: Int,
) : DisplayManager.DisplayListener { ) : DisplayManager.DisplayListener {
val latch = CountDownLatch(1) val deferred = CompletableDeferred<Unit>()
override fun onDisplayAdded(displayId: Int) { override fun onDisplayAdded(displayId: Int) {
} }
@ -132,7 +133,7 @@ class RefreshRateService
override fun onDisplayChanged(displayId: Int) { override fun onDisplayChanged(displayId: Int) {
if (displayId == this.displayId) { if (displayId == this.displayId) {
Timber.v("Got display change for $displayId") Timber.v("Got display change for $displayId")
latch.countDown() deferred.complete(Unit)
} }
} }