Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
Expand Down Expand Up @@ -124,6 +125,7 @@ internal fun FrontendScreen(
val pendingDialog by viewModel.pendingDialog.collectAsStateWithLifecycle()
val pendingFileChooser by viewModel.pendingFileChooser.collectAsStateWithLifecycle()
val autoPlayVideoEnabled by viewModel.autoPlayVideoEnabled.collectAsStateWithLifecycle()
val keepScreenOnEnabled by viewModel.keepScreenOnEnabled.collectAsStateWithLifecycle()

// The fullscreen View handed over by the WebView is Activity-scoped. Keep it in screen
// state so it does not leak across configuration changes via the ViewModel.
Expand Down Expand Up @@ -173,6 +175,7 @@ internal fun FrontendScreen(
onGesture = viewModel::onGesture,
onExoPlayerFullscreenChanged = viewModel::onExoPlayerFullscreenChanged,
autoPlayVideoEnabled = autoPlayVideoEnabled,
keepScreenOnEnabled = keepScreenOnEnabled,
onPipReadinessChanged = onPipReadinessChanged,
modifier = modifier,
)
Expand All @@ -198,6 +201,7 @@ internal fun FrontendScreenContent(
modifier: Modifier = Modifier,
customView: View? = null,
autoPlayVideoEnabled: Boolean = false,
keepScreenOnEnabled: Boolean = false,
pendingPermissionRequest: PermissionRequest? = null,
pendingDialog: FrontendDialog? = null,
pendingFileChooser: FileChooserRequest? = null,
Expand Down Expand Up @@ -232,6 +236,8 @@ internal fun FrontendScreenContent(
pendingRequest = pendingFileChooser,
)

KeepScreenOnEffect(enabled = keepScreenOnEnabled)

Box(modifier = modifier.fillMaxSize()) {
// Always render WebView at base layer
SafeHAWebView(
Expand Down Expand Up @@ -645,6 +651,24 @@ private fun WebViewEffects(
}
}

/**
* Toggles `View.keepScreenOn` (mapped by the platform to `WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON`)
* based on the user's "Keep screen on" preference while the frontend is composed.
*
* The flag is always cleared on dispose so it does not leak to other screens that share the
* hosting activity window.
*/
@Composable
private fun KeepScreenOnEffect(enabled: Boolean) {
val view = LocalView.current
DisposableEffect(view, enabled) {
view.keepScreenOn = enabled
onDispose {
view.keepScreenOn = false
}
}
Comment thread
TimoPtr marked this conversation as resolved.
}

/**
* Renders PiP-eligible overlays and reports their combined [PipReadiness] to the host.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,17 @@ internal class FrontendViewModel @VisibleForTesting constructor(
emitAll(prefsRepository.autoPlayVideoFlow())
}.stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = false)

/**
* The user's "Keep screen on" preference.
*
* Applied by the screen to the hosting window so the device does not lock while the
* WebView is active. Exposed as a [StateFlow] so the screen can read the current value
* synchronously when first attaching to the window and react to subsequent changes.
*/
val keepScreenOnEnabled: StateFlow<Boolean> = flow {
emitAll(prefsRepository.keepScreenOnFlow())
}.stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = false)

init {
viewModelScope.launch {
_viewState.collectLatest { state ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonObject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
Expand Down Expand Up @@ -94,9 +95,11 @@ class FrontendViewModelTest {
private val gestureHandler: FrontendGestureHandler = mockk(relaxed = true)
private val zoomSettingsFlow = MutableStateFlow(ZoomSettings())
private val autoPlayVideoFlow = MutableStateFlow(false)
private val keepScreenOnFlow = MutableStateFlow(false)
private val prefsRepository: PrefsRepository = mockk(relaxed = true) {
coEvery { this@mockk.zoomSettingsFlow() } returns this@FrontendViewModelTest.zoomSettingsFlow
coEvery { this@mockk.autoPlayVideoFlow() } returns this@FrontendViewModelTest.autoPlayVideoFlow
coEvery { this@mockk.keepScreenOnFlow() } returns this@FrontendViewModelTest.keepScreenOnFlow
}

private val serverId = 1
Expand Down Expand Up @@ -1739,4 +1742,34 @@ class FrontendViewModelTest {
assertEquals(value, viewModel.autoPlayVideoEnabled.value)
}
}

@Nested
inner class KeepScreenOnSetting {

@Test
fun `Given pref flow emits new value when collected then exposed StateFlow reflects it`() = runTest {
val viewModel = createViewModel()
advanceUntilIdle()

assertFalse(viewModel.keepScreenOnEnabled.value)

keepScreenOnFlow.value = true
advanceUntilIdle()

assertTrue(viewModel.keepScreenOnEnabled.value)
}

@ParameterizedTest
@ValueSource(booleans = [true, false])
fun `Given pref flow seeded with value when ViewModel constructed then exposed StateFlow has that value`(
value: Boolean,
) = runTest {
keepScreenOnFlow.value = value

val viewModel = createViewModel()
advanceUntilIdle()

assertEquals(value, viewModel.keepScreenOnEnabled.value)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ interface PrefsRepository {

suspend fun setKeepScreenOnEnabled(enabled: Boolean)

/** Emits the current "Keep screen on" preference immediately on collection, then on every change. */
suspend fun keepScreenOnFlow(): Flow<Boolean>

suspend fun getScreenOrientation(): String?

suspend fun saveScreenOrientation(orientation: String?)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ internal class PrefsRepositoryImpl @Inject constructor(
localStorage().putBoolean(PREF_KEEP_SCREEN_ON_ENABLED, enabled)
}

override suspend fun keepScreenOnFlow(): Flow<Boolean> {
return localStorage().observeChanges(PREF_KEEP_SCREEN_ON_ENABLED) {
isKeepScreenOnEnabled()
}
}

override suspend fun getPageZoomLevel(): Int {
return localStorage().getInt(PREF_PAGE_ZOOM_LEVEL) ?: 100
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,21 @@ class PrefsRepositoryImplTest {
}
}

@Test
fun `Given collecting flow when keep screen on changes then updated keep screen on enabled is emitted`() = runTest {
coEvery { localStorage.getBoolean("keep_screen_on_enabled") } returns false

repository.keepScreenOnFlow().test {
assertFalse(awaitItem())

coEvery { localStorage.getBoolean("keep_screen_on_enabled") } returns true
keyChangesFlow.emit("keep_screen_on_enabled")

assertTrue(awaitItem())
cancelAndIgnoreRemainingEvents()
}
}

@Nested
inner class ZoomSettingsFlow {

Expand Down
Loading