From 733213d6a21280fc658f946e19718e9db954d76d Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:30:24 -0500 Subject: [PATCH] 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 --- .../damontecres/wholphin/MainActivity.kt | 25 ++++++++---- .../wholphin/services/RefreshRateService.kt | 39 ++++++++++--------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index 0f8b7ee4..af88392e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -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.util.ProvideLocalClock import com.github.damontecres.wholphin.util.DebugLogTree +import com.github.damontecres.wholphin.util.ExceptionHandler import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch import okhttp3.OkHttpClient import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.serializer.toUUIDOrNull @@ -126,19 +129,12 @@ class MainActivity : AppCompatActivity() { @OptIn(ExperimentalTvMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + instance = this Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}") lifecycle.addObserver(playbackLifecycleObserver) if (savedInstanceState == null) { 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 -> if (user?.hasPin == true) { 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 { const val INTENT_ITEM_ID = "itemId" const val INTENT_ITEM_TYPE = "itemType" @@ -391,6 +397,9 @@ class MainActivity : AppCompatActivity() { const val INTENT_EPISODE_NUMBER = "epNum" const val INTENT_SEASON_NUMBER = "seaNum" const val INTENT_SEASON_ID = "seaId" + + lateinit var instance: MainActivity + private set } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt index be6e461e..d961d4c5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt @@ -6,17 +6,17 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.view.Display -import androidx.lifecycle.LiveData -import com.github.damontecres.wholphin.ui.setValueOnMain +import com.github.damontecres.wholphin.MainActivity import com.github.damontecres.wholphin.ui.showToast -import com.github.damontecres.wholphin.util.EqualityMutableLiveData import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers 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.MediaStreamType import timber.log.Timber -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton import kotlin.math.roundToInt @@ -30,7 +30,6 @@ class RefreshRateService ) { private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) - private val originalMode = display.mode val supportedDisplayModes get() = display.supportedModes.orEmpty() @@ -44,9 +43,6 @@ class RefreshRateService ) } - private val _refreshRateMode = EqualityMutableLiveData(originalMode.modeId) - val refreshRateMode: LiveData = _refreshRateMode - /** * Find the best display mode for the given stream and signal to change to it */ @@ -54,10 +50,10 @@ class RefreshRateService stream: MediaStream, switchRefreshRate: Boolean, switchResolution: Boolean, - ) { + ) = withContext(Dispatchers.IO) { if (!switchRefreshRate && !switchResolution) { Timber.v("Not switching either refresh rate nor resolution") - return + return@withContext } val currentDisplayMode = display.mode require(stream.type == MediaStreamType.VIDEO) { "Stream is not video" } @@ -67,7 +63,7 @@ class RefreshRateService if (switchRefreshRate) stream.realFrameRate else currentDisplayMode.refreshRate if (width == null || height == null || frameRate == null) { 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) val targetMode = @@ -86,14 +82,20 @@ class RefreshRateService listener, Handler(Looper.myLooper() ?: Looper.getMainLooper()), ) - _refreshRateMode.setValueOnMain(targetMode.modeId) 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") 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") + } finally { + displayManager.unregisterDisplayListener(listener) } val targetRate = (targetMode.refreshRate * 1000).roundToInt() val isSeamless = @@ -110,7 +112,6 @@ class RefreshRateService // Wait the recommended 2 seconds (https://developer.android.com/media/optimize/performance/frame-rate) delay(2.seconds) } - displayManager.unregisterDisplayListener(listener) } } @@ -118,13 +119,13 @@ class RefreshRateService * Reset the display mode to the original */ fun resetRefreshRate() { - _refreshRateMode.value = originalMode.modeId + MainActivity.instance.changeDisplayMode(0) } private class Listener( val displayId: Int, ) : DisplayManager.DisplayListener { - val latch = CountDownLatch(1) + val deferred = CompletableDeferred() override fun onDisplayAdded(displayId: Int) { } @@ -132,7 +133,7 @@ class RefreshRateService override fun onDisplayChanged(displayId: Int) { if (displayId == this.displayId) { Timber.v("Got display change for $displayId") - latch.countDown() + deferred.complete(Unit) } }