Skip to content
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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ android {
applicationId = "dev.typetype.android"
minSdk = 23
targetSdk = 37
versionCode = 10803
versionName = "1.8.0-beta.3"
versionCode = 10804
versionName = "1.8.0-beta.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resValue("string", "app_name", "TypeType")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package dev.typetype.android.feature.player.components

import android.os.Looper
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.click
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import dev.typetype.android.feature.player.state.PlayerGestureState
import org.junit.Assert.assertTrue
import org.junit.Assert.assertFalse
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class PlayerControlVisibilityTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun tapShowsControlsAfterAutomaticHideAndKeepsToggling() {
val visible = mutableStateOf(true)
val player = GestureTestPlayer(Looper.getMainLooper())
val gestures = PlayerGestureState()
composeRule.setContent {
val currentVisibility = visible.value
PlayerGestureLayer(
player = player,
state = gestures,
onSingleTap = { visible.value = !currentVisibility },
onAdjustBrightness = {},
onAdjustVolume = {},
isFullscreen = true,
modifier = Modifier.size(600.dp, 300.dp).testTag("gesture"),
)
}
tap()
composeRule.runOnIdle { assertFalse(visible.value) }
composeRule.runOnIdle { visible.value = true }
composeRule.runOnIdle { visible.value = false }
tap()
composeRule.runOnIdle { assertTrue(visible.value) }
tap()
composeRule.runOnIdle { assertFalse(visible.value) }
tap()
composeRule.runOnIdle {
assertTrue(visible.value)
player.release()
}
}

private fun tap() {
composeRule.onNodeWithTag("gesture").performTouchInput {
advanceEventTime(500L)
click()
}
composeRule.mainClock.advanceTimeBy(500L)
composeRule.waitForIdle()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.os.Looper
import android.content.res.Configuration
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView
import android.view.View
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
Expand All @@ -15,6 +17,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.ViewCompat
import androidx.core.graphics.Insets
import android.graphics.Bitmap
import java.io.File
import dev.typetype.android.R
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
Expand All @@ -33,6 +43,7 @@ import org.junit.runner.RunWith
class PlayerControlsLayoutTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
private lateinit var controlsView: View

@Test
fun portraitControlsDoNotOverlapInsideShortVideoViewport() {
Expand All @@ -49,6 +60,54 @@ class PlayerControlsLayoutTest {
assertEquals(with(composeRule.density) { 74.dp.toPx() }, height, 1f)
}

@Test
fun fullscreenTopControlsStayAnchoredWhenStatusBarsDisappear() {
composeRule.runOnUiThread {
val window = composeRule.activity.window
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowCompat.getInsetsController(window, window.decorView)
.show(WindowInsetsCompat.Type.statusBars())
}
setControls(800.dp, 360.dp, fullscreen = true)
dispatchStatusBarInsets(40)
composeRule.waitForIdle()
val visibleTop = backButtonTopInViewport()
captureControls("status-bars-visible.png")
dispatchStatusBarInsets(0)
composeRule.waitForIdle()
captureControls("status-bars-hidden.png")
assertEquals(visibleTop, backButtonTopInViewport(), 1f)
}

private fun dispatchStatusBarInsets(top: Int) {
composeRule.runOnUiThread {
ViewCompat.dispatchApplyWindowInsets(
controlsView,
WindowInsetsCompat.Builder()
.setInsets(WindowInsetsCompat.Type.statusBars(), Insets.of(0, top, 0, 0))
.setVisible(WindowInsetsCompat.Type.statusBars(), top > 0)
.build(),
)
}
}

private fun backButtonTopInViewport(): Float {
val back = composeRule.onNodeWithContentDescription(
composeRule.activity.getString(R.string.player_back),
).fetchSemanticsNode().boundsInRoot
val viewport = composeRule.onNodeWithTag(PLAYER_CONTROLS_VIEWPORT_TAG)
.fetchSemanticsNode().boundsInRoot
return back.top - viewport.top
}

private fun captureControls(name: String) {
val bitmap = composeRule.onNodeWithTag(PLAYER_CONTROLS_VIEWPORT_TAG)
.captureToImage().asAndroidBitmap()
File(composeRule.activity.noBackupFilesDir, name).outputStream().use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
}

@Test
fun tabletControlsHaveLargerTargetsWithoutOverlapping() {
setControls(720.dp, 405.dp, 720)
Expand All @@ -71,6 +130,7 @@ class PlayerControlsLayoutTest {
) {
val player = controlsLayoutPlayer()
composeRule.setContent {
controlsView = LocalView.current
val configuration = Configuration(LocalConfiguration.current).apply {
smallestScreenWidthDp = smallestWidthDp
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ class PlayerGestureLayerTest {
}
}

private class GestureTestPlayer(looper: Looper) : SimpleBasePlayer(looper) {
internal class GestureTestPlayer(looper: Looper) : SimpleBasePlayer(looper) {
private var positionMs = 20_000L
private var playWhenReady = false
private var parameters = PlaybackParameters.DEFAULT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand Down Expand Up @@ -86,7 +88,11 @@ fun PlayerControls(
.testTag(PLAYER_TOP_CONTROLS_TAG)
.then(
if (isFullscreen) {
Modifier.windowInsetsPadding(WindowInsets.statusBars)
Modifier.windowInsetsPadding(
WindowInsets.displayCutout.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal,
),
)
} else {
Modifier
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -48,6 +49,7 @@ fun PlayerGestureLayer(
onBrightnessGestureStart: () -> Float = { state.brightnessFraction.floatValue },
onVolumeGestureStart: () -> Float = { state.volumeFraction.floatValue },
) {
val currentOnSingleTap by rememberUpdatedState(onSingleTap)
var savedSpeed by remember { mutableFloatStateOf(1f) }
var holdSpeed by remember { mutableFloatStateOf(2f) }
fun restoreSpeed() {
Expand Down Expand Up @@ -182,7 +184,7 @@ fun PlayerGestureLayer(
}
.pointerInput(player, config) {
detectTapGestures(
onTap = { onSingleTap() },
onTap = { currentOnSingleTap() },
onDoubleTap = { offset ->
val action = doubleTapAction(offset.x, size.width.toFloat())
if (!action.isEnabled(config.doubleTapSeekEnabled)) {
Expand Down