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 = 11001
versionName = "1.10.0-beta.1"
versionCode = 11002
versionName = "1.10.0-beta.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resValue("string", "app_name", "TypeType")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.HttpDataSource
import dev.typetype.android.core.error.CodedFailure
import dev.typetype.android.data.network.PlaybackNetworkState
import java.net.ConnectException
import java.net.NoRouteToHostException
import java.net.SocketException
import java.net.SocketTimeoutException
import java.net.UnknownHostException

internal sealed interface PlaybackNetworkRecoveryAction {
data object Wait : PlaybackNetworkRecoveryAction
Expand Down Expand Up @@ -41,9 +46,9 @@ internal class PlaybackNetworkRecoveryGate {
attempt = 0
}
if (!network.isAvailable) return PlaybackNetworkRecoveryAction.Wait
val delayMs = RETRY_DELAYS_MS.getOrNull(attempt)
?: return PlaybackNetworkRecoveryAction.Wait
attempt++
val retryIndex = attempt.coerceAtMost(RETRY_DELAYS_MS.lastIndex)
val delayMs = RETRY_DELAYS_MS[retryIndex]
attempt = (retryIndex + 1).coerceAtMost(RETRY_DELAYS_MS.lastIndex)
return PlaybackNetworkRecoveryAction.RetryAfter(delayMs)
}

Expand Down Expand Up @@ -115,6 +120,15 @@ private fun Throwable.hasHttpTransportFailure(): Boolean {
) {
return true
}
if (
current is ConnectException ||
current is NoRouteToHostException ||
current is SocketException ||
current is SocketTimeoutException ||
current is UnknownHostException
) {
return true
}
current = current?.cause?.takeUnless { it === current }
}
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.media3.common.PlaybackException
import dev.typetype.android.core.error.CodedFailure
import dev.typetype.android.data.network.PlaybackNetworkState
import java.io.IOException
import java.net.ConnectException
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
Expand Down Expand Up @@ -36,17 +37,20 @@ class PlaybackNetworkRecoveryGateTest {
}

@Test
fun `same route recovery uses a bounded progressive schedule`() {
fun `same route recovery keeps retrying at a capped interval`() {
val gate = PlaybackNetworkRecoveryGate()
val network = PlaybackNetworkState(true, 3L)
val expectedDelays = listOf(1_000L, 3_000L, 10_000L, 30_000L) +
List(4) { 60_000L }
List(8) { 60_000L }

val actions = expectedDelays.map { gate.failed("video", network) }
val exhausted = gate.failed("video", network)
val actions = List(expectedDelays.size) { gate.failed("video", network) }
val cappedRetries = List(3) { gate.failed("video", network) }

assertEquals(expectedDelays.map(PlaybackNetworkRecoveryAction::RetryAfter), actions)
assertEquals(PlaybackNetworkRecoveryAction.Wait, exhausted)
assertEquals(
List(3) { PlaybackNetworkRecoveryAction.RetryAfter(60_000L) },
cappedRetries,
)
}

@Test
Expand Down Expand Up @@ -119,4 +123,14 @@ class PlaybackNetworkRecoveryGateTest {

assertTrue(isNetworkFailure)
}

@Test
fun `raw connection refusal with unspecified code is classified as network interruption`() {
val isNetworkFailure = isNetworkPlaybackFailure(
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
ConnectException("Connection refused"),
)

assertTrue(isNetworkFailure)
}
}
7 changes: 7 additions & 0 deletions release-notes/v1.10.0-beta.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
TypeType Android 1.10.0-beta.2

- Keep playback retrying during temporary network or server outages, then resume
automatically when the instance becomes reachable again.
- Recognize raw socket connection failures as recoverable network interruptions.
- Continue retrying at a capped 60-second interval instead of stopping after a
fixed number of attempts.