Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
06bea31
fix: leave search before top-level navigation
Priveetee Aug 21, 2026
c89eb8d
feat: add continuous player motion model
Priveetee Aug 21, 2026
ed2c352
fix: align player gestures with window state
Priveetee Aug 21, 2026
a66bb8c
test: cover player motion and rotation
Priveetee Aug 21, 2026
1b99141
fix: expose subscription feed diagnostics
Priveetee Aug 21, 2026
b9a9ee2
fix: exit fullscreen when playback disappears
Priveetee Aug 21, 2026
4ed3ead
test: cover search and player gesture edges
Priveetee Aug 21, 2026
8cdf0dd
fix: preserve subscription failure codes
Priveetee Aug 21, 2026
a3adec9
fix: restore system bars after fullscreen
Priveetee Aug 21, 2026
c8c509f
test: cover search restoration
Priveetee Aug 21, 2026
acdfdc0
test: cover subscription cache recreation
Priveetee Aug 21, 2026
9f391a3
test: require live subscription pagination
Priveetee Aug 21, 2026
4211e2e
fix: remember player brightness
Priveetee Aug 22, 2026
ca067f9
feat: refine player level gestures
Priveetee Aug 22, 2026
5bb8841
feat: add fullscreen playback controls
Priveetee Aug 22, 2026
197edfd
fix: refresh subscriptions seamlessly
Priveetee Aug 22, 2026
f3bf47b
style: integrate settings rows
Priveetee Aug 22, 2026
c38f0f1
test: update subscription refresh behavior
Priveetee Aug 22, 2026
fb68b54
feat: refine Shorts interactions
Priveetee Aug 22, 2026
2ec5f6f
perf: smooth comment loading
Priveetee Aug 22, 2026
0313417
feat: paginate comment replies
Priveetee Aug 22, 2026
937adea
fix: improve search and comment presentation
Priveetee Aug 22, 2026
f73deec
refactor: improve settings and channel hierarchy
Priveetee Aug 22, 2026
ed2a85a
feat: refine player gestures and presentation
Priveetee Aug 22, 2026
0a8602a
feat: improve Shorts playback controls
Priveetee Aug 22, 2026
4cf8b3e
refactor: clarify settings detail hierarchy
Priveetee Aug 22, 2026
6f02b0c
fix: stabilize Shorts transitions and navigation
Priveetee Aug 22, 2026
63fe631
feat: refine subscriptions and player details
Priveetee Aug 22, 2026
cd12505
fix: normalize video metadata and menu state
Priveetee Aug 22, 2026
c998e88
feat: complete Shorts playback interactions
Priveetee Aug 22, 2026
96d5661
fix: restore embedded playback after expansion
Priveetee Aug 22, 2026
329385d
fix: settle Shorts playback transitions
Priveetee Aug 22, 2026
85857a8
feat: improve Shorts interactions
Priveetee Aug 22, 2026
ec1f648
perf: reuse prefetched playback streams
Priveetee Aug 22, 2026
33281cd
test: measure playback prefetch budgets
Priveetee Aug 22, 2026
853342d
perf: prioritize active Shorts playback
Priveetee Aug 22, 2026
759d29c
style: simplify notification rows
Priveetee Aug 22, 2026
d0cc74d
test: benchmark Shorts paging
Priveetee Aug 22, 2026
1c74e8e
fix: identify live replays from stream type
Priveetee Aug 22, 2026
5e328ae
fix: capture Shorts channel swipes above player
Priveetee Aug 22, 2026
9ff98b3
perf: reuse prepared Shorts playback sessions
Priveetee Aug 22, 2026
f259187
fix: preserve Shorts controls during channel swipes
Priveetee Aug 22, 2026
ef151ae
fix: correct Shorts channel swipe feedback
Priveetee Aug 22, 2026
c7f1590
feat: add comments to player quick actions
Priveetee Aug 22, 2026
2297599
style: tighten browse detail layouts
Priveetee Aug 22, 2026
d8e2e14
fix: trust user-installed certificate authorities
Priveetee Aug 22, 2026
6245274
fix: persist refresh cookies before process exit
Priveetee Aug 22, 2026
db54b55
build: prepare Android 1.5.0
Priveetee Aug 22, 2026
163dddf
docs: add Android 1.5.0 release notes
Priveetee Aug 22, 2026
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
12 changes: 10 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ android {
applicationId = "dev.typetype.android"
minSdk = 23
targetSdk = 37
versionCode = 10400
versionName = "1.4.0"
versionCode = 10500
versionName = "1.5.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resValue("string", "app_name", "TypeType")
}
Expand Down Expand Up @@ -100,6 +100,14 @@ android {
sourceSets["androidTest"].assets.directories.add("$projectDir/schemas")
}

androidComponents.onVariants { variant ->
if (variant.name == "benchmarkRelease") {
variant.sources.manifests.addStaticManifestFile(
"src/benchmarkRelease/AndroidManifest.xml",
)
}
}

dependencies {
implementation(project(":player"))
baselineProfile(project(":baseline-profile"))
Expand Down
155 changes: 155 additions & 0 deletions app/src/androidTest/java/dev/typetype/android/AppShellAdaptiveTest.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
package dev.typetype.android

import androidx.activity.ComponentActivity
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.unit.Density
Expand All @@ -30,7 +41,11 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.test.espresso.Espresso.closeSoftKeyboard
import dev.typetype.android.core.ui.navigation.HomeRoute
import dev.typetype.android.core.ui.navigation.LibraryRoute
import dev.typetype.android.core.ui.navigation.SearchRoute
import dev.typetype.android.core.ui.navigation.SubscriptionsRoute
import dev.typetype.android.domain.playback.PlaybackQueueController
import dev.typetype.android.domain.playback.PlaybackQueueEntry
import dev.typetype.android.domain.playback.PlaybackQueueSnapshot
Expand Down Expand Up @@ -117,6 +132,144 @@ class AppShellAdaptiveTest {
composeRule.onNodeWithText("Shorts").assertIsFocused()
}

@Test
fun topLevelNavigationDoesNotRestoreSearchOverItsOpeningTab() {
composeRule.setContent {
val navController = rememberNavController()
AppShell(
navController = navController,
playerHostController = PlayerHostController(FakePlaybackQueueController()),
onOpenSearch = { navController.navigate(SearchRoute) },
onOpenSettings = {},
onPlayVideo = {},
onOpenChannel = {},
onOpenAccounts = {},
onClosePlayback = {},
) { contentModifier ->
NavHost(
navController = navController,
startDestination = HomeRoute,
modifier = contentModifier,
) {
composable<HomeRoute> { androidx.compose.material3.Text("Home content") }
composable<SubscriptionsRoute> {
androidx.compose.material3.Text("Subscriptions content")
}
composable<LibraryRoute> { androidx.compose.material3.Text("Library content") }
composable<SearchRoute> { androidx.compose.material3.Text("Search content") }
}
}
}

composeRule.onNodeWithContentDescription("Search").performClick()
composeRule.onNodeWithText("Search content").assertIsDisplayed()
composeRule.onNodeWithText("Subscriptions").performClick()
composeRule.onNodeWithText("Subscriptions content").assertIsDisplayed()

composeRule.onNodeWithContentDescription("Search").performClick()
composeRule.onNodeWithText("Search content").assertIsDisplayed()
composeRule.onNodeWithText("Home").performClick()

composeRule.onNodeWithText("Home content").assertIsDisplayed()
composeRule.onNodeWithText("Search content").assertDoesNotExist()
}

@Test
fun closedKeyboardDoesNotTrapSearchNavigationOrBack() {
composeRule.setContent {
val navController = rememberNavController()
AppShell(
navController = navController,
playerHostController = PlayerHostController(FakePlaybackQueueController()),
onOpenSearch = { navController.navigate(SearchRoute) },
onOpenSettings = {},
onPlayVideo = {},
onOpenChannel = {},
onOpenAccounts = {},
onClosePlayback = {},
) { contentModifier ->
NavHost(
navController = navController,
startDestination = HomeRoute,
modifier = contentModifier,
) {
composable<HomeRoute> { androidx.compose.material3.Text("Home content") }
composable<SubscriptionsRoute> {
androidx.compose.material3.Text("Subscriptions content")
}
composable<LibraryRoute> { androidx.compose.material3.Text("Library content") }
composable<SearchRoute> {
var query by rememberSaveable { mutableStateOf("") }
BasicTextField(
value = query,
onValueChange = { query = it },
modifier = Modifier.testTag(SEARCH_FIELD_TAG),
)
}
}
}
}

composeRule.onNodeWithContentDescription("Search").performClick()
composeRule.onNodeWithTag(SEARCH_FIELD_TAG).performTextInput("video")
closeSoftKeyboard()
composeRule.onNodeWithText("Library").performClick()
composeRule.onNodeWithText("Library content").assertIsDisplayed()

composeRule.onNodeWithContentDescription("Search").performClick()
composeRule.onNodeWithTag(SEARCH_FIELD_TAG).assertTextEquals("")
composeRule.activity.onBackPressedDispatcher.onBackPressed()
composeRule.onNodeWithText("Library content").assertIsDisplayed()

composeRule.onNodeWithText("Home").performClick()
composeRule.onNodeWithContentDescription("Search").performClick()
closeSoftKeyboard()
composeRule.onNodeWithText("Subscriptions").performClick()
composeRule.onNodeWithText("Subscriptions content").assertIsDisplayed()
composeRule.onNodeWithTag(SEARCH_FIELD_TAG).assertDoesNotExist()
}

@Test
fun restoredSearchCanLeaveAndReopenWithoutStaleState() {
val restorationTester = StateRestorationTester(composeRule)
restorationTester.setContent {
val navController = rememberNavController()
AppShell(
navController = navController,
playerHostController = PlayerHostController(FakePlaybackQueueController()),
onOpenSearch = { navController.navigate(SearchRoute) },
onOpenSettings = {},
onPlayVideo = {},
onOpenChannel = {},
onOpenAccounts = {},
onClosePlayback = {},
) { contentModifier ->
NavHost(navController, HomeRoute, contentModifier) {
composable<HomeRoute> { androidx.compose.material3.Text("Home content") }
composable<LibraryRoute> { androidx.compose.material3.Text("Library content") }
composable<SearchRoute> {
var query by rememberSaveable { mutableStateOf("") }
BasicTextField(
value = query,
onValueChange = { query = it },
modifier = Modifier.testTag(SEARCH_FIELD_TAG),
)
}
}
}
}

composeRule.onNodeWithContentDescription("Search").performClick()
composeRule.onNodeWithTag(SEARCH_FIELD_TAG).performTextInput("video")
restorationTester.emulateSavedInstanceStateRestore()
composeRule.onNodeWithTag(SEARCH_FIELD_TAG).assertTextEquals("video")

composeRule.onNodeWithText("Library").performClick()
composeRule.onNodeWithText("Library content").assertIsDisplayed()
composeRule.onNodeWithContentDescription("Search").performClick()
composeRule.onNodeWithTag(SEARCH_FIELD_TAG).assertTextEquals("")
}

private fun setShellWidth(width: Dp) {
setShellSize(width = width, height = 800.dp)
}
Expand Down Expand Up @@ -172,6 +325,8 @@ class AppShellAdaptiveTest {
}
}

private const val SEARCH_FIELD_TAG = "search_field"

private class FakePlaybackQueueController : PlaybackQueueController {
override val state: StateFlow<PlaybackQueueState> = MutableStateFlow(PlaybackQueueState())
override fun start(title: String, entries: List<PlaybackQueueEntry>, shuffle: Boolean) = Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ class DeArrowBrandingEnvironmentTest {
assertEquals(0, calls.get())
}

@Test
fun deferredEnhancementsKeepOriginalBrandingWithoutLoading() {
val calls = AtomicInteger()
val environment = DeArrowBrandingEnvironment(
enabled = true,
preferences = DeArrowPreferences("dearrow", "dearrow", "accepted"),
loader = { _, _ ->
calls.incrementAndGet()
Result.success(BRANDING)
},
)

composeRule.setContent {
CompositionLocalProvider(LocalDeArrowBranding provides environment) {
val branding = rememberVideoBranding(
VIDEO_URL,
"Original",
"original.jpg",
120,
loadEnhancements = false,
)
Text(
text = "${branding.title}|${branding.thumbnailUrl}",
modifier = Modifier.testTag("branding"),
)
}
}

composeRule.onNodeWithTag("branding").assertTextEquals("Original|original.jpg")
assertEquals(0, calls.get())
}

@Test
fun videoCardRendersResolvedTitle() {
val environment = DeArrowBrandingEnvironment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import dev.typetype.android.R
import dev.typetype.android.data.network.ServerError
import dev.typetype.android.data.network.ServerResponseException
import java.io.IOException
import javax.net.ssl.SSLHandshakeException
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -56,6 +57,14 @@ class UserErrorMapperAndroidTest {
)
}

@Test
fun mapsTlsFailureToAnActionableMessage() {
assertEquals(
context.getString(R.string.error_secure_connection_failed),
mapper.message(SSLHandshakeException("certificate rejected"), R.string.setup_server_unreachable),
)
}

private fun assertMapped(status: Int, expectedResource: Int) {
assertEquals(
context.getString(expectedResource),
Expand Down
Loading