Skip to content

Commit b31aaff

Browse files
committed
Merge branch 'refs/heads/main' into bugfix/fix-debug-keystores
2 parents 82feefd + 30137d2 commit b31aaff

39 files changed

+537
-422
lines changed

.github/workflows/Crane.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
test:
2727
needs: build
28-
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
28+
runs-on: macos-13
2929
timeout-minutes: 30
3030
strategy:
3131
matrix:

.github/workflows/JetLagged.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
test:
2727
needs: build
28-
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
28+
runs-on: ubuntu-latest
2929
timeout-minutes: 30
3030
strategy:
3131
matrix:

.github/workflows/JetNews.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
androidTest:
2727
needs: build
28-
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
28+
runs-on: ubuntu-latest
2929
timeout-minutes: 30
3030
strategy:
3131
matrix:

.github/workflows/Jetchat.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
test:
2727
needs: build
28-
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
28+
runs-on: ubuntu-latest
2929
timeout-minutes: 30
3030
strategy:
3131
matrix:

.github/workflows/Owl.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
test:
2727
needs: build
28-
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
28+
runs-on: ubuntu-latest
2929
timeout-minutes: 30
3030
strategy:
3131
matrix:

.github/workflows/Reply.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
test:
2727
needs: build
28-
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
28+
runs-on: ubuntu-latest
2929
timeout-minutes: 30
3030
strategy:
3131
matrix:

Jetcaster/README.md

+39-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ project from Android Studio following the steps
1414
## Screenshots
1515

1616
![readme_cover](https://github.com/android/compose-samples/assets/10263978/a58ab950-71aa-48e0-8bc7-85443a1b4f6b)
17+
## Phone app
1718

18-
## Features
19+
### Features
1920

2021
This sample has 3 components: the home screen, the podcast details screen, and the player screen
2122

@@ -38,7 +39,7 @@ Some other notable things which are implemented:
3839

3940
* Images are all provided from each podcast's RSS feed, and loaded using [Coil][coil] library.
4041

41-
## Architecture
42+
### Architecture
4243
The app is built in a Redux-style, where each UI 'screen' has its own [ViewModel][viewmodel], which exposes a single [StateFlow][stateflow] containing the entire view state. Each [ViewModel][viewmodel] is responsible for subscribing to any data streams required for the view, as well as exposing functions which allow the UI to send events.
4344

4445
Using the example of the home screen in the [`com.example.jetcaster.ui.home`](app/src/main/java/com/example/jetcaster/ui/home) package:
@@ -58,6 +59,36 @@ This pattern is used across the different screens:
5859
- __Discover:__ [`com.example.jetcaster.ui.home.discover`](app/src/main/java/com/example/jetcaster/ui/home/discover)
5960
- __Podcast Category:__ [`com.example.jetcaster.ui.category`](app/src/main/java/com/example/jetcaster/ui/home/category)
6061

62+
## Wear
63+
64+
This sample showcases a 2-screen pager which allows navigation between the Player and the Library.
65+
From the Library, users can access latest episodes from subscribed podcasts, and queue.
66+
From the podcast, users can access episode details and add episodes to the queue.
67+
From the Player screen, users can access a volume screen and a playback speed screen.
68+
69+
The sample implements [Wear UX best practices for media apps][mediappsbestpractices], such as:
70+
- Support rotating side button (RSB) and Bezel for scrollable screens
71+
- Display scrollbar on scrolling
72+
- Display the time on top of the screens
73+
74+
The sample is built using the [Media Toolkit][[mediatoolkit]] which is an open source
75+
project part of [Horologist][horologist] to ease the development of media apps on Wear OS built on top of Compose for Wear.
76+
It provides ready to use UI screens, such the [EntityScreen][entityscreen]
77+
that is used in this sample to implement many screens such as Podcast, LatestEpisodes and Queue. [Horologist][horologist] also provides
78+
a VolumeScreen that can be reused by media apps to conveniently control volume either by interacting with the rotating side button(RSB)/Bezel or by
79+
using the provided buttons.
80+
For simplicity, this sample uses a mock Player which is reused across form factors,
81+
if you want to see an advanced Media sample built on Compose that uses Exoplayer and plays media content,
82+
refer to the [Media Toolkit sample][mediatoolkitsample].
83+
84+
The [official media app guidance for Wear OS][ [wearmediaguidance]]
85+
advices to download content on the watch before listening to preserve power, this feature will be added to this sample in future iterations. You can
86+
refer to the [Media Toolkit sample][mediatoolkitsample] to learn how to implement the media download feature.
87+
88+
### Architecture
89+
The architecture of the Wear app is similar to the phone app architecture: each UI 'screen' has its
90+
own [ViewModel][viewmodel] which exposes a `StateFlow<ScreenState>` for the UI to observe.
91+
6192
## Data
6293

6394
### Podcast data
@@ -114,3 +145,9 @@ limitations under the License.
114145
[jdk8desugar]: https://developer.android.com/studio/write/java8-support#library-desugaring
115146
[coil]: https://coil-kt.github.io/coil/
116147
[wsc]: https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes#window_size_classes
148+
[mediatoolkit]: https://google.github.io/horologist/media-toolkit/
149+
[mediatoolkitsample]: https://google.github.io/horologist/media-sample/
150+
[wearmediaguidance]: https://developer.android.com/media/implement/surfaces/wear-os#play-downloaded-content
151+
[horologist]: https://google.github.io/horologist/
152+
[entityscreen]: https://github.com/google/horologist/blob/main/media/ui/src/main/java/com/google/android/horologist/media/ui/screens/entity/EntityScreen.kt
153+
[mediappsbestpractices]: https://developer.android.com/design/ui/wear/guides/foundations/media-apps

Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/domain/PodcastCategoryFilterUseCaseTest.kt

+18
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,24 @@ class PodcastCategoryFilterUseCaseTest {
113113
result.episodes
114114
)
115115
}
116+
117+
@Test
118+
fun whenCategoryInfoNotNull_verifyLimitFlow() = runTest {
119+
val resultFlow = useCase(testCategory.asExternalModel())
120+
121+
categoriesStore.setEpisodesFromPodcast(
122+
testCategory.id,
123+
List(8) { testEpisodeToPodcast }.flatten()
124+
)
125+
categoriesStore.setPodcastsInCategory(
126+
testCategory.id,
127+
List(4) { testPodcasts }.flatten()
128+
)
129+
130+
val result = resultFlow.first()
131+
assertEquals(20, result.episodes.size)
132+
assertEquals(10, result.topPodcasts.size)
133+
}
116134
}
117135

118136
val testPodcasts = listOf(

Jetcaster/core/src/test/kotlin/com/example/jetcaster/core/data/repository/TestCategoryStore.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ class TestCategoryStore : CategoryStore {
4343
categoryId: Long,
4444
limit: Int
4545
): Flow<List<PodcastWithExtraInfo>> = podcastsInCategoryFlow.map {
46-
it[categoryId] ?: emptyList()
46+
it[categoryId]?.take(limit) ?: emptyList()
4747
}
4848

4949
override fun episodesFromPodcastsInCategory(
5050
categoryId: Long,
5151
limit: Int
5252
): Flow<List<EpisodeToPodcast>> = episodesFromPodcasts.map {
53-
it[categoryId] ?: emptyList()
53+
it[categoryId]?.take(limit) ?: emptyList()
5454
}
5555

5656
override suspend fun addCategory(category: Category): Long = -1

Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ import androidx.compose.foundation.layout.Box
2222
import androidx.compose.foundation.layout.fillMaxSize
2323
import androidx.compose.foundation.layout.size
2424
import androidx.compose.material3.CircularProgressIndicator
25-
import androidx.compose.material3.MaterialTheme
2625
import androidx.compose.runtime.Composable
2726
import androidx.compose.runtime.getValue
2827
import androidx.compose.runtime.mutableStateOf
2928
import androidx.compose.runtime.remember
3029
import androidx.compose.runtime.setValue
3130
import androidx.compose.ui.Alignment
3231
import androidx.compose.ui.Modifier
32+
import androidx.compose.ui.graphics.Brush
3333
import androidx.compose.ui.layout.ContentScale
3434
import androidx.compose.ui.platform.LocalContext
3535
import androidx.compose.ui.unit.dp
@@ -43,6 +43,7 @@ fun PodcastImage(
4343
contentDescription: String?,
4444
modifier: Modifier = Modifier,
4545
contentScale: ContentScale = ContentScale.Crop,
46+
placeholderBrush: Brush = thumbnailPlaceholderDefaultBrush(),
4647
) {
4748
var imagePainterState by remember {
4849
mutableStateOf<AsyncImagePainter.State>(AsyncImagePainter.State.Empty)
@@ -73,8 +74,9 @@ fun PodcastImage(
7374
else -> {
7475
Box(
7576
modifier = Modifier
77+
.background(placeholderBrush)
7678
.fillMaxSize()
77-
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
79+
7880
)
7981
}
8082
}

Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/thumbnailPlaceholder.kt renamed to Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt

+20-9
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,30 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.example.jetcaster.tv.ui.component
17+
package com.example.jetcaster.designsystem.component
1818

19+
import androidx.compose.foundation.isSystemInDarkTheme
1920
import androidx.compose.runtime.Composable
2021
import androidx.compose.ui.graphics.Brush
22+
import androidx.compose.ui.graphics.Color
2123
import androidx.compose.ui.graphics.SolidColor
22-
import androidx.compose.ui.graphics.painter.BrushPainter
23-
import androidx.tv.material3.ExperimentalTvMaterial3Api
24-
import androidx.tv.material3.MaterialTheme
24+
import com.example.jetcaster.designsystem.theme.surfaceVariantDark
25+
import com.example.jetcaster.designsystem.theme.surfaceVariantLight
2526

26-
@OptIn(ExperimentalTvMaterial3Api::class)
2727
@Composable
28-
internal fun thumbnailPlaceholder(
29-
brush: Brush = SolidColor(MaterialTheme.colorScheme.surfaceVariant)
30-
): BrushPainter {
31-
return BrushPainter(brush)
28+
internal fun thumbnailPlaceholderDefaultBrush(
29+
color: Color = thumbnailPlaceHolderDefaultColor()
30+
): Brush {
31+
return SolidColor(color)
32+
}
33+
34+
@Composable
35+
private fun thumbnailPlaceHolderDefaultColor(
36+
isInDarkMode: Boolean = isSystemInDarkTheme()
37+
): Color {
38+
return if (isInDarkMode) {
39+
surfaceVariantDark
40+
} else {
41+
surfaceVariantLight
42+
}
3243
}

Jetcaster/tv-app/build.gradle.kts

+4-4
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ android {
4040
buildTypes {
4141
getByName("release") {
4242
isMinifyEnabled = true
43-
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"),
44-
"proguard-rules.pro")
43+
proguardFiles(
44+
getDefaultProguardFile("proguard-android-optimize.txt"),
45+
"proguard-rules.pro"
46+
)
4547
}
4648
}
4749

@@ -79,15 +81,13 @@ dependencies {
7981
implementation(libs.androidx.lifecycle.runtime.compose)
8082
implementation(libs.androidx.activity.compose)
8183
implementation(libs.androidx.navigation.compose)
82-
implementation(libs.coil.kt.compose)
8384

8485
// Dependency injection
8586
implementation(libs.androidx.hilt.navigation.compose)
8687
implementation(libs.hilt.android)
8788
implementation(project(":core:model"))
8889
ksp(libs.hilt.compiler)
8990

90-
9191
implementation(project(":core"))
9292
implementation(project(":designsystem"))
9393

Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/PodcastList.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
package com.example.jetcaster.tv.model
1818

1919
import androidx.compose.runtime.Immutable
20-
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
20+
import com.example.jetcaster.core.model.PodcastInfo
2121

2222
@Immutable
2323
data class PodcastList(
24-
val member: List<PodcastWithExtraInfo>
25-
) : List<PodcastWithExtraInfo> by member
24+
val member: List<PodcastInfo>
25+
) : List<PodcastInfo> by member

Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) {
141141
LibraryScreen(
142142
navigateToDiscover = jetcasterAppState::navigateToDiscover,
143143
showPodcastDetails = {
144-
jetcasterAppState.showPodcastDetails(it.podcast.uri)
144+
jetcasterAppState.showPodcastDetails(it.uri)
145145
},
146146
playEpisode = {
147147
jetcasterAppState.playEpisode()
@@ -156,7 +156,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) {
156156
composable(Screen.Search.route) {
157157
SearchScreen(
158158
onPodcastSelected = {
159-
jetcasterAppState.showPodcastDetails(it.podcast.uri)
159+
jetcasterAppState.showPodcastDetails(it.uri)
160160
},
161161
modifier = Modifier
162162
.padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues())

Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt

+32-21
Original file line numberDiff line numberDiff line change
@@ -23,43 +23,54 @@ import androidx.compose.runtime.Composable
2323
import androidx.compose.ui.Alignment
2424
import androidx.compose.ui.Modifier
2525
import androidx.compose.ui.graphics.Color
26-
import com.example.jetcaster.core.data.database.model.Podcast
2726
import com.example.jetcaster.core.model.PlayerEpisode
27+
import com.example.jetcaster.core.model.PodcastInfo
2828
import com.example.jetcaster.designsystem.component.ImageBackgroundRadialGradientScrim
2929

3030
@Composable
31-
internal fun Background(
32-
podcast: Podcast,
33-
modifier: Modifier = Modifier,
34-
) = Background(imageUrl = podcast.imageUrl, modifier)
35-
36-
@Composable
37-
internal fun Background(
38-
episode: PlayerEpisode,
31+
internal fun BackgroundContainer(
32+
playerEpisode: PlayerEpisode,
3933
modifier: Modifier = Modifier,
40-
) = Background(imageUrl = episode.podcastImageUrl, modifier)
34+
contentAlignment: Alignment = Alignment.Center,
35+
content: @Composable BoxScope.() -> Unit
36+
) =
37+
BackgroundContainer(
38+
imageUrl = playerEpisode.podcastImageUrl,
39+
modifier,
40+
contentAlignment,
41+
content
42+
)
4143

4244
@Composable
43-
internal fun Background(
44-
imageUrl: String?,
45+
internal fun BackgroundContainer(
46+
podcastInfo: PodcastInfo,
4547
modifier: Modifier = Modifier,
46-
) {
47-
ImageBackgroundRadialGradientScrim(
48-
url = imageUrl,
49-
colors = listOf(Color.Black, Color.Transparent),
50-
modifier = modifier,
51-
)
52-
}
48+
contentAlignment: Alignment = Alignment.Center,
49+
content: @Composable BoxScope.() -> Unit
50+
) =
51+
BackgroundContainer(imageUrl = podcastInfo.imageUrl, modifier, contentAlignment, content)
5352

5453
@Composable
5554
internal fun BackgroundContainer(
56-
playerEpisode: PlayerEpisode,
55+
imageUrl: String,
5756
modifier: Modifier = Modifier,
5857
contentAlignment: Alignment = Alignment.Center,
5958
content: @Composable BoxScope.() -> Unit
6059
) {
6160
Box(modifier = modifier, contentAlignment = contentAlignment) {
62-
Background(episode = playerEpisode, modifier = Modifier.fillMaxSize())
61+
Background(imageUrl = imageUrl, modifier = Modifier.fillMaxSize())
6362
content()
6463
}
6564
}
65+
66+
@Composable
67+
private fun Background(
68+
imageUrl: String,
69+
modifier: Modifier = Modifier,
70+
) {
71+
ImageBackgroundRadialGradientScrim(
72+
url = imageUrl,
73+
colors = listOf(Color.Black, Color.Transparent),
74+
modifier = modifier,
75+
)
76+
}

0 commit comments

Comments
 (0)