diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/domain/ring/MediaRingPolicy.kt b/app/src/main/java/de/jeanlucmakiola/clockula/domain/ring/MediaRingPolicy.kt index 15955c6..6872131 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/domain/ring/MediaRingPolicy.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/domain/ring/MediaRingPolicy.kt @@ -60,6 +60,17 @@ object MediaRingPolicy { fun shouldFallBack(stall: Stall): Boolean = stall.stopped >= STALL_GRACE || stall.notPlaying >= BUFFER_GRACE + /** + * Whether the app turned down the ring's play request, so the start can + * fall back now instead of waiting out [START_TIMEOUT]. YouTube without + * Premium does this at once. Only an error reported at or after + * [requestedAtMillis] counts: one left over from the user's own listening + * says nothing about this request. Both times are `elapsedRealtime`, which + * is what `PlaybackState` stamps its updates with. + */ + fun refusedStart(isError: Boolean, updatedAtMillis: Long, requestedAtMillis: Long): Boolean = + isError && updatedAtMillis >= requestedAtMillis + /** * The media-stream index a fully ramped ring plays at. * diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ring/MediaAppRing.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ring/MediaAppRing.kt index 8dd18c9..6dd0afe 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ring/MediaAppRing.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ring/MediaAppRing.kt @@ -6,6 +6,7 @@ import android.media.AudioManager import android.media.browse.MediaBrowser import android.media.session.MediaController import android.media.session.PlaybackState +import android.os.SystemClock import de.jeanlucmakiola.clockula.data.ringtones.connectMediaBrowser import de.jeanlucmakiola.clockula.domain.MediaSourceRef import de.jeanlucmakiola.clockula.domain.ring.MediaPlayback @@ -86,6 +87,7 @@ internal class MediaAppRing(private val context: Context, private val ref: Media } } if (stopped) return false + val requestedAt = SystemClock.elapsedRealtime() runCatching { if (ref.mediaId != null) controls.playFromMediaId(ref.mediaId, null) else controls.play() }.onFailure { return false } @@ -93,11 +95,23 @@ internal class MediaAppRing(private val context: Context, private val ref: Media return withTimeoutOrNull(MediaRingPolicy.START_TIMEOUT) { // Strictly PLAYING here: an app stuck buffering a stream it cannot // reach must not count as a ring that started. - while (!stopped && playbackState() != PlaybackState.STATE_PLAYING) delay(POLL_MILLIS) + while (!stopped && playbackState() != PlaybackState.STATE_PLAYING) { + if (connectionLost || refused(requestedAt)) return@withTimeoutOrNull false + delay(POLL_MILLIS) + } !stopped } ?: false } + private fun refused(requestedAt: Long): Boolean { + val state = runCatching { controller?.playbackState }.getOrNull() ?: return false + return MediaRingPolicy.refusedStart( + isError = state.state == PlaybackState.STATE_ERROR, + updatedAtMillis = state.lastPositionUpdateTime, + requestedAtMillis = requestedAt, + ) + } + fun playback(): MediaPlayback { if (connectionLost) return MediaPlayback.STOPPED return when (playbackState()) { diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/domain/ring/MediaRingPolicyTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/domain/ring/MediaRingPolicyTest.kt index 379f4a9..39fce88 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/domain/ring/MediaRingPolicyTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/domain/ring/MediaRingPolicyTest.kt @@ -29,6 +29,16 @@ class MediaRingPolicyTest { assertThat(MediaRingPolicy.shouldFallBack(long)).isTrue() } + @Test + fun `an error after the play request ends the start, an older one does not`() { + val requestedAt = 10_000L + + assertThat(MediaRingPolicy.refusedStart(isError = true, updatedAtMillis = 10_050L, requestedAtMillis = requestedAt)).isTrue() + assertThat(MediaRingPolicy.refusedStart(isError = true, updatedAtMillis = requestedAt, requestedAtMillis = requestedAt)).isTrue() + assertThat(MediaRingPolicy.refusedStart(isError = true, updatedAtMillis = 9_000L, requestedAtMillis = requestedAt)).isFalse() + assertThat(MediaRingPolicy.refusedStart(isError = false, updatedAtMillis = 10_050L, requestedAtMillis = requestedAt)).isFalse() + } + @Test fun `buffering forever is a failure too`() { val brief = run(*repeat(MediaPlayback.TRANSITIONING, MediaRingPolicy.BUFFER_GRACE - tick))