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 @@ -22,8 +22,8 @@ android {

defaultConfig {
applicationId = "dev.typetype.android"
versionCode = 11109
versionName = "1.11.0-beta.9"
versionCode = 11110
versionName = "1.11.0-beta.10"
}

signingConfigs {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import dev.typetype.android.core.ui.navigation.AboutRoute
import dev.typetype.android.core.ui.navigation.AppUpdatesRoute
import dev.typetype.android.core.ui.navigation.AccountsRoute
import dev.typetype.android.core.ui.navigation.AppearanceRoute
import dev.typetype.android.core.ui.navigation.BlockedSettingsRoute
Expand Down Expand Up @@ -75,6 +76,7 @@ internal fun AdaptiveSettingsHost(
onOpenPrivacy = { navController.selectSettings(PrivacySettingsRoute) },
onOpenDiagnostics = { navController.selectSettings(DiagnosticsRoute) },
onOpenBlocked = { navController.selectSettings(BlockedSettingsRoute) },
onOpenUpdates = { navController.selectSettings(AppUpdatesRoute) },
onOpenAbout = { navController.selectSettings(AboutRoute) },
onSignOut = onSignOut,
)
Expand Down Expand Up @@ -113,6 +115,7 @@ private fun NavDestination.settingsTitle(): Int? = when {
hasRoute<PrivacySettingsRoute>() -> R.string.settings_privacy_title
hasRoute<DiagnosticsRoute>() -> R.string.diagnostics_title
hasRoute<BlockedSettingsRoute>() -> R.string.settings_blocked_title
hasRoute<AppUpdatesRoute>() -> R.string.updates_title
hasRoute<AboutRoute>() || hasRoute<LicensesRoute>() -> R.string.settings_about_title
else -> null
}
5 changes: 5 additions & 0 deletions app/src/main/java/dev/typetype/android/AppNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.navigation.compose.rememberNavController
import dev.typetype.android.core.ui.branding.DeArrowBrandingViewModel
import dev.typetype.android.core.ui.branding.LocalDeArrowBranding
import dev.typetype.android.core.ui.navigation.AboutRoute
import dev.typetype.android.core.ui.navigation.AppUpdatesRoute
import dev.typetype.android.core.ui.navigation.AccountsRoute
import dev.typetype.android.core.ui.navigation.AddServerRoute
import dev.typetype.android.core.ui.navigation.AppearanceRoute
Expand Down Expand Up @@ -47,6 +48,7 @@ import dev.typetype.android.feature.player.PlayerViewModel
import dev.typetype.android.feature.player.components.LocalPlaybackRecovery
import dev.typetype.android.feature.search.SearchRoute as SearchRouteScreen
import dev.typetype.android.feature.settings.about.AboutScreen
import dev.typetype.android.feature.settings.updates.AppUpdatesScreen
import dev.typetype.android.feature.settings.accounts.AccountSettingsRoute as AccountSettingsRouteScreen
import dev.typetype.android.feature.settings.appearance.AppearanceRoute as AppearanceRouteScreen
import dev.typetype.android.feature.settings.blocked.BlockedSettingsRoute as BlockedSettingsRouteScreen
Expand Down Expand Up @@ -269,6 +271,9 @@ fun AppNavHost(
onNavigateBack = { navController.popBackStack() },
)
}
composable<AppUpdatesRoute> {
AppUpdatesScreen(onNavigateBack = { navController.popBackStack() })
}
composable<AboutRoute> {
AboutScreen(
onNavigateBack = { navController.popBackStack() },
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/dev/typetype/android/SettingsNavigation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import dev.typetype.android.core.ui.navigation.AboutRoute
import dev.typetype.android.core.ui.navigation.AppUpdatesRoute
import dev.typetype.android.core.ui.navigation.AccountsRoute
import dev.typetype.android.core.ui.navigation.AppearanceRoute
import dev.typetype.android.core.ui.navigation.BlockedSettingsRoute
Expand Down Expand Up @@ -49,6 +50,7 @@ internal fun NavGraphBuilder.settingsDestinations(
onOpenPrivacy = { navController.navigate(PrivacySettingsRoute) },
onOpenDiagnostics = { navController.navigate(DiagnosticsRoute) },
onOpenBlocked = { navController.navigate(BlockedSettingsRoute) },
onOpenUpdates = { navController.navigate(AppUpdatesRoute) },
onOpenAbout = { navController.navigate(AboutRoute) },
onSignOut = onSignOut,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
package dev.typetype.android.data.stream

import dev.typetype.android.core.error.CodedFailure
import dev.typetype.android.data.network.ServerResponseException
import dev.typetype.android.data.network.extractServerError
import retrofit2.Response

const val SABR_PLAYBACK_NOT_FOUND = "sabr_playback_not_found"

internal fun Response<*>.rejectSabrRedirect(message: String) {
if (code() in 300..399) sabrContractMismatch(message)
}

// The server answers an evicted idle session with a 404 and its generic "error" code.
internal fun sabrSessionResponseException(response: Response<*>): ServerResponseException {
val error = extractServerError(response)
return ServerResponseException(
if (response.code() == HTTP_NOT_FOUND) error.copy(code = SABR_PLAYBACK_NOT_FOUND) else error,
)
}

internal fun sabrContractMismatch(message: String, cause: Throwable? = null): Nothing =
throw SabrPlaybackContractFailure(message, "youtube_sabr_contract_mismatch", cause)

Expand All @@ -21,3 +33,5 @@ private class SabrPlaybackContractFailure(
override val requestId: String? = null
override val statusCode: Int? = null
}

private const val HTTP_NOT_FOUND = 404
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ internal class SabrPlaybackSessionPreparer(
listOf("sabr", "playback", binding.sessionId, "seek"),
"SABR seek left its server session endpoint",
)
if (!response.isSuccessful) throw serverResponseException(response)
if (!response.isSuccessful) throw sabrSessionResponseException(response)
val control = response.body()
?.requireControlResponse(target, binding.sessionId, binding.generation)
?: sabrContractMismatch("SABR returned an empty seek response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import dev.typetype.android.data.network.TypeTypeMediaApi
import dev.typetype.android.data.network.dto.SabrPlaybackPositionRequestDto
import dev.typetype.android.data.network.dto.SabrPlaybackResponse
import dev.typetype.android.data.network.dto.SabrPlaybackWindowRequestDto
import dev.typetype.android.data.network.serverResponseException
import dev.typetype.android.domain.stream.SabrPlaybackBufferedRange
import dev.typetype.android.domain.stream.SabrPlaybackSession
import dev.typetype.android.domain.stream.SabrPlaybackTarget
Expand Down Expand Up @@ -45,7 +44,7 @@ internal class SabrPlaybackWindowWait(
api.prefetchSabrPlayback(control.sessionId, request())
}
prefetch.requireWindowEndpoint(baseUrl, control.sessionId, "prefetch")
if (!prefetch.isSuccessful) throw serverResponseException(prefetch)
if (!prefetch.isSuccessful) throw sabrSessionResponseException(prefetch)
val pending = prefetch.body()
?.requireWindowIdentity(control)
?: sabrContractMismatch("SABR returned an empty prefetch response")
Expand All @@ -55,7 +54,7 @@ internal class SabrPlaybackWindowWait(
api.sabrPlaybackSegments(control.sessionId, request())
}
segments.requireWindowEndpoint(baseUrl, control.sessionId, "segments")
if (!segments.isSuccessful) throw serverResponseException(segments)
if (!segments.isSuccessful) throw sabrSessionResponseException(segments)
val window = segments.body()
?.requireWindowIdentity(control)
?: sabrContractMismatch("SABR returned an empty segment window")
Expand Down Expand Up @@ -108,7 +107,7 @@ internal class SabrPlaybackWindowWait(
listOf("sabr", "playback", control.sessionId, "position"),
"SABR position update left its server session endpoint",
)
if (!response.isSuccessful) throw serverResponseException(response)
if (!response.isSuccessful) throw sabrSessionResponseException(response)
val body = response.body()
?: sabrContractMismatch("SABR returned an empty position response")
if (body.sessionId != control.sessionId || body.generation != control.generation) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package dev.typetype.android.data.stream

import dev.typetype.android.data.network.ServerResponseException
import dev.typetype.android.domain.stream.SabrPlaybackSnapshot
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class SabrMissingSessionTest {
private val script = SabrServerScript()

@After
fun tearDown() = script.shutdown()

@Test
fun anEvictedSessionIsReportedAsMissing() = runBlocking {
script.enqueue(EVICTED_SESSION_BODY, code = 404)

val failure = runCatching { refresh() }.exceptionOrNull()

assertTrue(failure is ServerResponseException)
assertEquals(SABR_PLAYBACK_NOT_FOUND, (failure as ServerResponseException).failureCode)
assertEquals(404, failure.statusCode)
}

@Test
fun anEvictedSessionDuringSeekIsReportedAsMissing() = runBlocking {
script.enqueue(EVICTED_SESSION_BODY, code = 404)

val failure = runCatching {
SabrPlaybackSessionPreparer(pause = {})
.seek(script.api, script.baseUrl, script.target(), script.binding(), PLAYER_TIME_MS)
}.exceptionOrNull()

assertEquals(SABR_PLAYBACK_NOT_FOUND, (failure as ServerResponseException).failureCode)
}

@Test
fun aMissingVideoKeepsTheServerCode() = runBlocking {
script.enqueue("""{"error":"Video not found","code":"video_not_found"}""", code = 404)

val failure = runCatching {
SabrPlaybackSessionPreparer(pause = {})
.prepareOnce(script.api, script.baseUrl, script.target(), PLAYER_TIME_MS)
}.exceptionOrNull()

assertEquals("video_not_found", (failure as ServerResponseException).failureCode)
}

private suspend fun refresh() = SabrPlaybackSessionPreparer(pause = {})
.refresh(script.api, script.baseUrl, script.target(), script.binding()) {
SabrPlaybackSnapshot(playerTimeMs = PLAYER_TIME_MS, bufferedRanges = emptyList())
}

private companion object {
const val PLAYER_TIME_MS = 245_000L
const val EVICTED_SESSION_BODY = """{"error":"No active SABR playback session","code":"error"}"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.HttpDataSource
import dev.typetype.android.core.error.CodedFailure
import dev.typetype.android.data.stream.SABR_PLAYBACK_NOT_FOUND
import dev.typetype.android.data.stream.SABR_PREPARATION_TIMEOUT
import dev.typetype.android.data.stream.SabrPlaybackRecoveryException
import java.io.InterruptedIOException
Expand Down Expand Up @@ -81,7 +82,7 @@ private fun Int.recoveryCause(sabrMedia: Boolean = true): PlaybackRecoveryCause
private val SESSION_FAILURE_CODES = setOf(
SABR_PREPARATION_TIMEOUT,
SABR_CONTRACT_FAILURE_CODE,
"sabr_playback_not_found",
SABR_PLAYBACK_NOT_FOUND,
)
private val GONE_STATUS_CODES = setOf(404, 409, 410)
private const val MAX_CAUSE_DEPTH = 8
64 changes: 37 additions & 27 deletions core/resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -722,33 +722,43 @@
<string name="youtube_takeout_notification_title">Importing YouTube data</string>
<string name="youtube_takeout_notification_progress">%1$s · %2$s</string>
<string name="about_section_app">Application</string>
<string name="about_section_app_updates">App updates</string>
<string name="about_update_channel_fdroid">F-Droid manages updates</string>
<string name="about_update_channel_github_stable">GitHub Stable</string>
<string name="about_update_channel_github_beta">GitHub Beta</string>
<string name="about_update_beta_warning">Beta builds may be less stable. You can switch back when a newer Stable build is released.</string>
<string name="about_update_check">Check</string>
<string name="about_update_checking">Checking GitHub for updates…</string>
<string name="about_update_managed_by_fdroid">F-Droid will check for updates.</string>
<string name="about_update_up_to_date">You are up to date on this channel.</string>
<string name="about_update_available">An update is available.</string>
<string name="about_update_newer_build_installed">Your installed build is newer than the latest release on this channel.</string>
<string name="about_update_release">Version %1$s</string>
<string name="about_update_download">Download</string>
<string name="about_update_downloading">Downloading and verifying APK…</string>
<string name="about_update_progress">%1$d%% downloaded</string>
<string name="about_update_ready_to_install">The APK is verified and ready to install.</string>
<string name="about_update_install">Install update</string>
<string name="about_update_installer_opened">Android’s installer was opened. Confirm the update there.</string>
<string name="about_update_permission_requested">Allow TypeType to install apps, then return here and tap Install update.</string>
<string name="about_update_failed">Could not prepare this update.</string>
<string name="about_update_failed_network">Could not reach GitHub. Check your connection and try again.</string>
<string name="about_update_failed_release">GitHub returned invalid release information.</string>
<string name="about_update_failed_checksum">The APK checksum did not match. The file was discarded.</string>
<string name="about_update_failed_package">The downloaded file is not a compatible TypeType update.</string>
<string name="about_update_failed_downgrade">Android will not install an older build over this one.</string>
<string name="about_update_failed_signature">The APK signing certificate does not match this installation.</string>
<string name="about_update_failed_installer">Could not open Android’s package installer.</string>
<string name="updates_title">Updates</string>
<string name="updates_entry_subtitle">Update channel and new versions</string>
<string name="updates_installed_version">TypeType %1$s</string>
<string name="updates_status_checking">Checking for updates…</string>
<string name="updates_status_fdroid">F-Droid checks for updates</string>
<string name="updates_status_up_to_date">You’re up to date</string>
<string name="updates_status_available">Version %1$s is available</string>
<string name="updates_status_newer_build">This build is newer than the latest release on this channel</string>
<string name="updates_status_downloading">Downloading version %1$s…</string>
<string name="updates_status_ready">Version %1$s is verified and ready to install</string>
<string name="updates_status_installer_opened">Confirm the update in Android’s installer</string>
<string name="updates_status_permission">Allow TypeType to install apps, then tap Install</string>
<string name="updates_status_failed">Could not prepare this update</string>
<string name="updates_failed_network">Could not reach GitHub. Check your connection and try again.</string>
<string name="updates_failed_release">GitHub returned invalid release information.</string>
<string name="updates_failed_checksum">The APK checksum did not match. The file was discarded.</string>
<string name="updates_failed_package">The downloaded file is not a compatible TypeType update.</string>
<string name="updates_failed_downgrade">Android will not install an older build over this one.</string>
<string name="updates_failed_signature">The APK signing certificate does not match this installation.</string>
<string name="updates_failed_installer">Could not open Android’s package installer.</string>
<string name="updates_download_size">%1$s to download</string>
<string name="updates_progress">%1$s of %2$s</string>
<string name="updates_check">Check for updates</string>
<string name="updates_download">Download</string>
<string name="updates_install">Install</string>
<string name="updates_retry">Try again</string>
<string name="updates_channel_label">Update channel</string>
<string name="updates_channel_fdroid">F-Droid</string>
<string name="updates_channel_fdroid_description">Updates come from the F-Droid app</string>
<string name="updates_channel_stable">GitHub Stable</string>
<string name="updates_channel_stable_description">Tested releases, downloaded from GitHub</string>
<string name="updates_channel_beta">GitHub Beta</string>
<string name="updates_channel_beta_description">Early builds that may be less stable</string>
<string name="updates_whats_new">What’s new in %1$s</string>
<string name="updates_show_more">Show more</string>
<string name="updates_show_less">Show less</string>
<string name="updates_verified_note">Every download is checked against its checksum and signing certificate before Android installs it.</string>
<string name="about_license">License</string>
<string name="about_license_value">GPL v3</string>
<string name="about_section_server">Server</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ data object DiagnosticsRoute
@Serializable
data object BlockedSettingsRoute

@Serializable
data object AppUpdatesRoute

@Serializable
data object AboutRoute

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class SettingsScreenAccessibilityTest {
onOpenPrivacy = {},
onOpenDiagnostics = {},
onOpenBlocked = {},
onOpenUpdates = {},
onOpenAbout = {},
onSignOut = {},
)
Expand All @@ -147,6 +148,7 @@ class SettingsScreenAccessibilityTest {
R.string.settings_privacy_title,
R.string.diagnostics_title,
R.string.settings_blocked_title,
R.string.updates_title,
R.string.settings_about_title,
R.string.settings_sign_out,
).map(::resource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fun SettingsRoute(
onOpenPrivacy: () -> Unit,
onOpenDiagnostics: () -> Unit,
onOpenBlocked: () -> Unit,
onOpenUpdates: () -> Unit,
onOpenAbout: () -> Unit,
onSignOut: () -> Unit,
selectedTitleRes: Int? = null,
Expand All @@ -46,6 +47,7 @@ fun SettingsRoute(
onOpenPrivacy = onOpenPrivacy,
onOpenDiagnostics = onOpenDiagnostics,
onOpenBlocked = onOpenBlocked,
onOpenUpdates = onOpenUpdates,
onOpenAbout = onOpenAbout,
onSignOut = onSignOut,
)
Expand Down
Loading