diff --git a/mobile/src/androidTest/kotlin/io/putdotio/android/MobileFullscreenVideoProofTest.kt b/mobile/src/androidTest/kotlin/io/putdotio/android/MobileFullscreenVideoProofTest.kt index 91f3a13e..659a36d4 100644 --- a/mobile/src/androidTest/kotlin/io/putdotio/android/MobileFullscreenVideoProofTest.kt +++ b/mobile/src/androidTest/kotlin/io/putdotio/android/MobileFullscreenVideoProofTest.kt @@ -32,7 +32,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.Lifecycle import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import io.putdotio.android.design.PutioTheme @@ -43,6 +42,7 @@ import io.putdotio.android.playback.MOBILE_PLAYER_SIDE_SHEET_TAG import io.putdotio.android.playback.PlaybackAudioTrack import io.putdotio.android.playback.PlaybackSubtitleTrack import io.putdotio.android.playback.languageName +import io.putdotio.android.playback.DefaultMobilePlayerFactory import io.putdotio.android.playback.MobilePlayerFactory import io.putdotio.android.playback.MobilePlayerScreen import io.putdotio.android.playback.PlaybackContent @@ -50,9 +50,7 @@ import io.putdotio.android.playback.PlaybackMediaType import io.putdotio.android.playback.PlaybackState import io.putdotio.android.playback.PlaybackTarget import io.putdotio.android.playback.SubtitleTrackIdentity -import io.putdotio.android.playback.audioAttributes import io.putdotio.android.playback.playbackAudioTracks -import io.putdotio.android.playback.playbackRenderersFactory import io.putdotio.android.playback.playbackState import io.putdotio.android.playback.playbackSubtitleTracks import io.putdotio.sdk.files.PlaybackSource @@ -359,19 +357,14 @@ private class FullscreenProofPlayerFactory : MobilePlayerFactory { var renderedFrame = false private set fun current(): Player = checkNotNull(player) - // The production player streams through the download cache's HTTP source, which cannot read - // the caller's local file; this one keeps its renderers and audio attributes. override fun create(context: Context, mediaType: PlaybackMediaType): Player = - ExoPlayer.Builder(context, playbackRenderersFactory(context)) - .setAudioAttributes(mediaType.audioAttributes(), true) - .build() - .also { - player = it - renderedFrame = false - it.addListener(object : Player.Listener { - override fun onRenderedFirstFrame() { renderedFrame = true } - }) - } + DefaultMobilePlayerFactory.create(context, mediaType).also { + player = it + renderedFrame = false + it.addListener(object : Player.Listener { + override fun onRenderedFirstFrame() { renderedFrame = true } + }) + } // This private-video proof must not stop an unrelated real audio session. override fun stopAudio(context: Context) = Unit } diff --git a/mobile/src/androidTest/kotlin/io/putdotio/android/MobilePictureInPictureProofTest.kt b/mobile/src/androidTest/kotlin/io/putdotio/android/MobilePictureInPictureProofTest.kt index 60a9beda..3965b257 100644 --- a/mobile/src/androidTest/kotlin/io/putdotio/android/MobilePictureInPictureProofTest.kt +++ b/mobile/src/androidTest/kotlin/io/putdotio/android/MobilePictureInPictureProofTest.kt @@ -15,12 +15,12 @@ import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import io.putdotio.android.design.PutioTheme import io.putdotio.android.files.FilesItemId +import io.putdotio.android.playback.DefaultMobilePlayerFactory import io.putdotio.android.playback.MobilePlayerFactory import io.putdotio.android.playback.MobilePlayerScreen import io.putdotio.android.playback.PlaybackContent @@ -28,8 +28,6 @@ import io.putdotio.android.playback.PlaybackMediaType import io.putdotio.android.playback.PlaybackPositionObserver import io.putdotio.android.playback.PlaybackTarget import io.putdotio.android.playback.SubtitleStartupPolicy -import io.putdotio.android.playback.audioAttributes -import io.putdotio.android.playback.playbackRenderersFactory import io.putdotio.android.playback.playbackState import io.putdotio.android.playback.withReportingLease import io.putdotio.sdk.files.PlaybackSource @@ -357,10 +355,7 @@ class MobilePictureInPictureProofTest { private class MenuButtons(val action: Rect, val expand: Rect, val close: Rect) -/** - * Production renderers and audio attributes over a local file, which the production player's - * download-cache HTTP source cannot read; positions go through the real observer to a recorder. - */ +/** The production player over a local file; positions go through the real observer to a recorder. */ private class PictureInPictureProofPlayerFactory(private val lease: String) : MobilePlayerFactory { var player: Player? = null private set @@ -373,21 +368,18 @@ private class PictureInPictureProofPlayerFactory(private val lease: String) : Mo fun current(): Player = checkNotNull(player) override fun create(context: Context, mediaType: PlaybackMediaType): Player = - ExoPlayer.Builder(context, playbackRenderersFactory(context)) - .setAudioAttributes(mediaType.audioAttributes(), true) - .build() - .also { - created += 1 - player = it - renderedFrame = false - it.addListener( - object : Player.Listener { - override fun onRenderedFirstFrame() { - renderedFrame = true - } - }, - ) - } + DefaultMobilePlayerFactory.create(context, mediaType).also { + created += 1 + player = it + renderedFrame = false + it.addListener( + object : Player.Listener { + override fun onRenderedFirstFrame() { + renderedFrame = true + } + }, + ) + } override fun reportableItem(item: MediaItem, useStartFrom: Boolean): MediaItem = item.withReportingLease(lease) diff --git a/mobile/src/androidTest/kotlin/io/putdotio/android/MobilePlaybackOptionsProofTest.kt b/mobile/src/androidTest/kotlin/io/putdotio/android/MobilePlaybackOptionsProofTest.kt index 857150af..95d11df5 100644 --- a/mobile/src/androidTest/kotlin/io/putdotio/android/MobilePlaybackOptionsProofTest.kt +++ b/mobile/src/androidTest/kotlin/io/putdotio/android/MobilePlaybackOptionsProofTest.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.test.hasText import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.click @@ -202,9 +203,10 @@ class MobilePlaybackOptionsProofTest { openSpeedOptions() val choice = compose.onNodeWithText("1.25×") val instrumentation = InstrumentationRegistry.getInstrumentation() - for (ignored in 0 until 8) { - if (choice.fetchSemanticsNode().config.getOrElse(SemanticsProperties.Focused) { false }) break - instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN) + // Focus opens on the speed track, which takes the arrow keys; Tab moves on to the quick speeds. + for (ignored in 0 until 12) { + if (focused("1.25×")) break + instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_TAB) compose.waitForIdle() } choice.assertIsFocused() @@ -214,6 +216,10 @@ class MobilePlaybackOptionsProofTest { } } + private fun focused(label: String) = compose.onAllNodesWithText(label).fetchSemanticsNodes().any { + it.config.getOrElse(SemanticsProperties.Focused) { false } + } + /** The audio choices: the video's Audio side sheet, or Now Playing's sheet under More. */ private fun showOptions() { showControls() @@ -268,8 +274,11 @@ class MobilePlaybackOptionsProofTest { } else { hasContentDescription("Playback speed ", substring = true) } - if (compose.onAllNodes(trigger).fetchSemanticsNodes().isEmpty()) { + // Controls that are fading out still exist but aren't displayed; a tap brings them back. + repeat(3) { + if (runCatching { compose.onNode(trigger).assertIsDisplayed() }.isSuccess) return compose.onNodeWithTag(MOBILE_PLAYER_GESTURE_TAG).performTouchInput { click() } + compose.waitForIdle() } compose.onNode(trigger).assertIsDisplayed() } diff --git a/mobile/src/main/kotlin/io/putdotio/android/playback/MobilePlayerFactory.kt b/mobile/src/main/kotlin/io/putdotio/android/playback/MobilePlayerFactory.kt index 987c9eca..2fb49831 100644 --- a/mobile/src/main/kotlin/io/putdotio/android/playback/MobilePlayerFactory.kt +++ b/mobile/src/main/kotlin/io/putdotio/android/playback/MobilePlayerFactory.kt @@ -4,6 +4,7 @@ import androidx.core.content.ContextCompat import androidx.media3.common.MediaItem import androidx.media3.common.Player as Media3Player import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.session.MediaController @@ -109,9 +110,15 @@ internal object DefaultMobilePlayerFactory : MobilePlayerFactory { ): Media3Player { return ExoPlayer.Builder(context, playbackRenderersFactory(context)) // Completed downloads play from the cache; everything else streams through the same source. + // Local schemes such as file:// bypass it; network URLs still go through the cache. .setMediaSourceFactory( DefaultMediaSourceFactory( - MobileDownloadCache.get(context).let { it.playbackFactory(it.activeUserId ?: NO_DOWNLOAD_USER) }, + DefaultDataSource.Factory( + context, + MobileDownloadCache.get(context).let { + it.playbackFactory(it.activeUserId ?: NO_DOWNLOAD_USER) + }, + ), ), ) .setAudioAttributes(mediaType.audioAttributes(), true)