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 = 11101
versionName = "1.11.0-beta.1"
versionCode = 11102
versionName = "1.11.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 @@ -15,19 +15,23 @@ class DescriptionSectionCountTest {
val composeRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun unknownViewCountIsHidden() {
fun unknownViewCountExplainsProviderLimit() {
setDescription(viewCount = -1)

composeRule.onNodeWithText("-1").assertDoesNotExist()
composeRule.onNodeWithContentDescription(viewsLabel("-1")).assertDoesNotExist()
composeRule.onNodeWithText(
composeRule.activity.getString(R.string.video_views_hidden_by_provider),
).assertIsDisplayed()
}

@Test
fun zeroViewCountRemainsVisible() {
setDescription(viewCount = 0)

composeRule.onNodeWithText("0").assertIsDisplayed()
composeRule.onNodeWithContentDescription(viewsLabel("0")).assertIsDisplayed()
composeRule.onNodeWithContentDescription(
composeRule.activity.getString(R.string.player_views_count, "0"),
).assertIsDisplayed()
}

private fun setDescription(viewCount: Long) {
Expand All @@ -45,7 +49,4 @@ class DescriptionSectionCountTest {
}
}
}

private fun viewsLabel(count: String): String =
composeRule.activity.getString(R.string.player_views_count, count)
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ import dev.typetype.android.core.ui.branding.rememberVideoBranding
import dev.typetype.android.domain.feed.Video
import dev.typetype.android.domain.feed.VideoAvailability
import dev.typetype.android.domain.feed.availabilityAt
import dev.typetype.android.domain.feed.badgeStatusAt
import dev.typetype.android.domain.feed.releaseTimeMillis
import dev.typetype.android.domain.feed.VideoBadgeStatus
import dev.typetype.android.feature.player.CARD_PREWARM_DELAY_MILLIS
import dev.typetype.android.feature.player.LocalPlaybackPrewarm
import kotlinx.coroutines.delay
Expand Down Expand Up @@ -177,16 +179,28 @@ private fun formatRelatedViews(views: Long): String = when {

@Composable
private fun Video.metadataText(): String {
val views = stringResource(R.string.video_views_short, formatRelatedViews(viewCount))
val nowMillis = System.currentTimeMillis()
val scheduledAt = releaseTimeMillis()?.takeIf { it > nowMillis }
val badge = badgeStatusAt(nowMillis)
val views = when {
viewCount >= 0L -> stringResource(R.string.video_views_short, formatRelatedViews(viewCount))
scheduledAt != null &&
(badge == VideoBadgeStatus.Premiere || badge == VideoBadgeStatus.Upcoming) ->
stringResource(R.string.video_premieres_at, formatRelativeDate(scheduledAt))
else -> stringResource(R.string.video_views_hidden_by_provider)
}
val published = releaseTimeMillis()
?.takeIf { it <= System.currentTimeMillis() }
?.takeIf { scheduledAt == null }
?.let { timestamp ->
DateUtils.getRelativeTimeSpanString(
timestamp,
System.currentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
formatRelativeDate(timestamp)
}
return listOfNotNull(views, published).joinToString(" · ")
}

private fun formatRelativeDate(timestampMillis: Long): String =
DateUtils.getRelativeTimeSpanString(
timestampMillis,
System.currentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ import dev.typetype.android.core.ui.branding.rememberVideoBranding
import dev.typetype.android.domain.feed.Video
import dev.typetype.android.domain.feed.VideoAvailability
import dev.typetype.android.domain.feed.availabilityAt
import dev.typetype.android.domain.feed.badgeStatusAt
import dev.typetype.android.domain.feed.releaseTimeMillis
import dev.typetype.android.domain.feed.VideoBadgeStatus
import dev.typetype.android.feature.player.CARD_PREWARM_DELAY_MILLIS
import dev.typetype.android.feature.player.LocalPlaybackPrewarm

Expand Down Expand Up @@ -214,13 +216,24 @@ private fun WatchedBadge(modifier: Modifier = Modifier) {

@Composable
private fun Video.metadataText(): String? {
val views = formatViews(viewCount)?.let { stringResource(R.string.video_views_short, it) }
val published = releaseTimeMillis()?.let(::formatPublishedDate)
val nowMillis = System.currentTimeMillis()
val scheduledAt = releaseTimeMillis()?.takeIf { it > nowMillis }
val badge = badgeStatusAt(nowMillis)
val views = when {
viewCount >= 0L -> stringResource(
R.string.video_views_short,
formatViews(viewCount),
)
scheduledAt != null &&
(badge == VideoBadgeStatus.Premiere || badge == VideoBadgeStatus.Upcoming) ->
stringResource(R.string.video_premieres_at, formatPublishedDate(scheduledAt))
else -> stringResource(R.string.video_views_hidden_by_provider)
}
val published = releaseTimeMillis()?.takeIf { scheduledAt == null }?.let(::formatPublishedDate)
return listOfNotNull(views, published).takeIf { it.isNotEmpty() }?.joinToString(" · ")
}

private fun formatViews(views: Long): String? = when {
views < 0L -> null
private fun formatViews(views: Long): String = when {
views >= 1_000_000_000 -> "%.1fB".format(views / 1_000_000_000.0)
views >= 1_000_000 -> "%.1fM".format(views / 1_000_000.0)
views >= 1_000 -> "%.1fK".format(views / 1_000.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,17 @@ internal fun loadProgressiveStream(
enriched = stream.withMetadataFrom(metadataStream)
send(PlayerStreamUpdate.MetadataEnriched(enriched))
}
if (details != null && enriched.relatedStreams.isEmpty()) {
if (details != null &&
(enriched.relatedStreams.isEmpty() || enriched.viewCount < 0L)
) {
for (delayMillis in RELATED_METADATA_RETRY_DELAYS) {
retryDelay(delayMillis)
val retry = loadMetadata() ?: break
retry.onSuccess { metadataStream ->
enriched = enriched.withMetadataFrom(metadataStream)
send(PlayerStreamUpdate.MetadataEnriched(enriched))
}
if (enriched.relatedStreams.isNotEmpty()) break
if (enriched.relatedStreams.isNotEmpty() && enriched.viewCount >= 0L) break
}
}
if (enriched.needsChannelMetadata()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,19 @@ fun DescriptionSection(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
viewCount.knownCountOrNull()?.let { knownViews ->
val formattedViews = formatCompact(knownViews)
VideoStat(
value = formattedViews,
icon = Icons.Outlined.Visibility,
contentDescription = stringResource(
val knownViews = viewCount.knownCountOrNull()
val formattedViews = knownViews?.let(::formatCompact)
?: stringResource(dev.typetype.android.R.string.video_views_hidden_by_provider)
VideoStat(
value = formattedViews,
icon = Icons.Outlined.Visibility,
contentDescription = knownViews?.let {
stringResource(
dev.typetype.android.R.string.player_views_count,
formattedViews,
),
)
}
formatCompact(it),
)
} ?: formattedViews,
)
if (likeCount > 0) {
VideoStat(
value = formatCompact(likeCount),
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="video_views_short">%1$s views</string>
<string name="video_premieres_at">Premieres %1$s</string>
<string name="video_views_hidden_by_provider">Views hidden by provider</string>
<string name="welcome_title">Welcome aboard</string>
<string name="welcome_get_started">Get started</string>
<string name="uploader_subscribers_short">%1$s subscribers</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class PlayerStreamLoaderTest {
} else {
listOf(relatedVideo())
}
Result.success(stream("Detailed", related = related))
Result.success(stream("Detailed", related = related, viewCount = 42L))
},
loadProgress = { 0L },
retryDelay = {},
Expand All @@ -181,12 +181,37 @@ class PlayerStreamLoaderTest {
assertEquals(listOf("related"), retried.stream.relatedStreams.map(Video::id))
}

@Test
fun `unknown bootstrap view count is recovered from metadata`() = runBlocking {
val metadataRequests = AtomicInteger()
val updates = loadProgressiveStream(
loadPlayback = { Result.success(stream("Bootstrap", viewCount = -1L)) },
loadMetadata = {
val request = metadataRequests.incrementAndGet()
Result.success(
stream(
"Detailed",
viewCount = if (request == 1) -1L else 42L,
related = listOf(relatedVideo()),
),
)
},
loadProgress = { 0L },
retryDelay = {},
).take(3).toList()

val recovered = updates[2] as PlayerStreamUpdate.MetadataEnriched
assertEquals(2, metadataRequests.get())
assertEquals(42L, recovered.stream.viewCount)
}

private fun stream(
title: String,
accountId: String = "account",
uploaderUrl: String = "",
subscriberCount: Long = -1L,
related: List<Video> = emptyList(),
viewCount: Long = -1L,
) = Stream(
playbackContract = StreamPlaybackContract.ServerSabr,
id = "video",
Expand All @@ -199,7 +224,7 @@ class PlayerStreamLoaderTest {
thumbnailUrl = "",
description = "",
durationSeconds = 120L,
viewCount = -1L,
viewCount = viewCount,
likeCount = -1L,
dislikeCount = -1L,
uploadedAtMillis = -1L,
Expand Down
5 changes: 5 additions & 0 deletions release-notes/v1.11.0-beta.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
TypeType Android 1.11.0-beta.2

- Keep view counts visible when the server can provide them, including authenticated YouTube sessions.
- Show scheduled premiere times for upcoming videos instead of a negative count.
- Retry playback metadata when counts or related videos are missing.