From 4d024e69616c8c186fdbd0ff2b9ab5cc1429ec27 Mon Sep 17 00:00:00 2001 From: Kimin Ryu Date: Wed, 30 Aug 2023 18:24:46 +0900 Subject: [PATCH 01/42] init :core:playback module --- core/playback/.gitignore | 1 + core/playback/build.gradle.kts | 11 +++++++++++ core/playback/src/main/AndroidManifest.xml | 4 ++++ gradle/libs.versions.toml | 5 +++++ settings.gradle.kts | 1 + 5 files changed, 22 insertions(+) create mode 100644 core/playback/.gitignore create mode 100644 core/playback/build.gradle.kts create mode 100644 core/playback/src/main/AndroidManifest.xml diff --git a/core/playback/.gitignore b/core/playback/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/playback/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/playback/build.gradle.kts b/core/playback/build.gradle.kts new file mode 100644 index 00000000..55e34cfc --- /dev/null +++ b/core/playback/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("droidknights.android.library") +} + +android { + namespace = "com.droidknights.app2023.core.playback" +} + +dependencies { + implementation(projects.core.model) +} diff --git a/core/playback/src/main/AndroidManifest.xml b/core/playback/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/core/playback/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55a6a3ee..f6ad8117 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ androidxComposeCompiler = "1.4.7" androidxComposeNavigation = "2.6.0" androidxComposeMaterial3 = "1.1.0" androidxActivity = "1.7.2" +androidxMedia3 = "1.1.1" hilt = "2.46.1" hiltNavigationCompose = "1.0.0" @@ -61,6 +62,10 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" } androidx-compose-navigation-test = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxComposeNavigation" } +androidx-media3-player = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "androidxMedia3" } +androidx-media3-player-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "androidxMedia3" } +androidx-media3-player-session = { group = "androidx.media3", name = "media3-session", version.ref = "androidxMedia3" } + hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 232f5f93..9845078e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,7 @@ include( ":core:domain", ":core:navigation", ":core:model", + ":core:playback", ":core:ui", ":core:testing", ":core:datastore", From 69cb9060bf72d9092a3bbea81429ab1c5881d4bc Mon Sep 17 00:00:00 2001 From: Kimin Ryu Date: Wed, 30 Aug 2023 18:59:51 +0900 Subject: [PATCH 02/42] =?UTF-8?q?sessions.json=EC=97=90=20video=20sample?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/data/src/main/assets/sessions.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/data/src/main/assets/sessions.json b/core/data/src/main/assets/sessions.json index c07fbec7..89c076d8 100644 --- a/core/data/src/main/assets/sessions.json +++ b/core/data/src/main/assets/sessions.json @@ -122,7 +122,11 @@ ], "room": "Track3", "startTime": "2023-09-12T11:45:00.000", - "endTime": "2023-09-12T12:15:00.000" + "endTime": "2023-09-12T12:15:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "8", From 3c0e6efe95be46337cecfafc8bacbe25adc40a0d Mon Sep 17 00:00:00 2001 From: Kimin Ryu Date: Wed, 30 Aug 2023 19:06:54 +0900 Subject: [PATCH 03/42] =?UTF-8?q?Video=20model=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20Mapper=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - manifestUrl: 재생을 위한 dash manifest url - thumbnailUrl: 영상의 썸네일 url --- .../app2023/core/data/api/model/SessionResponse.kt | 1 + .../app2023/core/data/api/model/VideoResponse.kt | 9 +++++++++ .../app2023/core/data/mapper/SessionMapper.kt | 10 +++++++++- .../com/droidknights/app2023/core/model/Session.kt | 1 + .../java/com/droidknights/app2023/core/model/Video.kt | 6 ++++++ 5 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 core/data/src/main/java/com/droidknights/app2023/core/data/api/model/VideoResponse.kt create mode 100644 core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/api/model/SessionResponse.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/api/model/SessionResponse.kt index 4752eb65..91c43120 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/api/model/SessionResponse.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/api/model/SessionResponse.kt @@ -14,4 +14,5 @@ internal data class SessionResponse( val room: RoomResponse?, val startTime: LocalDateTime, val endTime: LocalDateTime, + val video: VideoResponse? = null, ) diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/api/model/VideoResponse.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/api/model/VideoResponse.kt new file mode 100644 index 00000000..a5b12fc3 --- /dev/null +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/api/model/VideoResponse.kt @@ -0,0 +1,9 @@ +package com.droidknights.app2023.core.data.api.model + +import kotlinx.serialization.Serializable + +@Serializable +internal data class VideoResponse( + val manifestUrl: String, + val thumbnailUrl: String, +) \ No newline at end of file diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt index 599bde65..6304df86 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt @@ -4,11 +4,13 @@ import com.droidknights.app2023.core.data.api.model.LevelResponse import com.droidknights.app2023.core.data.api.model.RoomResponse import com.droidknights.app2023.core.data.api.model.SessionResponse import com.droidknights.app2023.core.data.api.model.SpeakerResponse +import com.droidknights.app2023.core.data.api.model.VideoResponse import com.droidknights.app2023.core.model.Level import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag +import com.droidknights.app2023.core.model.Video internal fun SessionResponse.toData(): Session = Session( id = this.id, @@ -19,7 +21,8 @@ internal fun SessionResponse.toData(): Session = Session( tags = this.tags.map { Tag(it) }, room = this.room?.toData() ?: Room.ETC, startTime = this.startTime, - endTime = this.endTime + endTime = this.endTime, + video = this.video?.toData(), ) internal fun LevelResponse.toData(): Level = when (this) { @@ -41,3 +44,8 @@ internal fun SpeakerResponse.toData(): Speaker = Speaker( introduction = this.introduction, imageUrl = this.imageUrl ) + +internal fun VideoResponse.toData(): Video = Video( + manifestUrl = this.manifestUrl, + thumbnailUrl = this.thumbnailUrl +) diff --git a/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt b/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt index 7c127488..42da9a8f 100644 --- a/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt +++ b/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt @@ -12,4 +12,5 @@ data class Session( val room: Room, val startTime: LocalDateTime, val endTime: LocalDateTime, + val video: Video? = null, ) diff --git a/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt b/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt new file mode 100644 index 00000000..9f666643 --- /dev/null +++ b/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt @@ -0,0 +1,6 @@ +package com.droidknights.app2023.core.model + +data class Video( + val manifestUrl: String, + val thumbnailUrl: String, +) \ No newline at end of file From 7495ff4bcdf0d186477c17044c49bb819ca42879 Mon Sep 17 00:00:00 2001 From: Kimin Ryu Date: Wed, 30 Aug 2023 19:07:52 +0900 Subject: [PATCH 04/42] =?UTF-8?q?GithubRawApi=EC=9D=98=20endPoint=EB=A5=BC?= =?UTF-8?q?=20workspace=20=EC=86=8C=EC=9C=A0=20repository=EC=9D=98=20media?= =?UTF-8?q?3=20branch=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/droidknights/app2023/core/data/api/GithubRawApi.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt index 7cb079ca..4b18d5f6 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt @@ -6,9 +6,9 @@ import retrofit2.http.GET internal interface GithubRawApi { - @GET("/droidknights/DroidKnights2023_App/main/core/data/src/main/assets/sponsors.json") + @GET("/workspace/DroidKnights2023_App/media3/core/data/src/main/assets/sponsors.json") suspend fun getSponsors(): List - @GET("/droidknights/DroidKnights2023_App/main/core/data/src/main/assets/sessions.json") + @GET("/workspace/DroidKnights2023_App/media3/core/data/src/main/assets/sessions.json") suspend fun getSessions(): List } From ef74596e07b8ad464ea04456523a1a38febac68d Mon Sep 17 00:00:00 2001 From: Kimin Ryu Date: Wed, 30 Aug 2023 19:34:50 +0900 Subject: [PATCH 05/42] =?UTF-8?q?Session=20model=20=EB=82=B4=20video=20typ?= =?UTF-8?q?e=EC=9D=84=20non-null=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app2023/core/data/mapper/SessionMapper.kt | 2 +- .../app2023/core/model/Session.kt | 2 +- .../droidknights/app2023/core/model/Video.kt | 8 +++- .../app2023/feature/session/SessionCard.kt | 2 + .../feature/session/SessionDetailScreen.kt | 45 ++++++++++++++----- .../feature/session/SessionDetailViewModel.kt | 8 ++++ 6 files changed, 54 insertions(+), 13 deletions(-) diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt index 6304df86..78943747 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt @@ -22,7 +22,7 @@ internal fun SessionResponse.toData(): Session = Session( room = this.room?.toData() ?: Room.ETC, startTime = this.startTime, endTime = this.endTime, - video = this.video?.toData(), + video = this.video?.toData() ?: Video.None, ) internal fun LevelResponse.toData(): Level = when (this) { diff --git a/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt b/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt index 42da9a8f..2261a775 100644 --- a/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt +++ b/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt @@ -12,5 +12,5 @@ data class Session( val room: Room, val startTime: LocalDateTime, val endTime: LocalDateTime, - val video: Video? = null, + val video: Video, ) diff --git a/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt b/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt index 9f666643..1503f10a 100644 --- a/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt +++ b/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt @@ -3,4 +3,10 @@ package com.droidknights.app2023.core.model data class Video( val manifestUrl: String, val thumbnailUrl: String, -) \ No newline at end of file +) { + val isReady = manifestUrl.isNotBlank() && thumbnailUrl.isNotBlank() + + companion object { + val None = Video("", "") + } +} \ No newline at end of file diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt index 97f1da13..19dde594 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt @@ -32,6 +32,7 @@ import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag +import com.droidknights.app2023.core.model.Video import kotlinx.datetime.LocalDateTime @Composable @@ -151,6 +152,7 @@ private fun SessionCardPreview() { startTime = LocalDateTime(2023, 9, 12, 16, 10, 0), endTime = LocalDateTime(2023, 9, 12, 16, 45, 0), room = Room.TRACK1, + video = Video.None ) KnightsTheme { diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt index 569633e2..004f99e9 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconToggleButton @@ -44,9 +46,10 @@ import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag -import kotlinx.coroutines.delay +import com.droidknights.app2023.core.model.Video import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.delay import kotlinx.datetime.LocalDateTime @Composable @@ -71,7 +74,10 @@ internal fun SessionDetailScreen( onBackClick = onBackClick ) Box { - SessionDetailContent(uiState = sessionUiState) + SessionDetailContent( + uiState = sessionUiState, + onPlayButtonClick = { viewModel.playSession() } + ) if (effect is SessionDetailEffect.ShowToastForBookmarkState) { SessionDetailBookmarkStatePopup( bookmarked = (effect as SessionDetailEffect.ShowToastForBookmarkState).bookmarked @@ -113,10 +119,13 @@ private fun SessionDetailTopAppBar( } @Composable -private fun SessionDetailContent(uiState: SessionDetailUiState) { +private fun SessionDetailContent( + uiState: SessionDetailUiState, + onPlayButtonClick: () -> Unit, +) { when (uiState) { is SessionDetailUiState.Loading -> SessionDetailLoading() - is SessionDetailUiState.Success -> SessionDetailContent(uiState.session) + is SessionDetailUiState.Success -> SessionDetailContent(uiState.session, onPlayButtonClick) } } @@ -128,7 +137,10 @@ private fun SessionDetailLoading() { } @Composable -private fun SessionDetailContent(session: Session) { +private fun SessionDetailContent( + session: Session, + onPlayButtonClick: () -> Unit, +) { Column( modifier = Modifier .fillMaxSize() @@ -145,6 +157,17 @@ private fun SessionDetailContent(session: Session) { Spacer(modifier = Modifier.height(40.dp)) } SessionDetailSpeaker(session.speakers.toPersistentList()) + Button( + modifier = Modifier + .padding(top = 16.dp) + .fillMaxWidth(), + enabled = session.video.isReady, + onClick = onPlayButtonClick + ) { + Text( + if (session.video.isReady) "재생하기" else "영상 미제공 세션" + ) + } } } @@ -222,7 +245,7 @@ private fun BookmarkToggleButton( private val SampleSessionHasContent = Session( id = "2", - title = "세션 제목은 세션 제목 - 개요 있음", + title = "세션 제목은 세션 제목 - 개요, 영상 있음", content = "세션에 대한 소개와 세션에서의 장단점과 세션을 실제로 사용한 사례와 세션 내용에 대한 QnA 진행", speakers = listOf( Speaker(name = "스피커1", introduction = "", "https://raw.githubusercontent.com/droidknights/DroidKnights2023_App/main/storage/speaker/차영호.png"), @@ -232,12 +255,13 @@ private val SampleSessionHasContent = Session( tags = listOf(Tag("Dev Environment")), room = Room.TRACK1, startTime = LocalDateTime.parse("2023-09-12T11:00:00.000"), - endTime = LocalDateTime.parse("2023-09-12T11:30:00.000") + endTime = LocalDateTime.parse("2023-09-12T11:30:00.000"), + video = Video("qwer", "asdf") ) private val SampleSessionNoContent = Session( id = "2", - title = "세션 제목은 세션 제목 - 개요 없음", + title = "세션 제목은 세션 제목 - 개요, 영상 없음", content = "", speakers = listOf( Speaker(name = "스피커1", introduction = "", "https://raw.githubusercontent.com/droidknights/DroidKnights2023_App/main/storage/speaker/차영호.png"), @@ -247,7 +271,8 @@ private val SampleSessionNoContent = Session( tags = listOf(Tag("Dev Environment")), room = Room.TRACK1, startTime = LocalDateTime.parse("2023-09-12T11:00:00.000"), - endTime = LocalDateTime.parse("2023-09-12T11:30:00.000") + endTime = LocalDateTime.parse("2023-09-12T11:30:00.000"), + video = Video.None ) class SessionDetailContentProvider : PreviewParameterProvider { @@ -277,7 +302,7 @@ private fun SessionDetailContentPreview( @PreviewParameter(SessionDetailContentProvider::class) session: Session ) { KnightsTheme { - SessionDetailContent(session = session) + SessionDetailContent(session = session, onPlayButtonClick = {}) } } diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt index 237b1ce4..a8778dd4 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt @@ -61,6 +61,14 @@ class SessionDetailViewModel @Inject constructor( } } + fun playSession() { + val uiState = sessionUiState.value + if (uiState !is SessionDetailUiState.Success || !uiState.session.video.isReady) { + return + } + // TODO: play session video + } + fun hidePopup() { viewModelScope.launch { _sessionUiEffect.value = SessionDetailEffect.Idle From e3ce3fef9cb6e58d8edfcc2c9d540e0eb259ffd9 Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 30 Aug 2023 22:23:06 +0900 Subject: [PATCH 06/42] =?UTF-8?q?media3=20dependencies=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/playback/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/playback/build.gradle.kts b/core/playback/build.gradle.kts index 55e34cfc..abdd8a88 100644 --- a/core/playback/build.gradle.kts +++ b/core/playback/build.gradle.kts @@ -8,4 +8,7 @@ android { dependencies { implementation(projects.core.model) + implementation(libs.androidx.media3.player) + implementation(libs.androidx.media3.player.session) + implementation(libs.androidx.media3.player.dash) } From b4504e62eea6c6f6efa93e6e3e8032721c2731e5 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 11:02:18 +0900 Subject: [PATCH 07/42] =?UTF-8?q?sessions.json=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모든 session에 sample video 추가 --- core/data/src/main/assets/sessions.json | 126 ++++++++++++++++++++---- 1 file changed, 105 insertions(+), 21 deletions(-) diff --git a/core/data/src/main/assets/sessions.json b/core/data/src/main/assets/sessions.json index 89c076d8..6504a616 100644 --- a/core/data/src/main/assets/sessions.json +++ b/core/data/src/main/assets/sessions.json @@ -8,7 +8,11 @@ "tags": [], "room": null, "startTime": "2023-09-12T10:45:00.000", - "endTime": "2023-09-12T11:00:00.000" + "endTime": "2023-09-12T11:00:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "2", @@ -27,7 +31,11 @@ ], "room": "Track1", "startTime": "2023-09-12T11:00:00.000", - "endTime": "2023-09-12T11:30:00.000" + "endTime": "2023-09-12T11:30:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "3", @@ -46,7 +54,11 @@ ], "room": "Track2", "startTime": "2023-09-12T11:00:00.000", - "endTime": "2023-09-12T11:30:00.000" + "endTime": "2023-09-12T11:30:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "4", @@ -65,7 +77,11 @@ ], "room": "Track3", "startTime": "2023-09-12T11:00:00.000", - "endTime": "2023-09-12T11:30:00.000" + "endTime": "2023-09-12T11:30:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "5", @@ -84,7 +100,11 @@ ], "room": "Track1", "startTime": "2023-09-12T11:45:00.000", - "endTime": "2023-09-12T12:15:00.000" + "endTime": "2023-09-12T12:15:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "6", @@ -103,7 +123,11 @@ ], "room": "Track2", "startTime": "2023-09-12T11:45:00.000", - "endTime": "2023-09-12T12:15:00.000" + "endTime": "2023-09-12T12:15:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "7", @@ -145,7 +169,11 @@ ], "room": "Track1", "startTime": "2023-09-12T13:25:00.000", - "endTime": "2023-09-12T14:10:00.000" + "endTime": "2023-09-12T14:10:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "9", @@ -164,7 +192,11 @@ ], "room": "Track2", "startTime": "2023-09-12T13:25:00.000", - "endTime": "2023-09-12T14:10:00.000" + "endTime": "2023-09-12T14:10:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "10", @@ -183,7 +215,11 @@ ], "room": "Track3", "startTime": "2023-09-12T13:25:00.000", - "endTime": "2023-09-12T14:10:00.000" + "endTime": "2023-09-12T14:10:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "11", @@ -202,7 +238,11 @@ ], "room": "Track1", "startTime": "2023-09-12T14:25:00.000", - "endTime": "2023-09-12T14:55:00.000" + "endTime": "2023-09-12T14:55:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "12", @@ -221,7 +261,11 @@ ], "room": "Track2", "startTime": "2023-09-12T14:25:00.000", - "endTime": "2023-09-12T14:55:00.000" + "endTime": "2023-09-12T14:55:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "13", @@ -240,7 +284,11 @@ ], "room": "Track3", "startTime": "2023-09-12T14:25:00.000", - "endTime": "2023-09-12T14:55:00.000" + "endTime": "2023-09-12T14:55:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "14", @@ -259,7 +307,11 @@ ], "room": "Track1", "startTime": "2023-09-12T15:10:00.000", - "endTime": "2023-09-12T15:40:00.000" + "endTime": "2023-09-12T15:40:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "15", @@ -278,7 +330,11 @@ ], "room": "Track2", "startTime": "2023-09-12T15:10:00.000", - "endTime": "2023-09-12T15:40:00.000" + "endTime": "2023-09-12T15:40:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "16", @@ -297,7 +353,11 @@ ], "room": "Track3", "startTime": "2023-09-12T15:10:00.000", - "endTime": "2023-09-12T15:40:00.000" + "endTime": "2023-09-12T15:40:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "17", @@ -316,7 +376,11 @@ ], "room": "Track1", "startTime": "2023-09-12T16:00:00.000", - "endTime": "2023-09-12T16:45:00.000" + "endTime": "2023-09-12T16:45:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "18", @@ -335,7 +399,11 @@ ], "room": "Track2", "startTime": "2023-09-12T16:00:00.000", - "endTime": "2023-09-12T16:45:00.000" + "endTime": "2023-09-12T16:45:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "19", @@ -354,7 +422,11 @@ ], "room": "Track3", "startTime": "2023-09-12T16:00:00.000", - "endTime": "2023-09-12T16:45:00.000" + "endTime": "2023-09-12T16:45:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "20", @@ -373,7 +445,11 @@ ], "room": "Track1", "startTime": "2023-09-12T17:00:00.000", - "endTime": "2023-09-12T17:30:00.000" + "endTime": "2023-09-12T17:30:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "21", @@ -392,7 +468,11 @@ ], "room": "Track2", "startTime": "2023-09-12T17:00:00.000", - "endTime": "2023-09-12T17:30:00.000" + "endTime": "2023-09-12T17:30:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } }, { "id": "22", @@ -411,6 +491,10 @@ ], "room": "Track3", "startTime": "2023-09-12T17:00:00.000", - "endTime": "2023-09-12T17:30:00.000" + "endTime": "2023-09-12T17:30:00.000", + "video": { + "manifestUrl": "https://workspace.github.io/media-samples/sample/output.mpd", + "thumbnailUrl": "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + } } ] From 145707a249892de64e1f67eefeb3e44d1ea05e6a Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 11:04:23 +0900 Subject: [PATCH 08/42] =?UTF-8?q?core:playback=20dependencies=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core:domain, kotlinx-coroutines-guava, kotlinx-serialization-json 추가 --- core/playback/build.gradle.kts | 4 ++++ gradle/libs.versions.toml | 1 + 2 files changed, 5 insertions(+) diff --git a/core/playback/build.gradle.kts b/core/playback/build.gradle.kts index abdd8a88..f0447e1c 100644 --- a/core/playback/build.gradle.kts +++ b/core/playback/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("droidknights.android.library") + id("kotlinx-serialization") } android { @@ -8,6 +9,9 @@ android { dependencies { implementation(projects.core.model) + implementation(projects.core.domain) + implementation(libs.coroutines.guava) + implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.media3.player) implementation(libs.androidx.media3.player.session) implementation(libs.androidx.media3.player.dash) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6ad8117..4df728e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,6 +103,7 @@ turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutine" } coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" } +coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "coroutine" } coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutine" } oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "ossLicenses" } From c684829650b9d0fb7886b33ec4f37183babfbab0 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:16:59 +0900 Subject: [PATCH 09/42] libs.versions.toml update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - material-icons-extended 추가 --- .../src/main/kotlin/com/droidknights/app2023/ComposeAndroid.kt | 3 ++- gradle/libs.versions.toml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build-logic/src/main/kotlin/com/droidknights/app2023/ComposeAndroid.kt b/build-logic/src/main/kotlin/com/droidknights/app2023/ComposeAndroid.kt index 8ed9c57c..7ed1f06a 100644 --- a/build-logic/src/main/kotlin/com/droidknights/app2023/ComposeAndroid.kt +++ b/build-logic/src/main/kotlin/com/droidknights/app2023/ComposeAndroid.kt @@ -18,7 +18,8 @@ internal fun Project.configureComposeAndroid() { val bom = libs.findLibrary("androidx-compose-bom").get() add("implementation", platform(bom)) add("androidTestImplementation", platform(bom)) - + + add("implementation", libs.findLibrary("androidx.compose.materialIcons").get()) add("implementation", libs.findLibrary("androidx.compose.material3").get()) add("implementation", libs.findLibrary("androidx.compose.ui").get()) add("implementation", libs.findLibrary("androidx.compose.ui.tooling.preview").get()) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4df728e2..011abe9f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,6 +54,7 @@ androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "li androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } +androidx-compose-materialIcons = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidxComposeMaterial3" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" } From dce8d02a4d2c5bab4c5c95cd12cf858d92d166c0 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:18:40 +0900 Subject: [PATCH 10/42] =?UTF-8?q?=EC=9E=AC=EC=83=9D=20=EC=A4=91=EC=9D=B8?= =?UTF-8?q?=20Session=EC=9D=98=20id=EB=A5=BC=20datastore=EC=97=90=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlaybackPreferencesDataSource.kt | 27 +++++++++++++++++++ .../core/datastore/model/PlaybackData.kt | 5 ++++ 2 files changed, 32 insertions(+) create mode 100644 core/datastore/src/main/java/com/droidknights/app2023/core/datastore/PlaybackPreferencesDataSource.kt create mode 100644 core/datastore/src/main/java/com/droidknights/app2023/core/datastore/model/PlaybackData.kt diff --git a/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/PlaybackPreferencesDataSource.kt b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/PlaybackPreferencesDataSource.kt new file mode 100644 index 00000000..39978e1a --- /dev/null +++ b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/PlaybackPreferencesDataSource.kt @@ -0,0 +1,27 @@ +package com.droidknights.app2023.core.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.droidknights.app2023.core.datastore.model.PlaybackData +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class PlaybackPreferencesDataSource @Inject constructor( + private val dataStore: DataStore +) { + object PreferencesKey { + val CURRENT_SESSION_ID = stringPreferencesKey("CURRENT_SESSION_ID") + } + + val playbackData = dataStore.data.map { preferences -> + preferences[PreferencesKey.CURRENT_SESSION_ID]?.let(::PlaybackData) + } + + suspend fun updateCurrentSessionId(sessionId: String) { + dataStore.edit { preferences -> + preferences[PreferencesKey.CURRENT_SESSION_ID] = sessionId + } + } +} diff --git a/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/model/PlaybackData.kt b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/model/PlaybackData.kt new file mode 100644 index 00000000..2dd08f7f --- /dev/null +++ b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/model/PlaybackData.kt @@ -0,0 +1,5 @@ +package com.droidknights.app2023.core.datastore.model + +data class PlaybackData( + val currentSessionId: String +) \ No newline at end of file From 9a5e2519079ba61ee2603a7a5d6532edaa7305ad Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:20:17 +0900 Subject: [PATCH 11/42] =?UTF-8?q?=EC=9E=AC=EC=83=9D=20=EC=A4=91=EC=9D=B8?= =?UTF-8?q?=20Session=20id=EB=A5=BC=20=EC=A0=80=EC=9E=A5/=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?SessionRepository=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20Usecase=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../droidknights/app2023/core/data/di/DataModule.kt | 4 +++- .../data/repository/DefaultSessionRepository.kt | 10 ++++++++++ .../core/data/repository/SessionRepository.kt | 4 ++++ .../usecase/GetCurrentPlayingSessionUseCase.kt | 13 +++++++++++++ .../usecase/UpdateCurrentPlayingSessionUseCase.kt | 13 +++++++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/GetCurrentPlayingSessionUseCase.kt create mode 100644 core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/UpdateCurrentPlayingSessionUseCase.kt diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/di/DataModule.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/di/DataModule.kt index 9f356fb1..e09d0589 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/di/DataModule.kt @@ -11,6 +11,7 @@ import com.droidknights.app2023.core.data.repository.DefaultSponsorRepository import com.droidknights.app2023.core.data.repository.SessionRepository import com.droidknights.app2023.core.data.repository.SettingsRepository import com.droidknights.app2023.core.data.repository.SponsorRepository +import com.droidknights.app2023.core.datastore.PlaybackPreferencesDataSource import dagger.Binds import dagger.Module import dagger.Provides @@ -47,7 +48,8 @@ internal abstract class DataModule { @Singleton fun provideSessionRepository( githubRawApi: GithubRawApi, - ): SessionRepository = DefaultSessionRepository(githubRawApi) + playbackPreferencesDataSource: PlaybackPreferencesDataSource, + ): SessionRepository = DefaultSessionRepository(githubRawApi, playbackPreferencesDataSource) @Provides @Singleton diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepository.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepository.kt index 672da2cb..1e974df8 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepository.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepository.kt @@ -2,15 +2,18 @@ package com.droidknights.app2023.core.data.repository import com.droidknights.app2023.core.data.api.GithubRawApi import com.droidknights.app2023.core.data.mapper.toData +import com.droidknights.app2023.core.datastore.PlaybackPreferencesDataSource import com.droidknights.app2023.core.model.Session import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import javax.inject.Inject internal class DefaultSessionRepository @Inject constructor( private val githubRawApi: GithubRawApi, + private val preferencesDataSource: PlaybackPreferencesDataSource ) : SessionRepository { private var cachedSessions: List = emptyList() @@ -48,4 +51,11 @@ internal class DefaultSessionRepository @Inject constructor( } } } + + override fun getCurrentPlayingSessionId(): Flow = + preferencesDataSource.playbackData.map { it?.currentSessionId } + + override suspend fun updateCurrentPlayingSessionId(sessionId: String) { + preferencesDataSource.updateCurrentSessionId(sessionId) + } } diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/repository/SessionRepository.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/repository/SessionRepository.kt index 7e2b7061..00968be1 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/repository/SessionRepository.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/repository/SessionRepository.kt @@ -12,4 +12,8 @@ interface SessionRepository { suspend fun getBookmarkedSessionIds(): Flow> suspend fun bookmarkSession(sessionId: String, bookmark: Boolean) + + fun getCurrentPlayingSessionId(): Flow + + suspend fun updateCurrentPlayingSessionId(sessionId: String) } diff --git a/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/GetCurrentPlayingSessionUseCase.kt b/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/GetCurrentPlayingSessionUseCase.kt new file mode 100644 index 00000000..ecd0741f --- /dev/null +++ b/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/GetCurrentPlayingSessionUseCase.kt @@ -0,0 +1,13 @@ +package com.droidknights.app2023.core.domain.usecase + +import com.droidknights.app2023.core.data.repository.SessionRepository +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject + +class GetCurrentPlayingSessionUseCase @Inject constructor( + private val sessionRepository: SessionRepository, +) { + suspend operator fun invoke(): String? { + return sessionRepository.getCurrentPlayingSessionId().firstOrNull() + } +} diff --git a/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/UpdateCurrentPlayingSessionUseCase.kt b/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/UpdateCurrentPlayingSessionUseCase.kt new file mode 100644 index 00000000..775361ed --- /dev/null +++ b/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/UpdateCurrentPlayingSessionUseCase.kt @@ -0,0 +1,13 @@ +package com.droidknights.app2023.core.domain.usecase + +import com.droidknights.app2023.core.data.repository.SessionRepository +import javax.inject.Inject + +class UpdateCurrentPlayingSessionUseCase @Inject constructor( + private val sessionRepository: SessionRepository, +) { + + suspend operator fun invoke(sessionId: String) { + return sessionRepository.updateCurrentPlayingSessionId(sessionId) + } +} From 64f74727b86bcbedb36a7de9d7bd6ec82aaf00c0 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:33:44 +0900 Subject: [PATCH 12/42] =?UTF-8?q?core:playback=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ServiceScoped로 player와 session을 주입 - player의 listener를 기반으로 playbackState에 재생 상태 정보 저장 - MediaLibraryService를 구현 - LibrarySessionCallback 구현을 위해 MediaItemProvider를 구현. - MediaId는 json을 활용하여 세션, 트랙 뿐만 아니라 태그 등을 자유롭게 표현할 수 있도록 함. - 외부 모듈에 제공하는 player 컨트롤을 하기 위한 PlayerController --- core/playback/build.gradle.kts | 2 +- core/playback/src/main/AndroidManifest.xml | 14 +- .../app2023/core/playback/PlayerController.kt | 119 ++++++++++++++ .../core/playback/di/PlaybackModule.kt | 83 ++++++++++ .../core/playback/playstate/PlaybackState.kt | 13 ++ .../playstate/PlaybackStateListener.kt | 85 ++++++++++ .../playstate/PlaybackStateManager.kt | 19 +++ .../session/LibrarySessionCallback.kt | 83 ++++++++++ .../app2023/core/playback/session/MediaId.kt | 41 +++++ .../core/playback/session/MediaItemBuilder.kt | 53 ++++++ .../playback/session/MediaItemProvider.kt | 155 ++++++++++++++++++ .../core/playback/session/PlaybackService.kt | 63 +++++++ .../session/SessionActivityIntentProvider.kt | 7 + core/playback/src/main/res/values/strings.xml | 13 ++ 14 files changed, 747 insertions(+), 3 deletions(-) create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/di/PlaybackModule.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateManager.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/LibrarySessionCallback.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaId.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemBuilder.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/PlaybackService.kt create mode 100644 core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/SessionActivityIntentProvider.kt create mode 100644 core/playback/src/main/res/values/strings.xml diff --git a/core/playback/build.gradle.kts b/core/playback/build.gradle.kts index f0447e1c..1ae4b04b 100644 --- a/core/playback/build.gradle.kts +++ b/core/playback/build.gradle.kts @@ -9,7 +9,7 @@ android { dependencies { implementation(projects.core.model) - implementation(projects.core.domain) + implementation(projects.core.data) implementation(libs.coroutines.guava) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.media3.player) diff --git a/core/playback/src/main/AndroidManifest.xml b/core/playback/src/main/AndroidManifest.xml index a5918e68..3131538b 100644 --- a/core/playback/src/main/AndroidManifest.xml +++ b/core/playback/src/main/AndroidManifest.xml @@ -1,4 +1,14 @@ - - + + + + + + + + \ No newline at end of file diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt new file mode 100644 index 00000000..278c650e --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt @@ -0,0 +1,119 @@ +package com.droidknights.app2023.core.playback + +import android.content.ComponentName +import android.content.Context +import androidx.media3.common.Player +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import com.droidknights.app2023.core.data.repository.SessionRepository +import com.droidknights.app2023.core.playback.session.MediaId +import com.droidknights.app2023.core.playback.session.MediaItemProvider +import com.droidknights.app2023.core.playback.session.PlaybackService +import com.droidknights.app2023.core.playback.session.toMediaIdOrNull +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.guava.asDeferred +import kotlinx.coroutines.launch +import javax.inject.Inject + +class PlayerController @Inject constructor( + private val context: Context, + private val sessionRepository: SessionRepository, + private val mediaItemProvider: MediaItemProvider, +) { + + private var _controller: Deferred = newControllerAsync() + + private fun newControllerAsync() = MediaController + .Builder(context, SessionToken(context, ComponentName(context, PlaybackService::class.java))) + .buildAsync() + .asDeferred() + + @OptIn(ExperimentalCoroutinesApi::class) + private val controller: Deferred + get() { + if (_controller.isCompleted) { + val completedController = _controller.getCompleted() + if (!completedController.isConnected) { + completedController.release() + _controller = newControllerAsync() + } + } + return _controller + } + private val scope = CoroutineScope(Dispatchers.Main.immediate) + + fun setPosition(positionMs: Long) = executeAfterPrepare { controller -> + controller.seekTo(positionMs) + } + + fun fastForward() = executeAfterPrepare { controller -> + controller.seekForward() + } + + fun rewind() = executeAfterPrepare { controller -> + controller.seekBack() + } + + fun previous() = executeAfterPrepare { controller -> + controller.seekToPrevious() + } + + fun next() = executeAfterPrepare { controller -> + controller.seekToNext() + } + + fun play() = executeAfterPrepare { controller -> + controller.play() + } + + fun playPause() = executeAfterPrepare { controller -> + if (controller.isPlaying) { + controller.pause() + } else { + controller.play() + } + } + + fun setSpeed(speed: Float) = executeAfterPrepare { controller -> + controller.setPlaybackSpeed(speed) + } + + private suspend fun maybePrepare(controller: MediaController): Boolean { + val sessionId = sessionRepository.getCurrentPlayingSessionId().first() ?: return false + if (controller.currentSessionId() == sessionId && + controller.playbackState in listOf(Player.STATE_READY, Player.STATE_BUFFERING) + ) { + return true + } + val session = runCatching { sessionRepository.getSession(sessionId) }.getOrNull() ?: return false + controller.setMediaItem(mediaItemProvider.mediaItem(session)) + controller.prepare() + return true + } + + private fun MediaController.currentSessionId(): String? + = (currentMediaItem?.mediaId?.toMediaIdOrNull() as? MediaId.Session)?.id + + private inline fun executeAfterPrepare(crossinline action: suspend (MediaController) -> Unit) { + scope.launch { + val controller = awaitConnect() ?: return@launch + if (maybePrepare(controller)) { + action(controller) + } + } + } + + suspend fun awaitConnect(): MediaController? { + return try { + controller.await() + } catch (e: Exception) { + if (e is CancellationException) throw e + null + } + } +} diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/di/PlaybackModule.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/di/PlaybackModule.kt new file mode 100644 index 00000000..378ba06f --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/di/PlaybackModule.kt @@ -0,0 +1,83 @@ +package com.droidknights.app2023.core.playback.di + +import android.app.Service +import androidx.media3.common.AudioAttributes +import androidx.media3.common.C +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.dash.DashMediaSource +import androidx.media3.exoplayer.dash.DefaultDashChunkSource +import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter +import androidx.media3.session.MediaLibraryService +import com.droidknights.app2023.core.playback.playstate.PlaybackStateListener +import com.droidknights.app2023.core.playback.session.LibrarySessionCallback +import com.droidknights.app2023.core.playback.session.PlaybackService +import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ServiceComponent +import dagger.hilt.android.scopes.ServiceScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob + +@UnstableApi @Module +@InstallIn(ServiceComponent::class) +internal object PlaybackModule { + + @Provides + @ServiceScoped + fun player( + service: Service, + playbackStateListener: PlaybackStateListener, + ) : Player { + val dataSourceFactory = DefaultDataSource.Factory(service) + val mediaSourceFactory = DashMediaSource.Factory( + DefaultDashChunkSource.Factory( + dataSourceFactory.setTransferListener( + DefaultBandwidthMeter.Builder(service).build() + ) + ), + dataSourceFactory + ) + val audioAttributes = AudioAttributes.Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE) + .setUsage(C.USAGE_MEDIA) + .build() + val renderersFactory = DefaultRenderersFactory(service) + .forceEnableMediaCodecAsynchronousQueueing() + return ExoPlayer.Builder(service, renderersFactory, mediaSourceFactory) + .setAudioAttributes(audioAttributes, true) + .setHandleAudioBecomingNoisy(true) + .build() + .also { player -> + playbackStateListener.attachTo(player) + } + } + + @Provides + @ServiceScoped + fun scope(): CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + @Provides + @ServiceScoped + fun session( + service: Service, + player: Player, + callback: LibrarySessionCallback, + sessionActivityIntentProvider: SessionActivityIntentProvider + ): MediaLibraryService.MediaLibrarySession { + return MediaLibraryService.MediaLibrarySession.Builder(service as PlaybackService, player, callback) + .apply { + val pendingIntent = sessionActivityIntentProvider.toPlayer() + if (pendingIntent != null) { + setSessionActivity(pendingIntent) + } + } + .build() + } +} \ No newline at end of file diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt new file mode 100644 index 00000000..7b91192d --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt @@ -0,0 +1,13 @@ +package com.droidknights.app2023.core.playback.playstate + +import androidx.media3.common.C + +data class PlaybackState( + val isPlaying: Boolean = false, + val hasPrevious: Boolean = false, + val hasNext: Boolean = false, + val position: Long = C.TIME_UNSET, + val duration: Long = C.TIME_UNSET, + val speed: Float = 1F, + val aspectRatio: Float = 16F / 9F +) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt new file mode 100644 index 00000000..62f654a7 --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt @@ -0,0 +1,85 @@ +package com.droidknights.app2023.core.playback.playstate + +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player +import androidx.media3.common.VideoSize +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +internal class PlaybackStateListener @Inject constructor( + private val scope: CoroutineScope, + private val playbackStateManager: PlaybackStateManager +) : Player.Listener { + + private lateinit var player: Player + + fun attachTo(player: Player) { + this.player = player + player.addListener(this) + + scope.launch { + playbackStateManager.flow + .map { it.isPlaying } + .collectLatest { isPlaying -> + if (isPlaying) { + while (true) { + updatePlayState() + delay(400.milliseconds) + } + } + } + } + } + + override fun onPlaybackStateChanged(playbackState: Int) { + updatePlayState() + } + + override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { + updatePlayState() + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + updatePlayState() + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + updatePlayState() + } + + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + updatePlayState() + } + + override fun onVideoSizeChanged(videoSize: VideoSize) { + updatePlayState() + } + + private fun updatePlayState() { + val playbackState = player.playbackState + playbackStateManager.playbackState = PlaybackState( + isPlaying = when { + playbackState == Player.STATE_ENDED || playbackState == Player.STATE_IDLE -> false + player.playWhenReady -> true + else -> false + }, + hasPrevious = player.currentMediaItemIndex == 0 && player.hasPreviousMediaItem(), + hasNext = player.hasNextMediaItem(), + position = player.contentPosition, + duration = player.duration, + speed = player.playbackParameters.speed, + aspectRatio = with(player.videoSize) { + if (height == 0 || width == 0) 16F / 9F else width * pixelWidthHeightRatio / height + } + ) + } +} \ No newline at end of file diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateManager.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateManager.kt new file mode 100644 index 00000000..f17364fa --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateManager.kt @@ -0,0 +1,19 @@ +package com.droidknights.app2023.core.playback.playstate + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject +import javax.inject.Singleton + + +@Singleton +class PlaybackStateManager @Inject constructor() { + private val _playbackState = MutableStateFlow(PlaybackState()) + + val flow: StateFlow get() = _playbackState + var playbackState: PlaybackState + set(value) { + _playbackState.value = value + } + get() = _playbackState.value +} \ No newline at end of file diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/LibrarySessionCallback.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/LibrarySessionCallback.kt new file mode 100644 index 00000000..662e7f2b --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/LibrarySessionCallback.kt @@ -0,0 +1,83 @@ +package com.droidknights.app2023.core.playback.session + +import androidx.media3.common.MediaItem +import androidx.media3.session.LibraryResult +import androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED +import androidx.media3.session.MediaLibraryService.LibraryParams +import androidx.media3.session.MediaLibraryService.MediaLibrarySession +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ControllerInfo +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.guava.future +import javax.inject.Inject + +internal class LibrarySessionCallback @Inject constructor( + private val mediaItemProvider: MediaItemProvider, + private val scope: CoroutineScope, +) : MediaLibrarySession.Callback { + + override fun onGetLibraryRoot( + session: MediaLibrarySession, + browser: ControllerInfo, + params: LibraryParams?, + ): ListenableFuture> { + if (params?.isRecent == true) { + return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)) + } + return Futures.immediateFuture(LibraryResult.ofItem(mediaItemProvider.root(), params)) + } + + override fun onGetItem( + session: MediaLibrarySession, + browser: ControllerInfo, + mediaId: String, + ): ListenableFuture> = scope.future { + val item = mediaItemProvider.item(mediaId) + if (item != null) { + LibraryResult.ofItem(item, null) + } else { + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + } + } + + override fun onGetChildren( + session: MediaLibrarySession, + browser: ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: LibraryParams?, + ): ListenableFuture>> = scope.future { + val children = mediaItemProvider.children(parentId) + if (children != null) { + LibraryResult.ofItemList(children, params) + } else { + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + } + } + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: ControllerInfo, + mediaItems: List + ): ListenableFuture> = scope.future { + mediaItems.map { mediaItem -> + mediaItemProvider.item(mediaItem.mediaId) ?: mediaItem + } + } + + override fun onSubscribe( + session: MediaLibrarySession, + browser: ControllerInfo, + parentId: String, + params: LibraryParams? + ): ListenableFuture> = scope.future { + val children = mediaItemProvider.children(parentId) + ?: return@future LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + session.notifyChildrenChanged(browser, parentId, children.size, params) + LibraryResult.ofVoid() + } +} diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaId.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaId.kt new file mode 100644 index 00000000..aa004f4e --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaId.kt @@ -0,0 +1,41 @@ +package com.droidknights.app2023.core.playback.session + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed interface MediaId { + @Serializable + @SerialName("root") + object Root : MediaId + + @Serializable + @SerialName("session") + data class Session(val id: String) : MediaId + + @Serializable + @SerialName("tag") + data class Tag( + val name: String, + ) : MediaId + + @Serializable + @SerialName("track") + sealed interface Track : MediaId { + @Serializable + @SerialName("keynote") + object Keynote : Track + + @Serializable + @SerialName("track-01") + object TrackOne : Track + + @Serializable + @SerialName("track-02") + object TrackTwo : Track + + @Serializable + @SerialName("track-03") + object TrackThree : Track + } +} \ No newline at end of file diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemBuilder.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemBuilder.kt new file mode 100644 index 00000000..a0b10c53 --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemBuilder.kt @@ -0,0 +1,53 @@ +package com.droidknights.app2023.core.playback.session + +import android.net.Uri +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json + +internal fun MediaItem( + title: String, + mediaId: MediaId, + isPlayable: Boolean, + browsable: Boolean, + description: String? = null, + album: String? = null, + artist: String? = null, + genre: String? = null, + sourceUri: Uri? = null, + imageUri: Uri? = null, +): MediaItem { + val metadata = + MediaMetadata.Builder() + .setAlbumTitle(album) + .setTitle(title) + .setDescription(description) + .setArtist(artist) + .setGenre(genre) + .setIsBrowsable(browsable) + .setIsPlayable(isPlayable) + .setArtworkUri(imageUri) + .setMediaType( + when (mediaId) { + is MediaId.Session -> MediaMetadata.MEDIA_TYPE_VIDEO + is MediaId.Track -> MediaMetadata.MEDIA_TYPE_PLAYLIST + is MediaId.Tag -> MediaMetadata.MEDIA_TYPE_PLAYLIST + MediaId.Root -> MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS + }, + ) + .build() + + return MediaItem.Builder() + .setMediaId(Json.encodeToString(MediaId.serializer(), mediaId)) + .setMediaMetadata(metadata) + .setUri(sourceUri) + .build() +} + +fun String.toMediaIdOrNull(): MediaId? = + try { + Json.decodeFromString(MediaId.serializer(), this) + } catch (e: SerializationException) { + null + } diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt new file mode 100644 index 00000000..3ed45cff --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt @@ -0,0 +1,155 @@ +package com.droidknights.app2023.core.playback.session + +import android.app.Application +import android.net.Uri +import androidx.media3.common.MediaItem +import com.droidknights.app2023.core.data.repository.SessionRepository +import com.droidknights.app2023.core.model.Room +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.core.playback.R +import javax.inject.Inject + +class MediaItemProvider @Inject constructor( + private val sessionRepository: SessionRepository, + private val application: Application, +) { + + fun root(): MediaItem = MediaItem( + title = application.getString(R.string.media_session_root_title), + description = application.getString(R.string.media_session_root_description), + browsable = true, + isPlayable = false, + mediaId = MediaId.Root, + ) + + suspend fun item(id: String): MediaItem? { + val mediaId = id.toMediaIdOrNull() ?: return null + return when (mediaId) { + MediaId.Root -> root() + is MediaId.Session -> { sessionRepository.getSession(mediaId.id).let(::mediaItem) } + is MediaId.Tag -> { mediaItem(mediaId) } + is MediaId.Track -> { + val sessions = sessionRepository.getSessions() + mediaItem(mediaId, sessions) + } + } + } + + suspend fun children(id: String): List? { + val mediaId = id.toMediaIdOrNull() ?: return null + val sessions = runCatching { sessionRepository.getSessions() }.getOrNull() ?: return null + return when (mediaId) { + MediaId.Root -> { + listOf( + mediaItem(track = MediaId.Track.Keynote, sessions = sessions), + mediaItem(track = MediaId.Track.TrackOne, sessions = sessions), + mediaItem(track = MediaId.Track.TrackTwo, sessions = sessions), + mediaItem(track = MediaId.Track.TrackThree, sessions = sessions), + ) + } + is MediaId.Track.Keynote -> { mediaItems(mediaId, sessions) } + is MediaId.Track -> { mediaItems(mediaId, sessions) } + is MediaId.Tag -> { mediaItems(mediaId, sessions) } + is MediaId.Session -> { + runCatching { sessionRepository.getSession(mediaId.id) }.getOrNull() + ?.let { session -> listOf(mediaItem(session)) } + } + } + } + + private fun mediaItem( + track: MediaId.Track, + sessions: List, + ) = MediaItem( + title = when (track) { + is MediaId.Track.Keynote -> application.getString(R.string.media_session_keynote_title) + is MediaId.Track.TrackOne -> application.getString(R.string.media_session_track_1_title) + is MediaId.Track.TrackTwo -> application.getString(R.string.media_session_track_2_title) + is MediaId.Track.TrackThree -> application.getString(R.string.media_session_track_3_title) + }, + description = when (track) { + is MediaId.Track.Keynote -> application.getString(R.string.media_session_keynote_description) + is MediaId.Track.TrackOne -> application.getString(R.string.media_session_track_1_description) + is MediaId.Track.TrackTwo -> application.getString(R.string.media_session_track_2_description) + is MediaId.Track.TrackThree -> application.getString(R.string.media_session_track_3_description) + }, + mediaId = track, + browsable = true, + isPlayable = false, + imageUri = when (track) { + is MediaId.Track.Keynote -> Uri.parse("https://raw.githubusercontent.com/workspace/media-samples/main/img/logo.jpg") + is MediaId.Track.TrackOne -> Uri.parse("https://raw.githubusercontent.com/workspace/media-samples/main/img/track1.jpg") + is MediaId.Track.TrackTwo -> Uri.parse("https://raw.githubusercontent.com/workspace/media-samples/main/img/track2.jpg") + is MediaId.Track.TrackThree -> Uri.parse("https://raw.githubusercontent.com/workspace/media-samples/main/img/track3.jpg") + }, + artist = room(track).let { room -> + sessions.filter { session -> session.room == room } + .mapNotNull { it.speakers.firstOrNull() } + .joinToString(", ") { it.name } + }, + ) + + private fun mediaItem( + tag: MediaId.Tag + ) = MediaItem( + title = tag.name, + description = "${tag.name}에 관한 발표 목록입니다.", + mediaId = tag, + browsable = true, + isPlayable = false, + imageUri = Uri.parse("https://raw.githubusercontent.com/workspace/media-samples/main/img/logo.jpg"), + ) + + private fun mediaItems( + track: MediaId.Track.Keynote, + sessions: List, + ): List { + return room(track).let { room -> + sessions.filter { session -> session.room == room } + .map(::mediaItem) + } + sessions + .flatMap { session -> session.tags } + .distinctBy { it.name } + .map { tag -> MediaId.Tag(tag.name) } + .map { tag -> mediaItem(tag) } + } + + private fun mediaItems( + track: MediaId.Track, + sessions: List, + ) = room(track).let { room -> + sessions.filter { session -> session.room == room } + .map(::mediaItem) + } + + private fun mediaItems( + tag: MediaId.Tag, + sessions: List, + ) = sessions + .filter { session -> session.tags.any { it.name == tag.name } } + .map(::mediaItem) + + + fun mediaItem(session: Session): MediaItem = MediaItem( + title = session.title, + description = session.content, + mediaId = MediaId.Session(session.id), + browsable = false, + isPlayable = true, + sourceUri = session.video.manifestUrl.takeIf { it.isNotBlank() }?.let(Uri::parse), + imageUri = session.speakers.firstOrNull() + ?.imageUrl.takeIf { !it.isNullOrBlank() } + ?.let(Uri::parse) + ?: Uri.parse("https://raw.githubusercontent.com/workspace/media-samples/main/img/logo.jpg"), + artist = session.speakers.joinToString(",") { it.name }, + ) + + private fun room( + track: MediaId.Track + ) = when (track) { + is MediaId.Track.Keynote -> Room.ETC + is MediaId.Track.TrackOne -> Room.TRACK1 + is MediaId.Track.TrackTwo -> Room.TRACK2 + is MediaId.Track.TrackThree -> Room.TRACK3 + } +} diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/PlaybackService.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/PlaybackService.kt new file mode 100644 index 00000000..de969b45 --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/PlaybackService.kt @@ -0,0 +1,63 @@ +package com.droidknights.app2023.core.playback.session + +import android.content.Intent +import androidx.media3.common.Player +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import javax.inject.Inject + +@AndroidEntryPoint +class PlaybackService : MediaLibraryService() { + + @Inject + lateinit var session: MediaLibrarySession + + @Inject + lateinit var scope: CoroutineScope + + @Inject + lateinit var player: Player + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + if (!player.playWhenReady) { + // If the player isn't set to play when ready, the service is stopped and resources released. + // This is done because if the app is swiped away from recent apps without this check, + // the notification would remain in an unresponsive state. + // Further explanation can be found at: https://github.com/androidx/media/issues/167#issuecomment-1615184728 + release() + stopSelf() + } + } + + private fun release() { + player.release() + session.release() + scope.cancel() + } + + override fun onDestroy() { + super.onDestroy() + release() + } + + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? { + return session.takeUnless { session -> + session.invokeIsReleased + } + } +} + +private val MediaSession.invokeIsReleased: Boolean + get() = try { + // temporarily checked to debug + // https://github.com/androidx/media/issues/422 + MediaSession::class.java.getDeclaredMethod("isReleased") + .apply { isAccessible = true } + .invoke(this) as Boolean + } catch (e: Exception) { + false + } \ No newline at end of file diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/SessionActivityIntentProvider.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/SessionActivityIntentProvider.kt new file mode 100644 index 00000000..c12e6c42 --- /dev/null +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/SessionActivityIntentProvider.kt @@ -0,0 +1,7 @@ +package com.droidknights.app2023.core.playback.session + +import android.app.PendingIntent + +interface SessionActivityIntentProvider { + fun toPlayer(): PendingIntent? +} \ No newline at end of file diff --git a/core/playback/src/main/res/values/strings.xml b/core/playback/src/main/res/values/strings.xml new file mode 100644 index 00000000..4f86173a --- /dev/null +++ b/core/playback/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + 모든 세션 + DroidKnights 2023 전체 발표 목록 입니다. + Keynote + DroidKnights 2023 Keynote + Track 01 + DroidKnights 2023 Track 01 발표 목록 입니다. + Track 02 + DroidKnights 2023 Track 02 목록 입니다. + Track 03 + DroidKnights 2023 Track 03 목록 입니다. + \ No newline at end of file From 9190130ff3927697b02aa25bd7f94c9e87bd844c Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:38:46 +0900 Subject: [PATCH 13/42] =?UTF-8?q?feature:player=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - player는 session id를 선택적인 argument로 가진다. - argument, 마지막 재생된 세션, 첫번째 세션 순으로 재생할 session의 id를 찾는다. - PlayerView composable은 실험적인 구현이므로 검증이 더 필요하다. - 추후 플레이어 알림 클릭 시 사용할 deeplink 추가 --- feature/player/.gitignore | 1 + feature/player/build.gradle.kts | 15 ++ feature/player/src/main/AndroidManifest.xml | 3 + .../app2023/feature/player/PlayerScreen.kt | 231 ++++++++++++++++++ .../app2023/feature/player/PlayerUiState.kt | 16 ++ .../app2023/feature/player/PlayerView.kt | 58 +++++ .../app2023/feature/player/PlayerViewModel.kt | 68 ++++++ .../player/navigation/PlayerNavigation.kt | 41 ++++ settings.gradle.kts | 1 + 9 files changed, 434 insertions(+) create mode 100644 feature/player/.gitignore create mode 100644 feature/player/build.gradle.kts create mode 100644 feature/player/src/main/AndroidManifest.xml create mode 100644 feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerScreen.kt create mode 100644 feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerUiState.kt create mode 100644 feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerView.kt create mode 100644 feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerViewModel.kt create mode 100644 feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/navigation/PlayerNavigation.kt diff --git a/feature/player/.gitignore b/feature/player/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/player/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/player/build.gradle.kts b/feature/player/build.gradle.kts new file mode 100644 index 00000000..7a84837e --- /dev/null +++ b/feature/player/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("droidknights.android.feature") +} + +android { + namespace = "com.droidknights.app2023.feature.player" +} + +dependencies { + implementation(projects.core.playback) + implementation(libs.coroutines.guava) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.media3.player) + implementation(libs.androidx.media3.player.session) +} diff --git a/feature/player/src/main/AndroidManifest.xml b/feature/player/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9a40236b --- /dev/null +++ b/feature/player/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerScreen.kt b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerScreen.kt new file mode 100644 index 00000000..5efcaaa3 --- /dev/null +++ b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerScreen.kt @@ -0,0 +1,231 @@ +package com.droidknights.app2023.feature.player + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme + +@Composable +internal fun PlayerScreen( + onBackClick: () -> Unit, + viewModel: PlayerViewModel = hiltViewModel(), +) { + val playerUiState by viewModel.playerUiState.collectAsStateWithLifecycle() + + CompositionLocalProvider( + LocalContentColor provides Color.White + ) { + PlayerContent( + uiState = playerUiState, + onBackClick = onBackClick, + onPrevButtonClick = viewModel::prev, + onPlayPauseButtonClick = viewModel::playPause, + onNextButtonClick = viewModel::next, + onPositionChange = viewModel::setPosition + ) + } +} + +@Composable +private fun PlayerContent( + uiState: PlayerUiState, + onBackClick: () -> Unit, + onPrevButtonClick: () -> Unit, + onPlayPauseButtonClick: () -> Unit, + onNextButtonClick: () -> Unit, + onPositionChange: (Long) -> Unit, +) { + when (uiState) { + is PlayerUiState.Loading -> PlayerLoading() + is PlayerUiState.Success -> Box( + modifier = Modifier + .background(Color.Black) + .fillMaxSize() + .systemBarsPadding() + ) { + PlayerView( + modifier = Modifier + .align(Alignment.Center) + .aspectRatio(uiState.aspectRatio) + ) + Box(modifier = Modifier.fillMaxSize()) { + BackButton( + onClick = onBackClick, + modifier = Modifier.align(Alignment.TopStart) + ) + Row( + modifier = Modifier.align(Alignment.Center), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PrevButton( + enabled = uiState.hasPrevious, + onClick = onPrevButtonClick + ) + PlayPauseButton( + isPlaying = uiState.isPlaying, + onClick = onPlayPauseButtonClick, + ) + NextButton( + enabled = uiState.hasPrevious, + onClick = onNextButtonClick + ) + } + Row( + modifier = Modifier + .padding(8.dp) + .align(Alignment.BottomCenter), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PositionText(uiState.position) + PositionSeekBar( + modifier = Modifier.weight(1F), + position = uiState.position, + duration = uiState.duration, + onPositionChange = onPositionChange, + ) + PositionText(uiState.duration) + } + } + } + } +} + +@Composable +private fun PlayerLoading() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + +@Composable +internal fun BackButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + IconButton( + onClick = onClick, + modifier = modifier, + ) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "플레이어 종료", + ) + } +} + +@Composable +internal fun PlayPauseButton( + isPlaying: Boolean, + onClick: () -> Unit +) { + IconButton(onClick = onClick) { + if (isPlaying) { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.Pause, + contentDescription = "일시정지", + ) + } else { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.PlayArrow, + contentDescription = "재생", + ) + } + } +} + +@Composable +internal fun PrevButton( + enabled: Boolean, + onClick: () -> Unit +) { + IconButton(enabled = enabled, onClick = onClick) { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.SkipPrevious, + contentDescription = "이전 세션", + ) + } +} + +@Composable +internal fun NextButton( + enabled: Boolean, + onClick: () -> Unit +) { + IconButton(enabled = enabled, onClick = onClick) { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.SkipNext, + contentDescription = "다음 세션", + ) + } +} + +@Composable +internal fun PositionText(amount: Long) { + Text( + style = KnightsTheme.typography.labelSmallM, + text = amount.formatAsDuration() + ) +} + +private fun Long.formatAsDuration(): String { + val hours = this / 1000 / 3600 + val minutes = ((this / 1000) % 3600) / 60 + val seconds = (this / 1000) % 60 + + return when { + hours > 0 -> String.format("%02d:%02d:%02d", hours, minutes, seconds) + else -> String.format("%02d:%02d", minutes, seconds) + } +} +@Composable +internal fun PositionSeekBar( + modifier: Modifier = Modifier, + position: Long, + duration: Long, + onPositionChange: (Long) -> Unit, +) { + Slider( + modifier = modifier, + value = position.toFloat(), + onValueChange = { + onPositionChange(it.toLong()) + }, + valueRange = 0F..duration.toFloat().coerceAtLeast(0F) + ) +} \ No newline at end of file diff --git a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerUiState.kt b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerUiState.kt new file mode 100644 index 00000000..d3646faa --- /dev/null +++ b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerUiState.kt @@ -0,0 +1,16 @@ +package com.droidknights.app2023.feature.player + +sealed interface PlayerUiState { + + object Loading : PlayerUiState + + data class Success( + val isPlaying: Boolean, + val hasPrevious: Boolean, + val hasNext: Boolean, + val position: Long, + val duration: Long, + val speed: Float, + val aspectRatio: Float + ) : PlayerUiState +} \ No newline at end of file diff --git a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerView.kt b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerView.kt new file mode 100644 index 00000000..59a0cd43 --- /dev/null +++ b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerView.kt @@ -0,0 +1,58 @@ +package com.droidknights.app2023.feature.player + +import android.content.ComponentName +import android.view.SurfaceView +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.Player +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import com.droidknights.app2023.core.playback.session.PlaybackService +import kotlinx.coroutines.guava.await + +@Composable +fun PlayerView( + modifier: Modifier = Modifier +) { + val context = LocalContext.current + var player: Player? by remember { mutableStateOf(null) } + var surfaceView: SurfaceView? by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + player = MediaController + .Builder( + context, + SessionToken(context, ComponentName(context, PlaybackService::class.java)) + ) + .buildAsync() + .await() + } + + DisposableEffect(Unit) { + onDispose { + player?.clearVideoSurfaceView(surfaceView) + } + } + + AndroidView( + factory = { + SurfaceView(it).apply { + surfaceView = this + } + }, + update = { + if (player?.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE) == true) { + player?.setVideoSurfaceView(it) + } + }, + modifier = modifier + ) +} diff --git a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerViewModel.kt b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerViewModel.kt new file mode 100644 index 00000000..741170ab --- /dev/null +++ b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerViewModel.kt @@ -0,0 +1,68 @@ +package com.droidknights.app2023.feature.player + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.droidknights.app2023.core.domain.usecase.GetCurrentPlayingSessionUseCase +import com.droidknights.app2023.core.domain.usecase.UpdateCurrentPlayingSessionUseCase +import com.droidknights.app2023.core.playback.PlayerController +import com.droidknights.app2023.core.playback.playstate.PlaybackStateManager +import com.droidknights.app2023.feature.player.navigation.PlayerRoute +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PlayerViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + getCurrentPlayingSessionUseCase: GetCurrentPlayingSessionUseCase, + updateCurrentPlayingSessionUseCase: UpdateCurrentPlayingSessionUseCase, + private val playbackStateManager: PlaybackStateManager, + private val playerController: PlayerController, +) : ViewModel() { + private val _playerUiState = + MutableStateFlow(PlayerUiState.Loading) + val playerUiState: StateFlow = _playerUiState + + init { + viewModelScope.launch { + val sessionId = savedStateHandle.get(PlayerRoute.argumentName) + .takeIf { !it.isNullOrBlank() } + ?: getCurrentPlayingSessionUseCase() + ?: "1" // 처음부터 재생 + updateCurrentPlayingSessionUseCase(sessionId) + playerController.play() + } + viewModelScope.launch { + playbackStateManager.flow.collect { + _playerUiState.value = PlayerUiState.Success( + it.isPlaying, + it.hasPrevious, + it.hasNext, + it.position, + it.duration, + it.speed, + it.aspectRatio + ) + } + } + } + + fun playPause() { + playerController.playPause() + } + + fun prev() { + playerController.previous() + } + + fun next() { + playerController.next() + } + + fun setPosition(position: Long) { + playerController.setPosition(position) + } +} diff --git a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/navigation/PlayerNavigation.kt b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/navigation/PlayerNavigation.kt new file mode 100644 index 00000000..3b0c4936 --- /dev/null +++ b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/navigation/PlayerNavigation.kt @@ -0,0 +1,41 @@ +package com.droidknights.app2023.feature.player.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import androidx.navigation.navDeepLink +import com.droidknights.app2023.feature.player.PlayerScreen + +fun NavController.navigatePlayer(sessionId: String) { + navigate(PlayerRoute.route(sessionId)) +} + +fun NavGraphBuilder.playerNavGraph( + onBackClick: () -> Unit, +) { + composable( + route = PlayerRoute.route("{${PlayerRoute.argumentName}}"), + arguments = listOf( + navArgument("sessionId") { + type = NavType.StringType + defaultValue = "" + } + ), + deepLinks = listOf( + navDeepLink { uriPattern = PlayerRoute.deepLinkUriPattern } + ), + ) { + PlayerScreen( + onBackClick = onBackClick + ) + } +} + +object PlayerRoute { + const val route = "player" + fun route(sessionId: String = ""): String = "$route?$argumentName=$sessionId" + const val argumentName = "sessionId" + const val deepLinkUriPattern = "droidknights://$route" +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9845078e..1b8fbfca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,4 +35,5 @@ include( ":feature:setting", ":feature:contributor", ":feature:bookmark", + ":feature:player", ) From a31c1c0d4ca08f328dc407424edb9771549bca97 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:41:19 +0900 Subject: [PATCH 14/42] =?UTF-8?q?feature:session=EC=97=90=EC=84=9C=20playe?= =?UTF-8?q?r=EB=A1=9C=EC=9D=98=20route=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app2023/feature/session/SessionDetailScreen.kt | 5 ++++- .../app2023/feature/session/SessionDetailViewModel.kt | 8 -------- .../feature/session/navigation/SessionNavigation.kt | 6 +++++- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt index a10e3000..dbcd2631 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt @@ -62,6 +62,7 @@ import kotlinx.datetime.LocalDateTime internal fun SessionDetailScreen( sessionId: String, onBackClick: () -> Unit, + onShowPlayer: () -> Unit, viewModel: SessionDetailViewModel = hiltViewModel(), ) { val scrollState = rememberScrollState() @@ -82,7 +83,9 @@ internal fun SessionDetailScreen( Box { SessionDetailContent( uiState = sessionUiState, - onPlayButtonClick = { viewModel.playSession() } + onPlayButtonClick = { + onShowPlayer() + } ) if (effect is SessionDetailEffect.ShowToastForBookmarkState) { SessionDetailBookmarkStatePopup( diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt index a8778dd4..237b1ce4 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt @@ -61,14 +61,6 @@ class SessionDetailViewModel @Inject constructor( } } - fun playSession() { - val uiState = sessionUiState.value - if (uiState !is SessionDetailUiState.Success || !uiState.session.video.isReady) { - return - } - // TODO: play session video - } - fun hidePopup() { viewModelScope.launch { _sessionUiEffect.value = SessionDetailEffect.Idle diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/navigation/SessionNavigation.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/navigation/SessionNavigation.kt index 9200dbf4..8213d4a8 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/navigation/SessionNavigation.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/navigation/SessionNavigation.kt @@ -20,6 +20,7 @@ fun NavController.navigateSessionDetail(sessionId: String) { fun NavGraphBuilder.sessionNavGraph( onBackClick: () -> Unit, onSessionClick: (Session) -> Unit, + onShowPlayer: (String) -> Unit, onShowErrorSnackBar: (throwable: Throwable?) -> Unit ) { composable(SessionRoute.route) { @@ -41,7 +42,10 @@ fun NavGraphBuilder.sessionNavGraph( val sessionId = navBackStackEntry.arguments?.getString("id") ?: "" SessionDetailScreen( sessionId = sessionId, - onBackClick = onBackClick + onBackClick = onBackClick, + onShowPlayer = { + onShowPlayer(sessionId) + }, ) } } From a399cf8854ef97fa07a3214dcfb83834ffc5c1c8 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:45:33 +0900 Subject: [PATCH 15/42] =?UTF-8?q?feature:main=EC=97=90=EC=84=9C=20player?= =?UTF-8?q?=20NavGraph=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/main/build.gradle.kts | 1 + .../droidknights/app2023/feature/main/MainNavigator.kt | 6 ++++++ .../com/droidknights/app2023/feature/main/MainScreen.kt | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index c9b47813..ef374a6c 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(projects.feature.contributor) implementation(projects.feature.session) implementation(projects.feature.bookmark) + implementation(projects.feature.player) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) diff --git a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainNavigator.kt b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainNavigator.kt index 28fd8d9a..0a6bfa98 100644 --- a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainNavigator.kt @@ -11,6 +11,8 @@ import androidx.navigation.navOptions import com.droidknights.app2023.feature.bookmark.navigation.navigateBookmark import com.droidknights.app2023.feature.contributor.navigation.navigateContributor import com.droidknights.app2023.feature.home.navigation.navigateHome +import com.droidknights.app2023.feature.player.navigation.PlayerRoute +import com.droidknights.app2023.feature.player.navigation.navigatePlayer import com.droidknights.app2023.feature.session.navigation.navigateSession import com.droidknights.app2023.feature.session.navigation.navigateSessionDetail import com.droidknights.app2023.feature.setting.navigation.navigateSetting @@ -57,6 +59,10 @@ internal class MainNavigator( navController.navigateSessionDetail(sessionId) } + fun navigatePlayer(sessionId: String) { + navController.navigatePlayer(sessionId) + } + fun popBackStack() { navController.popBackStack() } diff --git a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainScreen.kt b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainScreen.kt index b79dc829..05e120d1 100644 --- a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainScreen.kt +++ b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainScreen.kt @@ -41,6 +41,7 @@ import com.droidknights.app2023.core.designsystem.theme.surfaceDim import com.droidknights.app2023.feature.bookmark.navigation.bookmarkNavGraph import com.droidknights.app2023.feature.contributor.navigation.contributorNavGraph import com.droidknights.app2023.feature.home.navigation.homeNavGraph +import com.droidknights.app2023.feature.player.navigation.playerNavGraph import com.droidknights.app2023.feature.session.navigation.sessionNavGraph import com.droidknights.app2023.feature.setting.navigation.settingNavGraph import kotlinx.collections.immutable.PersistentList @@ -102,8 +103,15 @@ internal fun MainScreen( sessionNavGraph( onBackClick = { navigator.popBackStack() }, onSessionClick = { navigator.navigateSessionDetail(it.id) }, + onShowPlayer = { sessionId -> + navigator.navigatePlayer(sessionId) + }, onShowErrorSnackBar = onShowErrorSnackBar ) + + playerNavGraph( + onBackClick = { navigator.popBackStack() }, + ) } } }, From baa69028a25a1ea2dd86e52a22e623fbb38559b6 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:46:50 +0900 Subject: [PATCH 16/42] =?UTF-8?q?=ED=94=8C=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EA=B0=80=EB=A1=9C=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20UI=EA=B0=80=20=EC=82=AC=EB=9D=BC?= =?UTF-8?q?=EC=A7=80=EB=8A=94=20=EB=8F=99=EC=9E=91=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app2023/feature/main/MainNavigator.kt | 10 +++++++++ .../app2023/feature/main/MainScreen.kt | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainNavigator.kt b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainNavigator.kt index 0a6bfa98..195039f0 100644 --- a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainNavigator.kt @@ -1,7 +1,9 @@ package com.droidknights.app2023.feature.main +import android.content.res.Configuration.ORIENTATION_PORTRAIT import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration import androidx.navigation.NavDestination import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController @@ -72,6 +74,14 @@ internal class MainNavigator( val currentRoute = currentDestination?.route ?: return false return currentRoute in MainTab } + + @Composable + fun shouldShowSystemUI(): Boolean { + val orientation = LocalConfiguration.current.orientation + val currentRoute = currentDestination?.route ?: return true + return !currentRoute.startsWith(PlayerRoute.route) || + orientation == ORIENTATION_PORTRAIT + } } @Composable diff --git a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainScreen.kt b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainScreen.kt index 05e120d1..64703ad3 100644 --- a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainScreen.kt +++ b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainScreen.kt @@ -1,5 +1,6 @@ package com.droidknights.app2023.feature.main +import android.app.Activity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -27,14 +28,19 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.navigation.compose.NavHost import com.droidknights.app2023.core.designsystem.theme.Neon01 import com.droidknights.app2023.core.designsystem.theme.surfaceDim @@ -69,6 +75,21 @@ internal fun MainScreen( } } + val view = LocalView.current + val shouldShowSystemUI = navigator.shouldShowSystemUI() + LaunchedEffect(shouldShowSystemUI) { + val window = (view.context as Activity).window + WindowCompat.getInsetsController(window, view).apply { + systemBarsBehavior = if (shouldShowSystemUI) { + show(WindowInsetsCompat.Type.systemBars()) + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } else { + hide(WindowInsetsCompat.Type.systemBars()) + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + } + Scaffold( content = { padding -> Box( From 172945ed19106c48dc99bd5a2c314237905a8a87 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:47:54 +0900 Subject: [PATCH 17/42] =?UTF-8?q?app=20module=EC=97=90=EC=84=9C=20SessionA?= =?UTF-8?q?ctivityIntentProvider=20=EB=B0=8F=20di=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 플레이어 알림 클릭 시 player로 이동 --- app/build.gradle.kts | 1 + .../droidknights/app2023/di/AndroidModule.kt | 22 +++++++++++++ .../misc/SessionActivityIntentProviderImpl.kt | 33 +++++++++++++++++++ feature/main/src/main/AndroidManifest.xml | 3 ++ 4 files changed, 59 insertions(+) create mode 100644 app/src/main/java/com/droidknights/app2023/di/AndroidModule.kt create mode 100644 app/src/main/java/com/droidknights/app2023/misc/SessionActivityIntentProviderImpl.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d2eed970..74c0c49c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,6 +26,7 @@ android { dependencies { implementation(projects.core.navigation) + implementation(projects.core.playback) implementation(projects.feature.main) implementation(projects.feature.home) diff --git a/app/src/main/java/com/droidknights/app2023/di/AndroidModule.kt b/app/src/main/java/com/droidknights/app2023/di/AndroidModule.kt new file mode 100644 index 00000000..4f9be928 --- /dev/null +++ b/app/src/main/java/com/droidknights/app2023/di/AndroidModule.kt @@ -0,0 +1,22 @@ +package com.droidknights.app2023.di + +import android.app.Application +import android.content.Context +import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider +import com.droidknights.app2023.misc.SessionActivityIntentProviderImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object AndroidModule { + @Provides + fun provideContext(app: Application): Context = app + + @Provides + fun toPlayerIntentProvider( + impl: SessionActivityIntentProviderImpl + ): SessionActivityIntentProvider = impl +} \ No newline at end of file diff --git a/app/src/main/java/com/droidknights/app2023/misc/SessionActivityIntentProviderImpl.kt b/app/src/main/java/com/droidknights/app2023/misc/SessionActivityIntentProviderImpl.kt new file mode 100644 index 00000000..58df60a7 --- /dev/null +++ b/app/src/main/java/com/droidknights/app2023/misc/SessionActivityIntentProviderImpl.kt @@ -0,0 +1,33 @@ +package com.droidknights.app2023.misc + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.TaskStackBuilder +import androidx.core.net.toUri +import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider +import com.droidknights.app2023.feature.main.MainActivity +import com.droidknights.app2023.feature.player.navigation.PlayerRoute +import javax.inject.Inject + +class SessionActivityIntentProviderImpl @Inject constructor( + private val context: Context, +) : SessionActivityIntentProvider { + override fun toPlayer(): PendingIntent? { + val deepLinkIntent = Intent( + Intent.ACTION_VIEW, + PlayerRoute.deepLinkUriPattern.toUri(), + context, + MainActivity::class.java + ) + + val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(deepLinkIntent) + getPendingIntent( + 0, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + } + return deepLinkPendingIntent + } +} diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml index a1479f61..04670d02 100644 --- a/feature/main/src/main/AndroidManifest.xml +++ b/feature/main/src/main/AndroidManifest.xml @@ -14,6 +14,9 @@ + + + From 01ef8b9a68e15bbc34ace9417ba014f0f8191b71 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 31 Aug 2023 21:48:29 +0900 Subject: [PATCH 18/42] =?UTF-8?q?fix:=20FOREGROUND=5FSERVICE=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 243ce8bd..70448251 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + Date: Thu, 31 Aug 2023 21:50:06 +0900 Subject: [PATCH 19/42] =?UTF-8?q?Android=20Auto=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref. https://developer.android.com/training/cars/media/auto --- app/src/main/AndroidManifest.xml | 3 +++ app/src/main/res/xml/automotive_app_desc.xml | 4 ++++ core/playback/src/main/AndroidManifest.xml | 1 + 3 files changed, 8 insertions(+) create mode 100644 app/src/main/res/xml/automotive_app_desc.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 70448251..2689c914 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,9 @@ android:theme="@style/Theme.DroidKnights2023" tools:targetApi="31"> + + diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/app/src/main/res/xml/automotive_app_desc.xml new file mode 100644 index 00000000..0a6a3c9f --- /dev/null +++ b/app/src/main/res/xml/automotive_app_desc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/playback/src/main/AndroidManifest.xml b/core/playback/src/main/AndroidManifest.xml index 3131538b..f2a171ba 100644 --- a/core/playback/src/main/AndroidManifest.xml +++ b/core/playback/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="ExportedService"> + From 4fda44bb88f9a4c62475d2bb270576add3609433 Mon Sep 17 00:00:00 2001 From: workspace Date: Fri, 1 Sep 2023 16:18:36 +0900 Subject: [PATCH 20/42] =?UTF-8?q?onPlaybackResumption=20=EB=8C=80=EC=9D=91?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playback/session/LibrarySessionCallback.kt | 9 +++++++++ .../core/playback/session/MediaItemProvider.kt | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/LibrarySessionCallback.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/LibrarySessionCallback.kt index 662e7f2b..2fb53d9f 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/LibrarySessionCallback.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/LibrarySessionCallback.kt @@ -80,4 +80,13 @@ internal class LibrarySessionCallback @Inject constructor( session.notifyChildrenChanged(browser, parentId, children.size, params) LibraryResult.ofVoid() } + + override fun onPlaybackResumption( + mediaSession: MediaSession, + controller: ControllerInfo + ): ListenableFuture { + return scope.future { + mediaItemProvider.currentMediaItemsOrKeynote() + } + } } diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt index 3ed45cff..ce25ff91 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt @@ -2,11 +2,14 @@ package com.droidknights.app2023.core.playback.session import android.app.Application import android.net.Uri +import androidx.media3.common.C import androidx.media3.common.MediaItem +import androidx.media3.session.MediaSession.MediaItemsWithStartPosition import com.droidknights.app2023.core.data.repository.SessionRepository import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.playback.R +import kotlinx.coroutines.flow.first import javax.inject.Inject class MediaItemProvider @Inject constructor( @@ -144,6 +147,17 @@ class MediaItemProvider @Inject constructor( artist = session.speakers.joinToString(",") { it.name }, ) + suspend fun currentMediaItemsOrKeynote() : MediaItemsWithStartPosition { + val currentPlayingSessionId = sessionRepository.getCurrentPlayingSessionId().first() ?: "1" + + val session = sessionRepository.getSession(currentPlayingSessionId) + return MediaItemsWithStartPosition( + listOf(mediaItem(session)), + C.INDEX_UNSET, + C.TIME_UNSET, + ) + } + private fun room( track: MediaId.Track ) = when (track) { From a6239cf0a4736cc4d592775e0ed9614def4c99b1 Mon Sep 17 00:00:00 2001 From: workspace Date: Fri, 1 Sep 2023 16:20:01 +0900 Subject: [PATCH 21/42] =?UTF-8?q?Android=20Automotive=20=EC=A7=80=EC=9B=90?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Activity 없이 core:playback module의 PlaybackService만으로 지원. ref. https://developer.android.com/training/cars/media/automotive-os --- app-tv/.gitignore | 1 + app-tv/build.gradle.kts | 29 ++++++++++++++++++ app-tv/proguard-rules.pro | 21 +++++++++++++ app-tv/src/main/AndroidManifest.xml | 25 +++++++++++++++ .../app2023/tv/DroidKnightsApplication.kt | 7 +++++ .../app2023/tv/di/AndroidModule.kt | 23 ++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 +++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 +++ .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 2172 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 3440 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 3786 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1448 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 2522 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2320 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 3000 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 3982 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 5260 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 4598 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 4564 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 6990 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 5532 bytes .../ic_launcher_foreground.webp | Bin 0 -> 4388 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 8966 bytes app-tv/src/main/res/values/colors.xml | 10 ++++++ .../res/values/ic_launcher_background.xml | 4 +++ app-tv/src/main/res/values/strings.xml | 3 ++ .../src/main/res/xml/automotive_app_desc.xml | 4 +++ settings.gradle.kts | 1 + 28 files changed, 138 insertions(+) create mode 100644 app-tv/.gitignore create mode 100644 app-tv/build.gradle.kts create mode 100644 app-tv/proguard-rules.pro create mode 100644 app-tv/src/main/AndroidManifest.xml create mode 100644 app-tv/src/main/kotlin/com/droidknights/app2023/tv/DroidKnightsApplication.kt create mode 100644 app-tv/src/main/kotlin/com/droidknights/app2023/tv/di/AndroidModule.kt create mode 100644 app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/values/colors.xml create mode 100644 app-tv/src/main/res/values/ic_launcher_background.xml create mode 100644 app-tv/src/main/res/values/strings.xml create mode 100644 app-tv/src/main/res/xml/automotive_app_desc.xml diff --git a/app-tv/.gitignore b/app-tv/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app-tv/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app-tv/build.gradle.kts b/app-tv/build.gradle.kts new file mode 100644 index 00000000..808490d9 --- /dev/null +++ b/app-tv/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("droidknights.android.application") +} + +android { + namespace = "com.droidknights.app2023.tv" + + defaultConfig { + applicationId = "com.droidknights.app2023.tv" + versionCode = 1 + versionName = "1.0" + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + implementation(projects.core.playback) + implementation(projects.core.designsystem) +} diff --git a/app-tv/proguard-rules.pro b/app-tv/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app-tv/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app-tv/src/main/AndroidManifest.xml b/app-tv/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c9045dc8 --- /dev/null +++ b/app-tv/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-tv/src/main/kotlin/com/droidknights/app2023/tv/DroidKnightsApplication.kt b/app-tv/src/main/kotlin/com/droidknights/app2023/tv/DroidKnightsApplication.kt new file mode 100644 index 00000000..8ac9cf56 --- /dev/null +++ b/app-tv/src/main/kotlin/com/droidknights/app2023/tv/DroidKnightsApplication.kt @@ -0,0 +1,7 @@ +package com.droidknights.app2023.tv + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class DroidKnightsApplication : Application() diff --git a/app-tv/src/main/kotlin/com/droidknights/app2023/tv/di/AndroidModule.kt b/app-tv/src/main/kotlin/com/droidknights/app2023/tv/di/AndroidModule.kt new file mode 100644 index 00000000..95d102ef --- /dev/null +++ b/app-tv/src/main/kotlin/com/droidknights/app2023/tv/di/AndroidModule.kt @@ -0,0 +1,23 @@ +package com.droidknights.app2023.tv.di + +import android.app.Application +import android.app.PendingIntent +import android.content.Context +import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object AndroidModule { + @Provides + fun provideContext(app: Application): Context = app + + @Provides + fun toPlayerIntentProvider(): SessionActivityIntentProvider = + object : SessionActivityIntentProvider { + override fun toPlayer(): PendingIntent? = null + } +} \ No newline at end of file diff --git a/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp b/app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..9690d1ed91994d744fcfc0f12e925da46a93e0dd GIT binary patch literal 2172 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 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app-tv/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..d8f4354d29c3f24447e362dc7eb76bca22975d78 GIT binary patch literal 3440 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 Smke?ClXc)QoBn^(D+mCeEx;fE literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..f7e3d57e0a007546ec2fb70971ab1a5fee2ac3a8 GIT binary patch literal 3786 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 AZU6uP literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp b/app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..a9b2e2b5fb18a7dccb61314c229a24d3fb69f8a0 GIT binary patch literal 1448 zcmV;Z1y}k~Nk&GX1pok7MM6+kP&iDJ1pojqFTe{B)rNtzZBqZTzk7&?34o}KKI_7j zBuQ~KVy4}(^#6Z)I2$3qJ>l8H9aG>jJh+Ppw*dfEmXu1MFu=MRjltkInS$=bG^ zk+fBuv~72n+O}fr1D1YlAUOo(_Ek06DEsxcJk$s^CW z!nP}*5sERz1K#sRT>oAmD2hk?caoy*kpZV_Y2RHmQgJexLnMcnBUS1%32984->r)HQptI4sASJk7hYQ|pIPoU-*q*=6Dk!AsPE|_(>(}87y5v3@AapOmO9Odt4 zbc>Jc#x+gTUN=smj(MBS)~8wcRkQnM{xVp1@w+zMI%dp&U)Z%-c}OI`DgOWepZumw zepe=cs8Zfl1rOrB=%!hln=yMKp#)?)`PvX{9yR5sjGdN!+1r`KSi}Y%huCChr2Rp8 z*3-Q0sCj=6!FIT68L!ImI5L{j^%R?c+5S@y>%c@8Cdd3mrY}F;wO2iV1`G#*Z~(^B z@1IS}_w`I<9mOYsp%?=L9Kv7;85$@)of#>t55In%!4Z&vV&PO+< zM`JV!6h^WuV-VAkJ%iliG22akZ;};Ka6DoK527`k$K@0d;h8ke1^@v-cmBIB)G9rE z>(2If=6Z6n3|+#6(uz=X_j{(sLJC1kRS{}T#vcQyiG0oY%D z=D=?0-g|eEXJUnKVt!x(8&fJM(vHb`iZ@_PnV24)ROua8?#+7}+;-H0*&RbZ4nk;v zabri-8_x|C8%J4X&~rrt>ipxE?jS&Id06K)h=AuF5WQYZ<XPdk?{d*&~WI%=qWzE87w&DDr{<~Xt7 zt?sK501yXIy>@rhROp;OFW7#`-g^ssqXJXe>b(E(6nfskK5Cys9rF)N#R-cyltA>9 z@p3p}$Zhr1GhR>!1`^o1j-s#lEOOr9{y*C}dwKTsc`WEt*hUYKL#eu}zcr!dADg|+ zoEOW)$aMZQaqMtm_h$SJ9rTAn)3}tXpU^By zLG5$4-NEJVr5jqrhpKoH0E|!oIeV`u$DgGePdUeLp=>&c%LEa#tb6O9ZF%2z@B{54 z5?FO#mBF;3z2nB*N&Vv0w%J?FQzuNh^P2hFEz_^t=WJUdE8d@dP9%7#dUBXNa Cvfvj0 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app-tv/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..6be0df8d6ea7c809f64e61ea21356d17038b8acb GIT binary patch literal 2522 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 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..97c5b37db9cbb7409fc80baf557af27d54e6536f GIT binary patch literal 3000 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 oPymt`W!XTJ1pZ)DF3>UgdF#*b21(~C1 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 g7A>}|(Z4aq7%hA>i>(kVy# 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 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..39f62427d733cdacd39f040893dad01e1d9adac7 GIT binary patch literal 5532 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�B8^3n-?h;;=Y0(zKdJ03c1r3sYIZnl%6*J2|6Sv)M=-|7O-$vnFCr z_Bh%?GZ@3hN*e=!M?8*(2HeuTT;8_^e`hi~+}Sz;s^l-quNafA0x2L>TSc%hSu=^G zu>=tv0e}}bymME>g_6b?dus^U9cHr}0F_`p-u~IfL^J9E&gRlu)Q{PiJ8vs<11*JHXcHs~tfCYX;{mT(eZ_KdS3QWAQ^plj2d>!SYkw~$O z0B}%qQE-d*?o$F3s*psij$y_eabqh11bCoG+qP^95dgx}#Vl_*$fSZS1&N4~r2-`! z9?O*wrD_@gCQZ<_696s(qn2#~=qdR|`9gGT-a0f3P;@==YM*-3TC~Ay-{uqvdxh&@ z?ryL;5!IAd$cx?c54aYt0x!KDL zFxvXS;?7b-OV1qh>Jb`&zX3=kiT@?Hs?Q!Ze?#+9{_{uc|E&zZ`2gv?3lPpGZ1;Nh z*6DNa+OLj6ojkXw=CS7Y?FtWZFv4g=1;~&9YbT5)#2FGe^PVWmu%&1>Tf805hQsWSfUtqz*yk&j5 z=&U7xRAE&K9Hl(eIJr+)NRNWs&>r-ccKOc&TxQ+KN>#@5V8k1mj=G~- zb)f9vppxw_DG~rn$R8>%GCF%g$fgNE1OVykJ2XZcV@wPv!CzF_&Dpk0R0`^{rJPIP zuxUp@0a{M&Zpypoe?9>VSQv}4Dhdq(u;7|)PFG&K_i?RAAHP~VoYhMuI0FDOYI`yM zyME9&%n>_x;ne~N?HGzqOu#^bDJN8R&tq;|z}^icg@7NJf8Exn*BRaWgem1=+tIa_ zhjY**F~KkMk8XbJmDOAD>HrY3f=W9ACK*Jz%)FBoi_@Dm034tV4wDnaR9y`{n&0%W z7QT=?S%?0~%=mBhosIy!&`>E}J-K&JaipnX`*ix?9lp~MZb~3XP3&OS0RSf!uucT@ zDgl!a6%M<6#4@ob=A5$y33d0lDMfHdgKWCBk*UgcIfk(H;d_1xAh#Av+l9cxMIgDz zR2sla8_7&nUg3=#ZD|WIp(X|bNLEo|x{4QsRIyegS}JkGix%Si;{2e5RKi%|K-F?V z$S|6e<}Svdwh~u}D~mHA6#z}D)sO{R065TWwQ3XYXtva7fFQj&1fX-$yB?p{HjApc zrG%8s%gdX};vC1_x*-5>?4OTU++Lt#-2s0MfHjqx#PP-vj6aOGb&3-WTF*Yjl086n z9}@G2YZuPe4Qlp=XpjP+p%)%x0=}Lh6CVNOsZEff8-SLUG66Dm*bsoC1=|=UqB4F7 zqKPH|G^ZeesZ@=oTnM%l!M^=nbNl_sS=0IN?J6*BJIoY-sSFrL zvS~fBM}L=QkN)i6+lyp7FwksB2DR^44%a3Jhl3V`5ZSu1O+?ZL7!zoxQ6Oaddg`$G z1DcF8TTJVk3Kve228UBPj2nqnm4_s#5R`i01oG&0w9ed*vz}K?zb%bm6)N&iso#OF7}M3>|$Cl zanb=3#i@X!z%+Q4zw^PKE%0wrTK&N)L6K`jr>0S5$;5h_8UKXS!$Gl1eSP~LHO=y22t z#KSq;13)@SgO(QT02xR?sf?>?tM4dNkV1%Z|cQmcrd*xZK0wd=Mpa^`O^TzRC*8unna!^g6 zj)?@Vpt%NVMNkloC1|l}&G^r}d@oSn{R>nT*G*Pu-Bc=^A=4`65aZ;VA eq;cHUXv}8D=#^BeQi5hQo|a1GZ$K5_KVSo7c1D)~ literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..588dd63edc8f647fe878791a3ca0ba18b52abe60 GIT binary patch literal 4388 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 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1dad85bf72f3f83c8212e7e59424e7734f965992 GIT binary patch literal 8966 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 + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app-tv/src/main/res/values/ic_launcher_background.xml b/app-tv/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..cfde9b43 --- /dev/null +++ b/app-tv/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #141414 + \ No newline at end of file diff --git a/app-tv/src/main/res/values/strings.xml b/app-tv/src/main/res/values/strings.xml new file mode 100644 index 00000000..d383fdf6 --- /dev/null +++ b/app-tv/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + 드로이드나이츠 2023 for automotive + \ No newline at end of file diff --git a/app-tv/src/main/res/xml/automotive_app_desc.xml b/app-tv/src/main/res/xml/automotive_app_desc.xml new file mode 100644 index 00000000..0f739ff8 --- /dev/null +++ b/app-tv/src/main/res/xml/automotive_app_desc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1b8fbfca..e074baed 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,6 +18,7 @@ dependencyResolutionManagement { rootProject.name = "DroidKnights2023" include( ":app", + "app-rv", ":core:designsystem", ":core:data", From b6c95ec57d5a94bc05149fdba4709a4bffa5d8ae Mon Sep 17 00:00:00 2001 From: workspace Date: Fri, 1 Sep 2023 16:36:52 +0900 Subject: [PATCH 22/42] package rename tv -> automotive --- {app-tv => app-automotive}/.gitignore | 0 {app-tv => app-automotive}/build.gradle.kts | 4 ++-- {app-tv => app-automotive}/proguard-rules.pro | 0 .../src/main/AndroidManifest.xml | 0 .../app2023/automotive}/DroidKnightsApplication.kt | 2 +- .../app2023/automotive}/di/AndroidModule.kt | 2 +- .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin .../res/mipmap-hdpi/ic_launcher_foreground.webp | Bin .../src/main/res/mipmap-hdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin .../res/mipmap-mdpi/ic_launcher_foreground.webp | Bin .../src/main/res/mipmap-mdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.webp | Bin .../res/mipmap-xhdpi/ic_launcher_foreground.webp | Bin .../main/res/mipmap-xhdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-xxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxhdpi/ic_launcher_foreground.webp | Bin .../main/res/mipmap-xxhdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxxhdpi/ic_launcher_foreground.webp | Bin .../main/res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin .../src/main/res/values/colors.xml | 0 .../src/main/res/values/ic_launcher_background.xml | 0 .../src/main/res/values/strings.xml | 0 .../src/main/res/xml/automotive_app_desc.xml | 0 settings.gradle.kts | 2 +- 28 files changed, 5 insertions(+), 5 deletions(-) rename {app-tv => app-automotive}/.gitignore (100%) rename {app-tv => app-automotive}/build.gradle.kts (80%) rename {app-tv => app-automotive}/proguard-rules.pro (100%) rename {app-tv => app-automotive}/src/main/AndroidManifest.xml (100%) rename {app-tv/src/main/kotlin/com/droidknights/app2023/tv => app-automotive/src/main/kotlin/com/droidknights/app2023/automotive}/DroidKnightsApplication.kt (75%) rename {app-tv/src/main/kotlin/com/droidknights/app2023/tv => app-automotive/src/main/kotlin/com/droidknights/app2023/automotive}/di/AndroidModule.kt (93%) rename {app-tv => app-automotive}/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-hdpi/ic_launcher.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-hdpi/ic_launcher_round.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-mdpi/ic_launcher.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-mdpi/ic_launcher_round.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xhdpi/ic_launcher.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xhdpi/ic_launcher_round.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xxhdpi/ic_launcher.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xxxhdpi/ic_launcher.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp (100%) rename {app-tv => app-automotive}/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp (100%) rename {app-tv => app-automotive}/src/main/res/values/colors.xml (100%) rename {app-tv => app-automotive}/src/main/res/values/ic_launcher_background.xml (100%) rename {app-tv => app-automotive}/src/main/res/values/strings.xml (100%) rename {app-tv => app-automotive}/src/main/res/xml/automotive_app_desc.xml (100%) diff --git a/app-tv/.gitignore b/app-automotive/.gitignore similarity index 100% rename from app-tv/.gitignore rename to app-automotive/.gitignore diff --git a/app-tv/build.gradle.kts b/app-automotive/build.gradle.kts similarity index 80% rename from app-tv/build.gradle.kts rename to app-automotive/build.gradle.kts index 808490d9..ff95aa11 100644 --- a/app-tv/build.gradle.kts +++ b/app-automotive/build.gradle.kts @@ -3,10 +3,10 @@ plugins { } android { - namespace = "com.droidknights.app2023.tv" + namespace = "com.droidknights.app2023.automotive" defaultConfig { - applicationId = "com.droidknights.app2023.tv" + applicationId = "com.droidknights.app2023.automotive" versionCode = 1 versionName = "1.0" } diff --git a/app-tv/proguard-rules.pro b/app-automotive/proguard-rules.pro similarity index 100% rename from app-tv/proguard-rules.pro rename to app-automotive/proguard-rules.pro diff --git a/app-tv/src/main/AndroidManifest.xml b/app-automotive/src/main/AndroidManifest.xml similarity index 100% rename from app-tv/src/main/AndroidManifest.xml rename to app-automotive/src/main/AndroidManifest.xml diff --git a/app-tv/src/main/kotlin/com/droidknights/app2023/tv/DroidKnightsApplication.kt b/app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/DroidKnightsApplication.kt similarity index 75% rename from app-tv/src/main/kotlin/com/droidknights/app2023/tv/DroidKnightsApplication.kt rename to app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/DroidKnightsApplication.kt index 8ac9cf56..c1a588b5 100644 --- a/app-tv/src/main/kotlin/com/droidknights/app2023/tv/DroidKnightsApplication.kt +++ b/app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/DroidKnightsApplication.kt @@ -1,4 +1,4 @@ -package com.droidknights.app2023.tv +package com.droidknights.app2023.automotive import android.app.Application import dagger.hilt.android.HiltAndroidApp diff --git a/app-tv/src/main/kotlin/com/droidknights/app2023/tv/di/AndroidModule.kt b/app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/di/AndroidModule.kt similarity index 93% rename from app-tv/src/main/kotlin/com/droidknights/app2023/tv/di/AndroidModule.kt rename to app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/di/AndroidModule.kt index 95d102ef..24373bab 100644 --- a/app-tv/src/main/kotlin/com/droidknights/app2023/tv/di/AndroidModule.kt +++ b/app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/di/AndroidModule.kt @@ -1,4 +1,4 @@ -package com.droidknights.app2023.tv.di +package com.droidknights.app2023.automotive.di import android.app.Application import android.app.PendingIntent diff --git a/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app-automotive/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to app-automotive/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app-automotive/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to app-automotive/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp b/app-automotive/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp rename to app-automotive/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app-automotive/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp similarity index 100% rename from app-tv/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp rename to app-automotive/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app-automotive/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to app-automotive/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp b/app-automotive/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp rename to app-automotive/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/app-tv/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app-automotive/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp similarity index 100% rename from app-tv/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp rename to app-automotive/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp diff --git a/app-tv/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app-automotive/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from app-tv/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to app-automotive/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app-automotive/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to app-automotive/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/app-tv/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app-automotive/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp rename to app-automotive/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp diff --git a/app-tv/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app-automotive/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to app-automotive/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/app-tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app-automotive/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to app-automotive/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app-automotive/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp rename to app-automotive/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp diff --git a/app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app-automotive/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to app-automotive/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app-automotive/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to app-automotive/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app-automotive/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp rename to app-automotive/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app-automotive/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to app-automotive/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/app-tv/src/main/res/values/colors.xml b/app-automotive/src/main/res/values/colors.xml similarity index 100% rename from app-tv/src/main/res/values/colors.xml rename to app-automotive/src/main/res/values/colors.xml diff --git a/app-tv/src/main/res/values/ic_launcher_background.xml b/app-automotive/src/main/res/values/ic_launcher_background.xml similarity index 100% rename from app-tv/src/main/res/values/ic_launcher_background.xml rename to app-automotive/src/main/res/values/ic_launcher_background.xml diff --git a/app-tv/src/main/res/values/strings.xml b/app-automotive/src/main/res/values/strings.xml similarity index 100% rename from app-tv/src/main/res/values/strings.xml rename to app-automotive/src/main/res/values/strings.xml diff --git a/app-tv/src/main/res/xml/automotive_app_desc.xml b/app-automotive/src/main/res/xml/automotive_app_desc.xml similarity index 100% rename from app-tv/src/main/res/xml/automotive_app_desc.xml rename to app-automotive/src/main/res/xml/automotive_app_desc.xml diff --git a/settings.gradle.kts b/settings.gradle.kts index e074baed..6bc8cc95 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,7 +18,7 @@ dependencyResolutionManagement { rootProject.name = "DroidKnights2023" include( ":app", - "app-rv", + "app-automotive", ":core:designsystem", ":core:data", From 11f16f3f89dc4da520b6eed6fb09196546c52828 Mon Sep 17 00:00:00 2001 From: workspace Date: Fri, 1 Sep 2023 16:46:49 +0900 Subject: [PATCH 23/42] =?UTF-8?q?=EB=88=84=EB=9D=BD=EB=90=9C=20feature:pla?= =?UTF-8?q?yer=20dependency=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 74c0c49c..38067d0c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(projects.core.playback) implementation(projects.feature.main) implementation(projects.feature.home) + implementation(projects.feature.player) implementation(projects.core.designsystem) } From 1ac99235ef2885e72f63099a2ea252625afa9515 Mon Sep 17 00:00:00 2001 From: workspace Date: Sat, 2 Sep 2023 02:08:22 +0900 Subject: [PATCH 24/42] =?UTF-8?q?compile=20sdk=2034=EB=A1=9C=20=EC=83=81?= =?UTF-8?q?=ED=96=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app-automotive/src/main/AndroidManifest.xml | 1 + app/src/main/AndroidManifest.xml | 1 + .../src/main/kotlin/com/droidknights/app2023/KotlinAndroid.kt | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app-automotive/src/main/AndroidManifest.xml b/app-automotive/src/main/AndroidManifest.xml index c9045dc8..7dfb5df3 100644 --- a/app-automotive/src/main/AndroidManifest.xml +++ b/app-automotive/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + + Date: Sat, 2 Sep 2023 02:12:00 +0900 Subject: [PATCH 25/42] =?UTF-8?q?tv=20app=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app-tv, feature:tv-main, feature:tv-session 모듈 추가 - 각 모듈은 기존 모바일 앱 구현을 동일하게 tv용으로 구성. - tv-session feature에서 tv용 compose 라이브러리를 사용하여 tv 맞춤 세션 브라우징 UI를 구현 - 플레이어는 feature:player를 재사용 - app-tv에서 SessionActivityIntentProviderImpl을 구현 후 주입. Now in Playing 카드 클릭 시 플레이어가 열리는 구현을 위함. --- app-tv/.gitignore | 1 + app-tv/build.gradle.kts | 30 ++++ app-tv/proguard-rules.pro | 21 +++ app-tv/src/main/AndroidManifest.xml | 25 +++ .../app2023/tv/DroidKnightsApplication.kt | 7 + .../app2023/tv/di/AndroidModule.kt | 22 +++ .../misc/SessionActivityIntentProviderImpl.kt | 33 ++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 2172 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 3440 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 3786 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1448 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 2522 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2320 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 3000 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 3982 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 5260 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 4598 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 4564 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 6990 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 5532 bytes .../ic_launcher_foreground.webp | Bin 0 -> 4388 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 8966 bytes .../res/values/ic_launcher_background.xml | 4 + app-tv/src/main/res/values/strings.xml | 3 + feature/tv-main/.gitignore | 1 + feature/tv-main/build.gradle.kts | 21 +++ feature/tv-main/src/main/AndroidManifest.xml | 18 ++ .../app2023/feature/tvmain/TvMainActivity.kt | 19 ++ .../app2023/feature/tvmain/TvMainNavigator.kt | 29 +++ .../app2023/feature/tvmain/TvMainScreen.kt | 34 ++++ feature/tv-session/.gitignore | 1 + feature/tv-session/build.gradle.kts | 14 ++ .../tv-session/src/main/AndroidManifest.xml | 4 + .../feature/tvsession/TvSessionCard.kt | 165 ++++++++++++++++++ .../feature/tvsession/TvSessionChip.kt | 34 ++++ .../feature/tvsession/TvSessionScreen.kt | 125 +++++++++++++ .../feature/tvsession/TvSessionUiState.kt | 12 ++ .../feature/tvsession/TvSessionViewModel.kt | 34 ++++ .../feature/tvsession/component/TvCard.kt | 66 +++++++ .../navigation/TvSessionNavigation.kt | 23 +++ .../src/main/res/values/strings.xml | 7 + gradle/libs.versions.toml | 4 + settings.gradle.kts | 5 +- 45 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 app-tv/.gitignore create mode 100644 app-tv/build.gradle.kts create mode 100644 app-tv/proguard-rules.pro create mode 100644 app-tv/src/main/AndroidManifest.xml create mode 100644 app-tv/src/main/java/com/droidknights/app2023/tv/DroidKnightsApplication.kt create mode 100644 app-tv/src/main/java/com/droidknights/app2023/tv/di/AndroidModule.kt create mode 100644 app-tv/src/main/java/com/droidknights/app2023/tv/misc/SessionActivityIntentProviderImpl.kt create mode 100644 app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app-tv/src/main/res/values/ic_launcher_background.xml create mode 100644 app-tv/src/main/res/values/strings.xml create mode 100644 feature/tv-main/.gitignore create mode 100644 feature/tv-main/build.gradle.kts create mode 100644 feature/tv-main/src/main/AndroidManifest.xml create mode 100644 feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainActivity.kt create mode 100644 feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainNavigator.kt create mode 100644 feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainScreen.kt create mode 100644 feature/tv-session/.gitignore create mode 100644 feature/tv-session/build.gradle.kts create mode 100644 feature/tv-session/src/main/AndroidManifest.xml create mode 100644 feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt create mode 100644 feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionChip.kt create mode 100644 feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionScreen.kt create mode 100644 feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionUiState.kt create mode 100644 feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionViewModel.kt create mode 100644 feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/component/TvCard.kt create mode 100644 feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/navigation/TvSessionNavigation.kt create mode 100644 feature/tv-session/src/main/res/values/strings.xml diff --git a/app-tv/.gitignore b/app-tv/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app-tv/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app-tv/build.gradle.kts b/app-tv/build.gradle.kts new file mode 100644 index 00000000..1d985750 --- /dev/null +++ b/app-tv/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("droidknights.android.application") +} + +android { + namespace = "com.droidknights.app2023.tv" + + defaultConfig { + applicationId = "com.droidknights.app2023.tv" + versionCode = 1 + versionName = "1.0" + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + implementation(projects.core.playback) + implementation(projects.feature.player) + implementation(projects.feature.tvMain) +} \ No newline at end of file diff --git a/app-tv/proguard-rules.pro b/app-tv/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app-tv/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app-tv/src/main/AndroidManifest.xml b/app-tv/src/main/AndroidManifest.xml new file mode 100644 index 00000000..06a0565c --- /dev/null +++ b/app-tv/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-tv/src/main/java/com/droidknights/app2023/tv/DroidKnightsApplication.kt b/app-tv/src/main/java/com/droidknights/app2023/tv/DroidKnightsApplication.kt new file mode 100644 index 00000000..8ac9cf56 --- /dev/null +++ b/app-tv/src/main/java/com/droidknights/app2023/tv/DroidKnightsApplication.kt @@ -0,0 +1,7 @@ +package com.droidknights.app2023.tv + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class DroidKnightsApplication : Application() diff --git a/app-tv/src/main/java/com/droidknights/app2023/tv/di/AndroidModule.kt b/app-tv/src/main/java/com/droidknights/app2023/tv/di/AndroidModule.kt new file mode 100644 index 00000000..1863757d --- /dev/null +++ b/app-tv/src/main/java/com/droidknights/app2023/tv/di/AndroidModule.kt @@ -0,0 +1,22 @@ +package com.droidknights.app2023.tv.di + +import android.app.Application +import android.content.Context +import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider +import com.droidknights.app2023.tv.misc.SessionActivityIntentProviderImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object AndroidModule { + @Provides + fun provideContext(app: Application): Context = app + + @Provides + fun toPlayerIntentProvider( + impl: SessionActivityIntentProviderImpl + ): SessionActivityIntentProvider = impl +} \ No newline at end of file diff --git a/app-tv/src/main/java/com/droidknights/app2023/tv/misc/SessionActivityIntentProviderImpl.kt b/app-tv/src/main/java/com/droidknights/app2023/tv/misc/SessionActivityIntentProviderImpl.kt new file mode 100644 index 00000000..004c107d --- /dev/null +++ b/app-tv/src/main/java/com/droidknights/app2023/tv/misc/SessionActivityIntentProviderImpl.kt @@ -0,0 +1,33 @@ +package com.droidknights.app2023.tv.misc + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.TaskStackBuilder +import androidx.core.net.toUri +import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider +import com.droidknights.app2023.feature.player.navigation.PlayerRoute +import com.droidknights.app2023.feature.tvmain.TvMainActivity +import javax.inject.Inject + +class SessionActivityIntentProviderImpl @Inject constructor( + private val context: Context, +) : SessionActivityIntentProvider { + override fun toPlayer(): PendingIntent? { + val deepLinkIntent = Intent( + Intent.ACTION_VIEW, + PlayerRoute.deepLinkUriPattern.toUri(), + context, + TvMainActivity::class.java + ) + + val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(deepLinkIntent) + getPendingIntent( + 0, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + } + return deepLinkPendingIntent + } +} diff --git a/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/app-tv/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp b/app-tv/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..9690d1ed91994d744fcfc0f12e925da46a93e0dd GIT binary patch literal 2172 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 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app-tv/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..d8f4354d29c3f24447e362dc7eb76bca22975d78 GIT binary patch literal 3440 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 Smke?ClXc)QoBn^(D+mCeEx;fE literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app-tv/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..f7e3d57e0a007546ec2fb70971ab1a5fee2ac3a8 GIT binary patch literal 3786 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 AZU6uP literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp b/app-tv/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..a9b2e2b5fb18a7dccb61314c229a24d3fb69f8a0 GIT binary patch literal 1448 zcmV;Z1y}k~Nk&GX1pok7MM6+kP&iDJ1pojqFTe{B)rNtzZBqZTzk7&?34o}KKI_7j zBuQ~KVy4}(^#6Z)I2$3qJ>l8H9aG>jJh+Ppw*dfEmXu1MFu=MRjltkInS$=bG^ zk+fBuv~72n+O}fr1D1YlAUOo(_Ek06DEsxcJk$s^CW z!nP}*5sERz1K#sRT>oAmD2hk?caoy*kpZV_Y2RHmQgJexLnMcnBUS1%32984->r)HQptI4sASJk7hYQ|pIPoU-*q*=6Dk!AsPE|_(>(}87y5v3@AapOmO9Odt4 zbc>Jc#x+gTUN=smj(MBS)~8wcRkQnM{xVp1@w+zMI%dp&U)Z%-c}OI`DgOWepZumw zepe=cs8Zfl1rOrB=%!hln=yMKp#)?)`PvX{9yR5sjGdN!+1r`KSi}Y%huCChr2Rp8 z*3-Q0sCj=6!FIT68L!ImI5L{j^%R?c+5S@y>%c@8Cdd3mrY}F;wO2iV1`G#*Z~(^B z@1IS}_w`I<9mOYsp%?=L9Kv7;85$@)of#>t55In%!4Z&vV&PO+< zM`JV!6h^WuV-VAkJ%iliG22akZ;};Ka6DoK527`k$K@0d;h8ke1^@v-cmBIB)G9rE z>(2If=6Z6n3|+#6(uz=X_j{(sLJC1kRS{}T#vcQyiG0oY%D z=D=?0-g|eEXJUnKVt!x(8&fJM(vHb`iZ@_PnV24)ROua8?#+7}+;-H0*&RbZ4nk;v zabri-8_x|C8%J4X&~rrt>ipxE?jS&Id06K)h=AuF5WQYZ<XPdk?{d*&~WI%=qWzE87w&DDr{<~Xt7 zt?sK501yXIy>@rhROp;OFW7#`-g^ssqXJXe>b(E(6nfskK5Cys9rF)N#R-cyltA>9 z@p3p}$Zhr1GhR>!1`^o1j-s#lEOOr9{y*C}dwKTsc`WEt*hUYKL#eu}zcr!dADg|+ zoEOW)$aMZQaqMtm_h$SJ9rTAn)3}tXpU^By zLG5$4-NEJVr5jqrhpKoH0E|!oIeV`u$DgGePdUeLp=>&c%LEa#tb6O9ZF%2z@B{54 z5?FO#mBF;3z2nB*N&Vv0w%J?FQzuNh^P2hFEz_^t=WJUdE8d@dP9%7#dUBXNa Cvfvj0 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app-tv/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..6be0df8d6ea7c809f64e61ea21356d17038b8acb GIT binary patch literal 2522 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 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app-tv/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..97c5b37db9cbb7409fc80baf557af27d54e6536f GIT binary patch literal 3000 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 oPymt`W!XTJ1pZ)DF3>UgdF#*b21(~C1 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 g7A>}|(Z4aq7%hA>i>(kVy# zq@>Sg7dqRvEpMCali4yeGh5%QGBd2*(+Q{c7j(jj+i+qgNij2pnK?0+!(e96^GLX@ zZBeSn4!UbdXR!U#w~Ow!ySoeYIv*{_6he!(ZPRwNW81cE+qP|6U(T0p+qP}nww;u# zOM)axlA3V6nqKyl*&NNlTXp`=`9J6Xod0wF&-p*6^Z+CS@Ph(iv1)4oGSUwpzzkKH z0Z0x21;A?6)&OKi3V`{lGP45%P}c8ny&^0+_wW|V>MLjW{C+=xn5xYHsOi03e=f1MY@~u8ZvnJ?C36^FBd$ z)6pG$=UYljVnFVO1^{tXB}GbF7hKnyM|so}MJ6P#2@nBvQEdRgGsI0duldE9{GSj3 zv{03lgD?PF09?~Y|Nr_j=fsx!Gmmc}Blr8_9#W${@4ZPEwLf$TiP}le5 z-PM{@SNB*$1ly<%Tw?Ne{8TAh@m~OP0$5Q| zF@NOx>&mNV!l$3wWz}+j`zG)A?{?&E|G}bv1rFmEt5CNQ2m5+Vf+k5`_WcyG5{p^n)EGhSwuS!cx zciTY`FIV_*3+3;Q`0ql1w|uwwx&E;7t+`$-OcS;ofg_G z)M@cUr=<@vje1Z<%HQv!X$2L2${B!}h<%>c&<8BTQeB=YL7DqLJQ#zh0}RMp~E(~2Q( z_h{AQJX>av?fa{@rW6e>^_w8#cXeSR832j_d;$;@iG#BHd*Y!=p z;GD~@Yf(*aLkN17myrq4paOuPIPAboV4evq`pp1tOE+C@PSh!7r^pnE`l{S8RV$yZ z6zeq*mGbhrLgCyjGtQ7Xx_*&=)}{VgIA5$JN{-^SisRJu%WfKLh=ZA~vi6xR<>he^ z(ryD-RAB%;0R#nM0)LqR-2g~8M<|?giHKCyv0g;F>Tt?AGOuWGwo)W=>-vDCwmCN8 znQ0SbLV{CZ00L29BY>bF?88jpoC#dsY5*TB>19>>2XNpuKwhiGW0iWaN;&IQ)cye+ zxF4{zl#s4AVUn6*=`9}5ESnf)2wiU-*AW*c*&Q3&JqVycSs8$!AjlN?4OcbtPRJ`k z`{}He;>KA+P}6nY(#99ocvwSiA{6SF-9+vTKu{2l)TXM%GIa@WP=-Xy)h!$ztSY*m z!k})sKq2T3yx7ezducN zg2CVx@#3smYUB5(#>b~hPzUjhZ+pmo`k7yiK%DlmNP5ad!zvm~Gb{&103!YTGl1Eo+S*#N=6x>6 z*~xKbe=;VEX?1luKZ;zggLQT0bQhP)WzKPs7&&(ASfcieq0jyaYC_t-fBy<`J9bR$ ze2=-@#^rKl#nS5Pes#Id_@?K4=GD}k`Q-?gYuYxEMB@r;X4^z$f+3VCvY$>w1Bhe| zv*;A_^Je6eoE0{S^XzRhh}mEF;gS}K19_Y$>lDuF!Wdm2sw!Mu&JSIP*&iu!o|P~G z5zj4gkhP!QHL0z5s~Au}1~e2HzzFHGq8Vmm<@IJ+(>OP$yLi3kLObcUh}S%!l$3}q z8(5+)B_%7>gNrkl+IYR@WH-*uK~v6g+Z{P_N(5~SlXj2COrpW5Eos?JRjI{QQYi}n z6(su}5Y9ntX3}bCzI2Spvyi%(Bc!vBY`CF2N4uvvfo_IUPS$CM$!S$vGRv$Snf3ec zXUMFdep(GzsJVGuam6%4?3!p;nTDl}F)T&v{NYu% zwx$H{_r!>vPqZnrR_bh-=kxg{IXu!GZk}G29v%6aj@C{QdB@V-EP6k^*ed{003`b1 zhs1T^Uc!c`Ve-_Y8+vl;Z|cf=z!O(rpK*s3!+_?#sH}lOIZ>x@8(0V9z=5^U*1~9B z32lt!wsmV;WTNR;v0(DqM|EMPrFSwY5o6g6P1W zrs$yFcJfqJq0?Gf?=Mj!_P$bcdT~9v(g$80&EJ z2u4^GvNL~A|BA}WH|s)*W(1AWgV-ypVU(NY&u|lT>4sp}N+}roFaVtZd@N5|7d~Aw zykgg``+RXv)rM5&9NSi1U40r+k^t9s?_f(SbOg=&wx3#BL zRh@cf&q?PF_)k5x>$DR)Y~b`0J1A!z+4P7v&g-2yey`u-Dz)Q6TNF@ROUkf`p*G>| zTU3+a6tLnW#P3Z_O#l)A2+9Y*MgVZcy*C==rjCrDO$~flhFD8J>!cBe2!*=j*9BMx z@JUrhc4fD>r`^$(^5b32N%#2TBt6m?@kCt+>ppMHt*t4++gp<8NAN^L#1ozv))U^i zC%tj32U^oMojbI5Z{H`?lK?gyjslnI1{KtF%xS7>m-hZK3T$E-z%o@CF+EiqQgjm; zKH2`0$i=Z>L;%%P)d;%Kb&ci(0!DCB zRa%b003-wOugkUO%IW{t^cB?J-kv7%Zha5xZ4;qTatudl6WO-^9@Wb(3x$$mc@CEV zlm;->m$WXmn32XXF-L=bVO6XwY+S#=Tj@%6z>f6bdDV ziKHW#8;rfjBKzsZhuzSbcWw6~R?q1M#FRBb@9BoRUNcQbARLYf?urYO?2ZqYBt?i2 z@6ITBFsn##OLmd$-e`=tyECih;kGooKs4ZXw+WNf)Win#*abHm@Tf}oB#V1KvE=on zBEjRSC9->|DN57s=*nWEw-~~Xq&n%abX8#uP#Myt1uw2HED~H$RAPlX=&(#t*}YqL zyZh;w#>SB%zi$E^tEvDb1P~OFMU7XD>$kjETl^GK=Ibw*DVEnr9;b;ufxv39(6rP1 zcI4ZdKt2FL5!v5Oe4}W}>qSiyUMsOeX`*z6pXPGKIUvUIsd25C*)3gEQe>Se%y#Kn zks8Hv0mu%Z-PZUmsc-6?|EjCcKDz17))dOqwL#*IP_@&A##*3OwrFFNn$NdXDax6u zvgaTOaz0VCfvAWq4xNIPEm{_>wi=pm1{8-&01^P$;qlBGyL-Ly;*nbO$sMGJJW&rf zMF}2ljG*g=%p|N>X+bJ&<2Y{jnAc01n@=%Cx8@Z^uFNm7UejX6O~y(~$M7iz?AWm* z4rBljl$6+7DdLvS43fltdYf2|jAL$AsCzU=*G=FgfS|M-hXF_mAj7tlRlaYVl@7}4 zE2lK=`ktJdy0U`7Uek4mlc%2C(UNxb*0u~?*E?jq>t0e--LtHs-!m*$KQ~Ku=bk=L zTH0s2?)I*%#>RnV=k;C+kAGEwqX2 zwA|5dl~>b#l|w&*^_S1d4#0Z=&$KA;$u8&tZ(NJ0XR8CaxM%7^H0`#IG|rHluvV)0 zvXLms2jO2^i>Ow)QRgNZ~gQ-z>-=zQl!UD0IVs8Ll1j(!X zurdn_zt6H8vIv0KssKP*w2Xr&05wGZjzj?W5wR^pIx&J3OFu*a-*sK5|J+sp$#q=^ z@Cz}Xej<;mHA4^)z-^rcprlGQ#YIfDi9n|qoR(W0cWHi}Ik5CD)?6~!KND^_eSlub%w^*}~e{ZIeW0jI8>g;A2g~LSBRsv{03veHegL1~4NM8HVQdXbf1L7~L6(Nd4k) z0~j2MbVYW6vj(Gi9En6EE~zMF0J9`p^tXCx&L3{FG?1r5GbNg>)`g_%av4WvL36X=2}hJs8a1k@tNKq`R|Gh#AjB7zM4fnyB)<(!1+ zn+BTU%Tlg zBIe4344GF0ZX-!jqi-1b?-dj#MIsu_ z(3*j4QQcF9tYVJ#ZR2|s%mQS8UbzT3^rCJaSIB33`A6_J7aTF8v5j2PXvwQa{FN!RoJ5r0M`tEz2{ZCm@EJ=-{k zISAWx3f{8qnYMGPvXX)C^&{D~YTLGLd8)1VF(xctYs|HonJEz4w!CAsBD1xvGppOS z$jr=R!sxA)m;kwvY^%02``kyLnY=G%X0kB^*%*mJkS9-(FuCNB#rzn~HqN_;egbf7 z+g7d2{oT7jTVPO?ckm*lGN^)x?7O}LIg%trl1DEdvv_yft%Pg$TB!M-0K)(7M4$}@ z^GilV1eT2bBnZsRFAkAl6?f_L31!^#&@?;^)`C*k?t1XmSUvFFKE_>z_$46$6#%}T zJ}GiqFm}KgqKJ zEh!jVWepGyt$*tSWHnd<_9~0z!)~kB@7NKr3)?(!xkw`{nV_io*$@p65tt z1>x`{v~WZ{4-m&9iLVwr6@b7g3II<1YB3}oh*Z?09SW_2(^Rx@Xgvlh9fkl$7zGF? zN`TQkf?4Ijix?606uw%=>T9; zMw0>TUiW~9Vf?&5_Ml~g)HDumYlpW0!|6Y3n*nHrfKiOL+UC@)J z(vCHVS6B=nvA*U@NHi3L0%mnNWheR9<=t;wOZWIm9N8TF70RVxS?)zpw@a{Jtl}2q40YG{XLTdZ;f~i179;`v$OPVgE zk{6(&y^RY1KoY&Erm^9qNYit40Kjw;B&ZHRlm}GQR|(WZq0fJW(P;qyM97fQpo?F= zlTUsDO$VWVGT(1Ba8X{6HkFS`>ehU{f#>u}jHtRT7ziRnUb1mss65kD2O5wX8Q z!I&|tj-ntU1=ymRaGdwa`K)Q8(sxg|o76_D=)O+5do~@}RG``wag+k^wE0t31iA?P zCJ%1`V{I)Rc85jgAP zQ&`hL72m(?ACMUGac-zgKr(y&_&YwA#41)oV4ptu?5F2PALefX0H_@Hfr*<0H*9S5+M6!UYIkmri z+w9S=p6WMGIJ^nz@&N>Kg#j%+qR=PDITB)k$FKD8)y-V{<~?SzMgh;a0>B006{fB2ZS8lSs2qgFG85x=VWY+|2B* zm2(d_d2<5*X!`%Q{H5*Qf4ucb)zjUXkfDruuqpp|yk5K`H$Gd@I0XX=|9i~(^OI+<_aEnT+ftjX}?4)r`p>f9SSxibW3=14gM+>=wSmdGTI0Zo)7G;C~ zoOK+=z?%O`Hvzye<7o}}z7HPhFEGplZa9v5jQaM1k*sane6^{^W}mlAkD(#3M#Hq{ z!-RGH0UF4?=)Z^*bAK_o~zc=doIp>3KIra^^GY9GaYenVU zzVGOMlKz^eW5BkWv&z{|nsYvQIplSRZqzN*+eSZF5&P;Dzx@)ub=Z_r>ouxgEIXo_ zjKD!zCa_Ipem9j8=C}}B5p3WBXRB(?)#+93{8f1;l~OQb`9{=|YnCj}#6{HSsH*D5 zs-3U66)mQzbE9^tq$cEivWF%`qh+!Zgv=0gX@?Z%q33@n6hK-850nA&!$--dJ|PcV z#m9;Mj?w!cxlhrY=pUn}UsNnm1_b;gEo5C#)hZjMINfOTJbBp3srGJ<1pojHm{L!t z-?+=2oB8TVT|M!g-?l&Xv+|2Srm9|kmU74c#)^$MSJ??94|295w?JlrIy7Mczz`ND z2C$0LO&&Km!zt_rM>xYN?19vPf5k$!i>q1%0H6>fBw)OH#^|>m^512?c;;GqvP)&w zCn`POr6{9X%0O~LOjkhn1T-;%o)!Qoniv35^#}WTr~A-`QSA4Po)?fVcqFPxtM6## zdP_q}m{Yr0kc*q1WPkIE;f1#?aYJeQITq`lNc|v8f+2Ps%I%A)H!53!tUMfBI)x&Ng z8&)#KN@rDybcQbD30rj_20`7FGztJPj<{c}0kX7Iw8GS@i)3L`a%>ogTK8Z1=4Y?C zIpP-e&DGAtXxsu)6JnqaVrT7W(0c^M-`jlN<=Pyx!*N1e?{4>Z?*H-cfBoj0j6Jml zKqbp$OQIfjBa3{mnEXc)TOkx=ZI5o zy6PGk0KhH)!l3A)f4J*!nM4I3Wt2;hxCamhj4?D0RE|69&JMn3ICZIw3ClLa8j$7A(`e=t=XAls-*g6{#*YQY0UB#i&x z$R+zd(4qItmj8Fn&pPbi|G)Q+7cHycjIxR81qMbgd*iT`m^*7Z^IQ4V-e>$&PxlRt z9;_HsKvD=~&d^4SopWCoV|w_U^e<=q?HIp2r~l3Mk6DZL=QxuX$W~(kz{!(KvU%(7 zJvp9SbxUidIqQwzR1GAo5&-$k-rU<_l(9TGNJPxoC6kY~`(cOQrExN6{bkPk)2Q`i zq)-4H`R~(Xr{>ZhS+II76=166ny_1ZWO~a0fWk=?EV9xqW8izhp9kz41i;7Rxc}Gz3=|%;ZY9$4$S-DhrJOBU{3P9F*;O{+KOlWRRtPu>0TSJ}%c(q{pg z+pDE5uus4Pxl7HXV2wKJs4M^w=IL1@lv)64Jk+rIRHKIJd+>rkPuPR{Olj0P3xL(B z^YHkKs@U{p&Pjf*Vq35eF=j-}_<@N_?Mh~(29Wa-%OnB0^{EuR!Cc=54H4qAX}`1n zZAKOd9xvc6e6}0v6%K3x*7z3pL!skRWrj&MJ0czmx z9lCLbbN~P)>MQA@!<8%+q)O1ygDNNA_2a|t{}t;!LV)}-^7G|wsctaiuQA(4dM^)Z zSd7`0-V(I{J!(GY;?8Pqas~h(T0Rz>O#2_#kWSC6Ne3GpAN&57Ui*u$uz1oXSBZE2 z-oujbfl1B(W8irlkOKk$@Z$-M#)GL8f^@xuQNy!P2Tl58ETgv4%h;jC0-0h$V0O0ZJ3pI0m6NRaX7Suo$74Y#3{~jL( z5`Mb({M{G-y*97AYc&t8003w-ci-cI`}CG_B9irWDY27;Px4EW!wfF0o)K2I5v(#* z0Fr1gpG7tAdF7v$0umyCzmP~Z0AMp#EKMh^QCH)2ZCMFIc2d;crIOe|Q2ySR|Ht?v ztRP>o;B9-%_o!hao#8b=xwg z5N&}pl+PVNr5CJ@QUQQs+jKxrJyQskR3-|=U~1f9Dk@qk~pld%)t?w`@Q+9SZMnttQtLD*z>n zL834#6aa*n1ldYfm=)ESln2=U`-|95Y(Rf`1l@}h&ZO)9(;a4e+0zH0OtTsk>Zx|W zBmnSh4{I`tZO^oMa?m3ONS>c@z#wQ32>Q*@CT^DX|EcveA^S&|$7 zd|BlOKsw^pK+I$61waQa9M^y$N&s;HKvGBd?Ee4&&ZfPuQn>;Eh?XDSqfz7K;-8%= zSU|=Uj#-yc7$7`Ks2`TK#QhW&4@}RWUIW&{(y$PcwNtc*+ofH1{IfXqo6f0@!+OR#w_n$lAL@*u5DMjRp@+An56O?)P zG&zc-5h}5zxxdU*NdmBBhYE0k!4e{s1fXWzJe_$IH5Oia@&ZsO@YPMv}0f5cRg-5?d8No-$MTCX%1r)~P1Qzp` zWjI2P(KI8K0{RulYLMZsT$b6twiSS2g@d4g9FmME6PHhf1U&|TP~MJ(f`>?#G}rf( zCx=;OlfJ{59zB>E9r+BL>V@kor^^ZvF}HjGfws)byt`OOrIFGlC4mVhI~sr$S7t z8ziB*)YRVPigj z{!Sp22;K|;Tzf~s1SlPFzlnnjfIqgh9i%2nA}_Kqo&CH)s@#DEUN4KI0?}{K2zPG1*!bQLK;Ak$OX845W+bnW{0 zWADQ{yBjMPLK=E=vW}uMda^z#jt;ofmjxiW<$l>2O1bKI)?+>S%=`(sVOM~Jv zT2`TJ#nP|^007n)xGTH2tKM!~01&!7l;4K#9GjLB064ZH5r)lmf6P_iyt}9JSmV&A zQ2@XS(W)e65tOBuX~K|X?g0SM7}UI76|)2CFgRv~4Rt<$eN(mpz;Z6t+6qi!8U_G> z20#;iT)zA`TOXX)oqvKVrr`{nNoG025RZbG`D&fUpaz1}ibkW9HG-*M!ctSJ1pvz# z11N8zk4t_WyS#JS0N&n3tW?j`U=WE|TvjWJMoC9Pfv^ygx`lC~H@h|fSatcg-+LYL zWv36$2WNimduXOB1wdSqfrSE!pd5sT$n*f66FkH|@3CzGKxkK{0zpX!18jP;;fd39 zQ$YbAtI`NSbW;@45rRNEkfI|LBj7#&iMt#8$p8SFI@2gn0zeyGICabd_|}2gj2O`f zjY1dzAR!cbJ8U9)D=Y*6P=I$AeK-cd(?Wou2gbi0CPfIsm_$NQ4n@<;q6TDdQ3Hk_ zO%9PFh%hPh(K1@iShomGwuA3m_ z4G+g)W)+;9*KLWAXCXzhZm%2XBrP+8Fg^i%yX^Cd{S`%?fm9Ovy}It^TO5>`S*lv% z^_lhcB*mbENv6vB=JGU?zABl)#{j^rD#Ad;-l;P!GZG<*4#ZNa_^>{_?3$DHRF#== zWGRHAC=<(v)6`gOFhT_#L`qGi?wue1>k$d9Rx2eA&O}0xfy&g6k4!F$G!YVX0Le-v zyX(XAzhcjvY)q@F%!dO=2qAQ;?J1w+#4=YW0+MJbmLw{9V107^&+~@4lAczpN_jkh zKp+93srH3kK}^k&Py{n*5N3#^;)C_^`L<=}oY2+P*VjtMX%dJ9Rmbl6jj}(6P1d0h zOAvKpW)ew~2kM6Pp}+2n{c~ll1G;)zRq|;NZi$6bJ+3IW!iOBth0QYZ+eUb2oUT_fF#L1`B{C> zwz^^2TtAmQC$z?N^;K2NjB{qm(twVRjzJAwAtD3US!I6Ai&;4>rj(H!7o8j^B?FOJ z^|BWS>z@2FKF^QxgZkd{C(&i*8P@9R>+5S(m2u+Cibm1V(P#|l2;eFr0*P!4O@oSn zRM)Pts8WKNX9H4SPpj3}SG5v!YG4vVXcUb`(I^^?A_N0h00O|k3_wz<~A0bl@N0E|Ne0C*%3pa1{> literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..39f62427d733cdacd39f040893dad01e1d9adac7 GIT binary patch literal 5532 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�B8^3n-?h;;=Y0(zKdJ03c1r3sYIZnl%6*J2|6Sv)M=-|7O-$vnFCr z_Bh%?GZ@3hN*e=!M?8*(2HeuTT;8_^e`hi~+}Sz;s^l-quNafA0x2L>TSc%hSu=^G zu>=tv0e}}bymME>g_6b?dus^U9cHr}0F_`p-u~IfL^J9E&gRlu)Q{PiJ8vs<11*JHXcHs~tfCYX;{mT(eZ_KdS3QWAQ^plj2d>!SYkw~$O z0B}%qQE-d*?o$F3s*psij$y_eabqh11bCoG+qP^95dgx}#Vl_*$fSZS1&N4~r2-`! z9?O*wrD_@gCQZ<_696s(qn2#~=qdR|`9gGT-a0f3P;@==YM*-3TC~Ay-{uqvdxh&@ z?ryL;5!IAd$cx?c54aYt0x!KDL zFxvXS;?7b-OV1qh>Jb`&zX3=kiT@?Hs?Q!Ze?#+9{_{uc|E&zZ`2gv?3lPpGZ1;Nh z*6DNa+OLj6ojkXw=CS7Y?FtWZFv4g=1;~&9YbT5)#2FGe^PVWmu%&1>Tf805hQsWSfUtqz*yk&j5 z=&U7xRAE&K9Hl(eIJr+)NRNWs&>r-ccKOc&TxQ+KN>#@5V8k1mj=G~- zb)f9vppxw_DG~rn$R8>%GCF%g$fgNE1OVykJ2XZcV@wPv!CzF_&Dpk0R0`^{rJPIP zuxUp@0a{M&Zpypoe?9>VSQv}4Dhdq(u;7|)PFG&K_i?RAAHP~VoYhMuI0FDOYI`yM zyME9&%n>_x;ne~N?HGzqOu#^bDJN8R&tq;|z}^icg@7NJf8Exn*BRaWgem1=+tIa_ zhjY**F~KkMk8XbJmDOAD>HrY3f=W9ACK*Jz%)FBoi_@Dm034tV4wDnaR9y`{n&0%W z7QT=?S%?0~%=mBhosIy!&`>E}J-K&JaipnX`*ix?9lp~MZb~3XP3&OS0RSf!uucT@ zDgl!a6%M<6#4@ob=A5$y33d0lDMfHdgKWCBk*UgcIfk(H;d_1xAh#Av+l9cxMIgDz zR2sla8_7&nUg3=#ZD|WIp(X|bNLEo|x{4QsRIyegS}JkGix%Si;{2e5RKi%|K-F?V z$S|6e<}Svdwh~u}D~mHA6#z}D)sO{R065TWwQ3XYXtva7fFQj&1fX-$yB?p{HjApc zrG%8s%gdX};vC1_x*-5>?4OTU++Lt#-2s0MfHjqx#PP-vj6aOGb&3-WTF*Yjl086n z9}@G2YZuPe4Qlp=XpjP+p%)%x0=}Lh6CVNOsZEff8-SLUG66Dm*bsoC1=|=UqB4F7 zqKPH|G^ZeesZ@=oTnM%l!M^=nbNl_sS=0IN?J6*BJIoY-sSFrL zvS~fBM}L=QkN)i6+lyp7FwksB2DR^44%a3Jhl3V`5ZSu1O+?ZL7!zoxQ6Oaddg`$G z1DcF8TTJVk3Kve228UBPj2nqnm4_s#5R`i01oG&0w9ed*vz}K?zb%bm6)N&iso#OF7}M3>|$Cl zanb=3#i@X!z%+Q4zw^PKE%0wrTK&N)L6K`jr>0S5$;5h_8UKXS!$Gl1eSP~LHO=y22t z#KSq;13)@SgO(QT02xR?sf?>?tM4dNkV1%Z|cQmcrd*xZK0wd=Mpa^`O^TzRC*8unna!^g6 zj)?@Vpt%NVMNkloC1|l}&G^r}d@oSn{R>nT*G*Pu-Bc=^A=4`65aZ;VA eq;cHUXv}8D=#^BeQi5hQo|a1GZ$K5_KVSo7c1D)~ literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..588dd63edc8f647fe878791a3ca0ba18b52abe60 GIT binary patch literal 4388 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 literal 0 HcmV?d00001 diff --git a/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app-tv/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1dad85bf72f3f83c8212e7e59424e7734f965992 GIT binary patch literal 8966 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 + + #141414 + \ No newline at end of file diff --git a/app-tv/src/main/res/values/strings.xml b/app-tv/src/main/res/values/strings.xml new file mode 100644 index 00000000..e54c648b --- /dev/null +++ b/app-tv/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + 드로이드나이츠 2023 for tv + \ No newline at end of file diff --git a/feature/tv-main/.gitignore b/feature/tv-main/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/tv-main/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/tv-main/build.gradle.kts b/feature/tv-main/build.gradle.kts new file mode 100644 index 00000000..06a2d476 --- /dev/null +++ b/feature/tv-main/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("droidknights.android.feature") +} + +android { + namespace = "com.droidknights.app2023.feature.tvmain" +} + +dependencies { + implementation(projects.feature.tvSession) + implementation(projects.feature.player) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.androidx.lifecycle.viewModelCompose) + + implementation(libs.androidx.tv.foundation) + implementation(libs.androidx.tv.material) +} diff --git a/feature/tv-main/src/main/AndroidManifest.xml b/feature/tv-main/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e5571f43 --- /dev/null +++ b/feature/tv-main/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainActivity.kt b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainActivity.kt new file mode 100644 index 00000000..9600ce64 --- /dev/null +++ b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainActivity.kt @@ -0,0 +1,19 @@ +package com.droidknights.app2023.feature.tvmain + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class TvMainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + KnightsTheme(darkTheme = true) { + TvMainScreen() + } + } + } +} diff --git a/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainNavigator.kt b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainNavigator.kt new file mode 100644 index 00000000..624fafd6 --- /dev/null +++ b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainNavigator.kt @@ -0,0 +1,29 @@ +package com.droidknights.app2023.feature.tvmain + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.droidknights.app2023.feature.player.navigation.navigatePlayer +import com.droidknights.app2023.feature.tvsession.navigation.TvSessionRoute + +internal class MainNavigator( + val navController: NavHostController, +) { + val startDestination = TvSessionRoute.route + + fun navigatePlayer(sessionId: String) { + navController.navigatePlayer(sessionId) + } + + fun popBackStack() { + navController.popBackStack() + } +} + +@Composable +internal fun rememberMainNavigator( + navController: NavHostController = rememberNavController(), +): MainNavigator = remember(navController) { + MainNavigator(navController) +} diff --git a/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainScreen.kt b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainScreen.kt new file mode 100644 index 00000000..d82ff354 --- /dev/null +++ b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainScreen.kt @@ -0,0 +1,34 @@ +package com.droidknights.app2023.feature.tvmain + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.NonInteractiveSurfaceDefaults +import androidx.tv.material3.Surface +import com.droidknights.app2023.feature.player.navigation.playerNavGraph +import com.droidknights.app2023.feature.tvsession.navigation.tvSessionNavGraph + +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +internal fun TvMainScreen( + navigator: MainNavigator = rememberMainNavigator() +) { + Surface( + colors = NonInteractiveSurfaceDefaults.colors( + containerColor = MaterialTheme.colorScheme.background + ) + ) { + NavHost( + navController = navigator.navController, + startDestination = navigator.startDestination, + ) { + tvSessionNavGraph( + onSessionClick = { navigator.navigatePlayer(it.id) }, + ) + playerNavGraph( + onBackClick = { navigator.popBackStack() }, + ) + } + } +} diff --git a/feature/tv-session/.gitignore b/feature/tv-session/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/tv-session/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/tv-session/build.gradle.kts b/feature/tv-session/build.gradle.kts new file mode 100644 index 00000000..0001d6ef --- /dev/null +++ b/feature/tv-session/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("droidknights.android.feature") +} + +android { + namespace = "com.droidknights.app2023.feature.tvsession" +} + +dependencies { + implementation(libs.kotlinx.immutable) + + implementation(libs.androidx.tv.foundation) + implementation(libs.androidx.tv.material) +} diff --git a/feature/tv-session/src/main/AndroidManifest.xml b/feature/tv-session/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/tv-session/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt new file mode 100644 index 00000000..52f02891 --- /dev/null +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt @@ -0,0 +1,165 @@ +package com.droidknights.app2023.feature.tvsession + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.droidknights.app2023.core.designsystem.component.NetworkImage +import com.droidknights.app2023.core.designsystem.component.TextChip +import com.droidknights.app2023.core.designsystem.theme.DarkGray +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import com.droidknights.app2023.core.designsystem.theme.LightGray +import com.droidknights.app2023.core.model.Level +import com.droidknights.app2023.core.model.Room +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.core.model.Speaker +import com.droidknights.app2023.core.model.Tag +import com.droidknights.app2023.core.model.Video +import com.droidknights.app2023.feature.tvsession.component.KnightsCard +import kotlinx.datetime.LocalDateTime + +@Composable +internal fun SessionCard( + session: Session, + modifier: Modifier = Modifier, + onSessionClick: (Session) -> Unit = { }, +) { + if (session.video.isReady) { + KnightsCard( + modifier = modifier, + onClick = { onSessionClick(session) } + ) { + SessionCardContent(session = session) + } + } else { + KnightsCard( + modifier = modifier + ) { + SessionCardContent(session = session) + } + } +} + +@Composable +private fun SessionCardContent( + session: Session, +) { + Column( + modifier = Modifier.padding(CardContentPadding) + ) { + // 카테고리 + Row(verticalAlignment = Alignment.CenterVertically) { + CategoryChip() + session.tags.forEach { tag -> + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = tag.name, + style = KnightsTheme.typography.labelLargeM, + color = DarkGray, + ) + } + } + + // 제목 + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = session.title, + style = KnightsTheme.typography.titleLargeB, + color = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.padding(end = 50.dp) + ) + + // 트랙 + Spacer(modifier = Modifier.height(12.dp)) + Row { + TrackChip(room = session.room) + Spacer(modifier = Modifier.width(8.dp)) + TimeChip(dateTime = session.startTime) + } + + // 발표자 + Spacer(modifier = Modifier.height(12.dp)) + Box(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.align(Alignment.BottomStart)) { + session.speakers.forEach { speaker -> + Text( + text = speaker.name, + style = KnightsTheme.typography.titleLargeB, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } + } + Row( + modifier = Modifier.align(Alignment.BottomEnd) + ) { + session.speakers.forEach { speaker -> + NetworkImage( + imageUrl = speaker.imageUrl, + modifier = Modifier + .size(80.dp) + .clip(CircleShape), + placeholder = painterResource(id = com.droidknights.app2023.core.ui.R.drawable.placeholder_speaker), + ) + } + } + } + } +} + +@Composable +private fun CategoryChip() { + TextChip( + text = stringResource(id = R.string.session_category), + containerColor = DarkGray, + labelColor = LightGray, + ) +} + +private val CardContentPadding = + PaddingValues(start = 24.dp, top = 16.dp, end = 24.dp, bottom = 24.dp) + +@Preview +@Composable +private fun SessionCardPreview() { + val fakeSession = Session( + id = "1", + title = "Jetpack Compose에 있는 것, 없는 것", + content = "", + speakers = listOf( + Speaker( + name = "안성용", + introduction = "안드로이드 개발자", + imageUrl = "https://picsum.photos/200", + ), + ), + level = Level.BASIC, + tags = listOf( + Tag("효율적인 코드베이스") + ), + startTime = LocalDateTime(2023, 9, 12, 16, 10, 0), + endTime = LocalDateTime(2023, 9, 12, 16, 45, 0), + room = Room.TRACK1, + video = Video.None + ) + + KnightsTheme { + SessionCard(fakeSession) + } +} diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionChip.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionChip.kt new file mode 100644 index 00000000..6a2da0ad --- /dev/null +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionChip.kt @@ -0,0 +1,34 @@ +package com.droidknights.app2023.feature.tvsession + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import com.droidknights.app2023.core.designsystem.component.TextChip +import com.droidknights.app2023.core.model.Room +import com.droidknights.app2023.core.ui.textRes +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toJavaLocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +internal fun TrackChip(room: Room) { + TextChip( + text = stringResource(id = room.textRes), + containerColor = MaterialTheme.colorScheme.secondaryContainer, + labelColor = MaterialTheme.colorScheme.onSecondaryContainer, + ) +} + +@Composable +internal fun TimeChip(dateTime: LocalDateTime) { + val pattern = stringResource(id = R.string.session_time_fmt) + val formatter = remember { DateTimeFormatter.ofPattern(pattern) } + val time = remember { dateTime.toJavaLocalDateTime().toLocalTime() } + + TextChip( + text = formatter.format(time), + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + labelColor = MaterialTheme.colorScheme.onTertiaryContainer, + ) +} diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionScreen.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionScreen.kt new file mode 100644 index 00000000..9c962dbb --- /dev/null +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionScreen.kt @@ -0,0 +1,125 @@ +package com.droidknights.app2023.feature.tvsession + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.tv.foundation.lazy.list.TvLazyColumn +import androidx.tv.foundation.lazy.list.TvLazyListScope +import androidx.tv.foundation.lazy.list.TvLazyRow +import androidx.tv.foundation.lazy.list.items +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import com.droidknights.app2023.core.model.Room +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.core.ui.RoomText +import kotlinx.collections.immutable.PersistentList + +@Composable +internal fun TvSessionScreen( + onSessionClick: (Session) -> Unit, + tvSessionViewModel: TvSessionViewModel = hiltViewModel(), +) { + val tvSessionUiState by tvSessionViewModel.uiState.collectAsStateWithLifecycle() + + TvSessionContent( + tvSessionUiState = tvSessionUiState, + modifier = Modifier.fillMaxSize(), + onSessionClick = onSessionClick, + ) +} + +@Composable +private fun TvSessionContent( + tvSessionUiState: TvSessionUiState, + onSessionClick: (Session) -> Unit, + modifier: Modifier = Modifier, +) { + when (tvSessionUiState) { + TvSessionUiState.Loading -> Box(modifier, contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + is TvSessionUiState.Sessions -> TvLazyColumn( + modifier = modifier, + contentPadding = PaddingValues(32.dp), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + sessionItems( + items = tvSessionUiState.sessions, + onSessionClick = onSessionClick, + ) + } + } +} + +private fun TvLazyListScope.sessionItems( + items: PersistentList, + onSessionClick: (Session) -> Unit, +) { + items.groupBy { it.room }.entries.forEach { (room, sessions) -> + item { + Column(verticalArrangement = Arrangement.spacedBy(32.dp)) { + RoomTitle( + modifier = Modifier.fillMaxWidth(), + room = room + ) + TvLazyRow( + modifier = Modifier + // https://github.com/android/nowinandroid/blob/main/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt#L179-L191 + .layout { measurable, constraints -> + val placeable = measurable.measure( + constraints.copy( + maxWidth = constraints.maxWidth + 64.dp.roundToPx(), + ), + ) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 32.dp), + horizontalArrangement = Arrangement.spacedBy(32.dp) + ) { + items(sessions) { session -> + SessionCard( + modifier = Modifier.width(480.dp), + session = session, + onSessionClick = onSessionClick + ) + } + } + } + } + } +} + +@Composable +private fun RoomTitle( + modifier: Modifier = Modifier, + room: Room, +) { + Column(modifier = modifier) { + RoomText( + room = room, + style = KnightsTheme.typography.titleLargeB, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Spacer(modifier = Modifier.height(8.dp)) + Divider(thickness = 2.dp, color = MaterialTheme.colorScheme.onPrimaryContainer) + } +} diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionUiState.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionUiState.kt new file mode 100644 index 00000000..6a1c5ab9 --- /dev/null +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionUiState.kt @@ -0,0 +1,12 @@ +package com.droidknights.app2023.feature.tvsession + +import com.droidknights.app2023.core.model.Session +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +sealed interface TvSessionUiState { + object Loading : TvSessionUiState + data class Sessions( + val sessions: PersistentList = persistentListOf(), + ) : TvSessionUiState +} diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionViewModel.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionViewModel.kt new file mode 100644 index 00000000..b39c9d4b --- /dev/null +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionViewModel.kt @@ -0,0 +1,34 @@ +package com.droidknights.app2023.feature.tvsession + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.droidknights.app2023.core.domain.usecase.GetSessionsUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class TvSessionViewModel @Inject constructor( + private val getSessionsUseCase: GetSessionsUseCase, +) : ViewModel() { + + private val _errorFlow = MutableSharedFlow() + val errorFlow: SharedFlow get() = _errorFlow + + val uiState: StateFlow = flow { emit(getSessionsUseCase().toPersistentList()) } + .map(TvSessionUiState::Sessions) + .catch { throwable -> _errorFlow.emit(throwable) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TvSessionUiState.Loading + ) +} diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/component/TvCard.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/component/TvCard.kt new file mode 100644 index 00000000..e823e240 --- /dev/null +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/component/TvCard.kt @@ -0,0 +1,66 @@ +package com.droidknights.app2023.feature.tvsession.component + +import android.content.res.Configuration +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.ClickableSurfaceDefaults +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.NonInteractiveSurfaceDefaults +import androidx.tv.material3.Surface +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme + +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun KnightsCard( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.surface, + content: @Composable BoxScope.() -> Unit, +) { + Surface( + modifier = modifier.fillMaxWidth(), + colors = NonInteractiveSurfaceDefaults.colors(containerColor = color), + shape = RoundedCornerShape(32.dp), + tonalElevation = 2.dp, + content = content, + ) +} + +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun KnightsCard( + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: () -> Unit = {}, + color: Color = MaterialTheme.colorScheme.surface, + content: @Composable BoxScope.() -> Unit, +) { + Surface( + onClick = onClick, + enabled = enabled, + modifier = modifier.fillMaxWidth(), + colors = ClickableSurfaceDefaults.colors( + containerColor = color, + focusedContainerColor = MaterialTheme.colorScheme.inversePrimary + ), + shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(32.dp)), + tonalElevation = 2.dp, + content = content, + ) +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun KnightsCardPreview() { + KnightsTheme { + KnightsCard(modifier = Modifier.size(320.dp, 160.dp), content = { }) + } +} diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/navigation/TvSessionNavigation.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/navigation/TvSessionNavigation.kt new file mode 100644 index 00000000..5acd4a16 --- /dev/null +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/navigation/TvSessionNavigation.kt @@ -0,0 +1,23 @@ +package com.droidknights.app2023.feature.tvsession.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.feature.tvsession.TvSessionScreen + +fun NavController.navigateTvSession() { + navigate(TvSessionRoute.route) +} + +fun NavGraphBuilder.tvSessionNavGraph( + onSessionClick: (Session) -> Unit, +) { + composable(TvSessionRoute.route) { + TvSessionScreen(onSessionClick = onSessionClick) + } +} + +object TvSessionRoute { + const val route: String = "tv-session" +} diff --git a/feature/tv-session/src/main/res/values/strings.xml b/feature/tv-session/src/main/res/values/strings.xml new file mode 100644 index 00000000..106c8f37 --- /dev/null +++ b/feature/tv-session/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + 세션 목록 + + 카테고리 + HH:mm 발표 + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 011abe9f..9674b788 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,6 +41,8 @@ androidxDatastore = "1.0.0" ossLicenses = "17.0.1" ossLicensesPlugin = "0.10.6" +tv-foundation = "1.0.0-alpha08" +tv-material = "1.0.0-alpha08" [libraries] android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } @@ -63,6 +65,8 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" } androidx-compose-navigation-test = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxComposeNavigation" } +androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tv-foundation" } +androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tv-material" } androidx-media3-player = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "androidxMedia3" } androidx-media3-player-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "androidxMedia3" } androidx-media3-player-session = { group = "androidx.media3", name = "media3-session", version.ref = "androidxMedia3" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 6bc8cc95..3dfdde0a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,7 +18,8 @@ dependencyResolutionManagement { rootProject.name = "DroidKnights2023" include( ":app", - "app-automotive", + ":app-automotive", + ":app-tv", ":core:designsystem", ":core:data", @@ -37,4 +38,6 @@ include( ":feature:contributor", ":feature:bookmark", ":feature:player", + ":feature:tv-main", + ":feature:tv-session" ) From 6cd0f3a3b6bb317b2b04bc968f0c59a91cd9882c Mon Sep 17 00:00:00 2001 From: workspace Date: Sat, 2 Sep 2023 05:28:26 +0900 Subject: [PATCH 26/42] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agp, kotlin, compose를 포함하여 대부분의 라이브러리를 최신화 - gradle 8.3 --- .../com/droidknights/app2023/Extension.kt | 6 +-- .../com/droidknights/app2023/KotlinAndroid.kt | 2 +- feature/tv-main/build.gradle.kts | 4 +- feature/tv-session/build.gradle.kts | 4 +- gradle/libs.versions.toml | 37 +++++++++++-------- gradle/wrapper/gradle-wrapper.properties | 2 +- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/build-logic/src/main/kotlin/com/droidknights/app2023/Extension.kt b/build-logic/src/main/kotlin/com/droidknights/app2023/Extension.kt index c2883b3f..d4f50025 100644 --- a/build-logic/src/main/kotlin/com/droidknights/app2023/Extension.kt +++ b/build-logic/src/main/kotlin/com/droidknights/app2023/Extension.kt @@ -9,13 +9,13 @@ import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.plugins.ExtensionContainer import org.gradle.kotlin.dsl.getByType -internal val Project.applicationExtension: CommonExtension<*, *, *, *> +internal val Project.applicationExtension: CommonExtension<*, *, *, *, *> get() = extensions.getByType() -internal val Project.libraryExtension: CommonExtension<*, *, *, *> +internal val Project.libraryExtension: CommonExtension<*, *, *, *, *> get() = extensions.getByType() -internal val Project.androidExtension: CommonExtension<*, *, *, *> +internal val Project.androidExtension: CommonExtension<*, *, *, *, *> get() = runCatching { libraryExtension } .recoverCatching { applicationExtension } .onFailure { println("Could not find Library or Application extension from this project") } diff --git a/build-logic/src/main/kotlin/com/droidknights/app2023/KotlinAndroid.kt b/build-logic/src/main/kotlin/com/droidknights/app2023/KotlinAndroid.kt index c95369e9..d8ee541b 100644 --- a/build-logic/src/main/kotlin/com/droidknights/app2023/KotlinAndroid.kt +++ b/build-logic/src/main/kotlin/com/droidknights/app2023/KotlinAndroid.kt @@ -21,7 +21,7 @@ internal fun Project.configureKotlinAndroid() { compileSdk = 34 defaultConfig { - minSdk = 24 + minSdk = 26 } compileOptions { diff --git a/feature/tv-main/build.gradle.kts b/feature/tv-main/build.gradle.kts index 06a2d476..431c2182 100644 --- a/feature/tv-main/build.gradle.kts +++ b/feature/tv-main/build.gradle.kts @@ -16,6 +16,6 @@ dependencies { implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.viewModelCompose) - implementation(libs.androidx.tv.foundation) - implementation(libs.androidx.tv.material) + implementation(libs.androidx.compose.tv.foundation) + implementation(libs.androidx.compose.tv.material) } diff --git a/feature/tv-session/build.gradle.kts b/feature/tv-session/build.gradle.kts index 0001d6ef..a98a7e8e 100644 --- a/feature/tv-session/build.gradle.kts +++ b/feature/tv-session/build.gradle.kts @@ -9,6 +9,6 @@ android { dependencies { implementation(libs.kotlinx.immutable) - implementation(libs.androidx.tv.foundation) - implementation(libs.androidx.tv.material) + implementation(libs.androidx.compose.tv.foundation) + implementation(libs.androidx.compose.tv.material) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9674b788..8696676d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,26 @@ [versions] -androidGradlePlugin = "8.0.2" -androidDesugarJdkLibs = "1.2.2" -androidxCore = "1.9.0" +androidGradlePlugin = "8.1.1" +androidDesugarJdkLibs = "2.0.3" +androidxCore = "1.10.1" +androidxCoreSplashscreen = "1.0.1" androidxAppCompat = "1.6.1" androidxLifecycle = "2.6.1" -androidxComposeBom = "2023.05.01" -androidxComposeCompiler = "1.4.7" -androidxComposeNavigation = "2.6.0" -androidxComposeMaterial3 = "1.1.0" +androidxComposeBom = "2023.08.00" +androidxComposeCompiler = "1.5.3" +androidxComposeNavigation = "2.7.1" +androidxComposeMaterial3 = "1.1.1" +androidxComposeTv = "1.0.0-alpha08" +androidXComposeWear = "1.2.0" androidxActivity = "1.7.2" androidxMedia3 = "1.1.1" -hilt = "2.46.1" +hilt = "2.47" hiltNavigationCompose = "1.0.0" okhttp = "4.11.0" retrofit = "2.9.0" retrofitKotlinxSerializationJson = "1.0.0" kotlinxSerializationJson = "1.5.1" -kotlinxDatetime = "0.2.1" +kotlinxDatetime = "0.4.0" kotlinxImmutable = "0.3.5" landscapist = "2.2.5" @@ -25,7 +28,7 @@ composeShimmer = "1.0.5" junit4 = "4.13.2" junitVintageEngine = "5.10.0" -kotlin = "1.8.21" +kotlin = "1.9.10" androidxTestExt = "1.1.4" androidxEspresso = "3.5.0" @@ -35,14 +38,13 @@ detekt = "1.23.0" mockk = "1.13.5" turbine = "1.0.0" -coroutine = "1.7.2" +coroutine = "1.7.3" androidxDatastore = "1.0.0" ossLicenses = "17.0.1" ossLicensesPlugin = "0.10.6" -tv-foundation = "1.0.0-alpha08" -tv-material = "1.0.0-alpha08" +play-services-wearable = "18.0.0" [libraries] android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } @@ -50,6 +52,7 @@ android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } @@ -65,8 +68,12 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" } androidx-compose-navigation-test = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxComposeNavigation" } -androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tv-foundation" } -androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tv-material" } +androidx-compose-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "androidxComposeTv" } +androidx-compose-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "androidxComposeTv" } +play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "play-services-wearable" } +androidx-compose-wear-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "androidXComposeWear" } +androidx-compose-wear-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "androidXComposeWear" } + androidx-media3-player = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "androidxMedia3" } androidx-media3-player-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "androidxMedia3" } androidx-media3-player-session = { group = "androidx.media3", name = "media3-session", version.ref = "androidxMedia3" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c895404e..bbf3ca44 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Jun 04 12:42:51 KST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From c55d90028915abc258a3461fe3d4196536a34992 Mon Sep 17 00:00:00 2001 From: workspace Date: Sat, 2 Sep 2023 18:35:51 +0900 Subject: [PATCH 27/42] =?UTF-8?q?wear=20os=20app=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app-wear-os, feature:wear-main, feature:wear-player, feature:wear-session 모듈 추가 - 각 모듈은 기존 모바일 앱 구현을 동일하게 wear용으로 구성. - wear-main에서는 wear용 compose-navigation을 이용(SwipeDismissableNavHost). 이 때, NavHost에 destination 추가 시, wear용 라이브러리를 사용 해야한다. (함수 이름도 같고, 크래시도 나지 않음) - wear-session feature에서 wear용 compose 라이브러리를 사용하여 wear 맞춤 세션 브라우징 UI를 구현(ScalingLazyColumn, onRotaryScrollEvent) - 플레이어는 wear os에 맞춰 재구현. video 없이 only audio 재생을 하도록 했다. - app-wear-os에서 SessionActivityIntentProviderImpl을 구현 후 주입. 재생 인디케이터 클릭 시 플레이어가 열리도록 함. - wear os player 구현에 필요하여 현재 media item의 title, artist, artworkUri를 PlaybackState에 추가 --- app-wear-os/.gitignore | 1 + app-wear-os/build.gradle.kts | 31 +++ app-wear-os/proguard-rules.pro | 21 ++ app-wear-os/src/main/AndroidManifest.xml | 27 +++ .../app2023/wear/DroidKnightsApplication.kt | 7 + .../app2023/wear/di/AndroidModule.kt | 22 ++ .../misc/SessionActivityIntentProviderImpl.kt | 33 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 2172 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 3440 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 3786 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1448 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 2522 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2320 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 3000 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 3982 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 5260 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 4598 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 4564 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 6990 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 5532 bytes .../ic_launcher_foreground.webp | Bin 0 -> 4388 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 8966 bytes .../res/values/ic_launcher_background.xml | 4 + app-wear-os/src/main/res/values/strings.xml | 3 + .../core/playback/playstate/PlaybackState.kt | 6 +- .../playstate/PlaybackStateListener.kt | 5 +- feature/wear-main/.gitignore | 1 + feature/wear-main/build.gradle.kts | 22 ++ .../wear-main/src/main/AndroidManifest.xml | 18 ++ .../feature/wearmain/WearMainActivity.kt | 19 ++ .../feature/wearmain/WearMainNavigator.kt | 25 +++ .../feature/wearmain/WearMainScreen.kt | 26 +++ feature/wear-player/.gitignore | 1 + feature/wear-player/build.gradle.kts | 14 ++ .../wear-player/src/main/AndroidManifest.xml | 3 + .../feature/wearplayer/WearPlayerScreen.kt | 201 ++++++++++++++++++ .../feature/wearplayer/WearPlayerUiState.kt | 21 ++ .../feature/wearplayer/WearPlayerViewModel.kt | 67 ++++++ .../navigation/WearPlayerNavigation.kt | 37 ++++ feature/wear-session/.gitignore | 1 + feature/wear-session/build.gradle.kts | 15 ++ .../wear-session/src/main/AndroidManifest.xml | 4 + .../feature/wearsession/WearSessionCard.kt | 135 ++++++++++++ .../feature/wearsession/WearSessionScreen.kt | 133 ++++++++++++ .../feature/wearsession/WearSessionUiState.kt | 12 ++ .../wearsession/WearSessionViewModel.kt | 34 +++ .../navigation/WearSessionNavigation.kt | 23 ++ .../src/main/res/values/strings.xml | 7 + gradle/libs.versions.toml | 1 + settings.gradle.kts | 6 +- 52 files changed, 993 insertions(+), 3 deletions(-) create mode 100644 app-wear-os/.gitignore create mode 100644 app-wear-os/build.gradle.kts create mode 100644 app-wear-os/proguard-rules.pro create mode 100644 app-wear-os/src/main/AndroidManifest.xml create mode 100644 app-wear-os/src/main/java/com/droidknights/app2023/wear/DroidKnightsApplication.kt create mode 100644 app-wear-os/src/main/java/com/droidknights/app2023/wear/di/AndroidModule.kt create mode 100644 app-wear-os/src/main/java/com/droidknights/app2023/wear/misc/SessionActivityIntentProviderImpl.kt create mode 100644 app-wear-os/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app-wear-os/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app-wear-os/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app-wear-os/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app-wear-os/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app-wear-os/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app-wear-os/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app-wear-os/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app-wear-os/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app-wear-os/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app-wear-os/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app-wear-os/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app-wear-os/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app-wear-os/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app-wear-os/src/main/res/values/ic_launcher_background.xml create mode 100644 app-wear-os/src/main/res/values/strings.xml create mode 100644 feature/wear-main/.gitignore create mode 100644 feature/wear-main/build.gradle.kts create mode 100644 feature/wear-main/src/main/AndroidManifest.xml create mode 100644 feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainActivity.kt create mode 100644 feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainNavigator.kt create mode 100644 feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainScreen.kt create mode 100644 feature/wear-player/.gitignore create mode 100644 feature/wear-player/build.gradle.kts create mode 100644 feature/wear-player/src/main/AndroidManifest.xml create mode 100644 feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerScreen.kt create mode 100644 feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerUiState.kt create mode 100644 feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerViewModel.kt create mode 100644 feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/navigation/WearPlayerNavigation.kt create mode 100644 feature/wear-session/.gitignore create mode 100644 feature/wear-session/build.gradle.kts create mode 100644 feature/wear-session/src/main/AndroidManifest.xml create mode 100644 feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt create mode 100644 feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionScreen.kt create mode 100644 feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionUiState.kt create mode 100644 feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionViewModel.kt create mode 100644 feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/navigation/WearSessionNavigation.kt create mode 100644 feature/wear-session/src/main/res/values/strings.xml diff --git a/app-wear-os/.gitignore b/app-wear-os/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app-wear-os/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app-wear-os/build.gradle.kts b/app-wear-os/build.gradle.kts new file mode 100644 index 00000000..fbc0d3cd --- /dev/null +++ b/app-wear-os/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("droidknights.android.application") +} + +android { + namespace = "com.droidknights.app2023.wear" + + defaultConfig { + applicationId = "com.droidknights.app2023.wear" + versionCode = 1 + versionName = "1.0" + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + implementation(projects.core.navigation) + implementation(projects.core.playback) + implementation(projects.feature.wearMain) + implementation(projects.feature.wearPlayer) +} \ No newline at end of file diff --git a/app-wear-os/proguard-rules.pro b/app-wear-os/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app-wear-os/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app-wear-os/src/main/AndroidManifest.xml b/app-wear-os/src/main/AndroidManifest.xml new file mode 100644 index 00000000..2f191b1d --- /dev/null +++ b/app-wear-os/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-wear-os/src/main/java/com/droidknights/app2023/wear/DroidKnightsApplication.kt b/app-wear-os/src/main/java/com/droidknights/app2023/wear/DroidKnightsApplication.kt new file mode 100644 index 00000000..60b78d6a --- /dev/null +++ b/app-wear-os/src/main/java/com/droidknights/app2023/wear/DroidKnightsApplication.kt @@ -0,0 +1,7 @@ +package com.droidknights.app2023.wear + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class DroidKnightsApplication : Application() diff --git a/app-wear-os/src/main/java/com/droidknights/app2023/wear/di/AndroidModule.kt b/app-wear-os/src/main/java/com/droidknights/app2023/wear/di/AndroidModule.kt new file mode 100644 index 00000000..a2537e4b --- /dev/null +++ b/app-wear-os/src/main/java/com/droidknights/app2023/wear/di/AndroidModule.kt @@ -0,0 +1,22 @@ +package com.droidknights.app2023.wear.di + +import android.app.Application +import android.content.Context +import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider +import com.droidknights.app2023.wear.misc.SessionActivityIntentProviderImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object AndroidModule { + @Provides + fun provideContext(app: Application): Context = app + + @Provides + fun toPlayerIntentProvider( + impl: SessionActivityIntentProviderImpl + ): SessionActivityIntentProvider = impl +} \ No newline at end of file diff --git a/app-wear-os/src/main/java/com/droidknights/app2023/wear/misc/SessionActivityIntentProviderImpl.kt b/app-wear-os/src/main/java/com/droidknights/app2023/wear/misc/SessionActivityIntentProviderImpl.kt new file mode 100644 index 00000000..db727702 --- /dev/null +++ b/app-wear-os/src/main/java/com/droidknights/app2023/wear/misc/SessionActivityIntentProviderImpl.kt @@ -0,0 +1,33 @@ +package com.droidknights.app2023.wear.misc + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.TaskStackBuilder +import androidx.core.net.toUri +import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider +import com.droidknights.app2023.feature.wearmain.WearMainActivity +import com.droidknights.app2023.feature.wearplayer.navigation.WearPlayerRoute +import javax.inject.Inject + +class SessionActivityIntentProviderImpl @Inject constructor( + private val context: Context, +) : SessionActivityIntentProvider { + override fun toPlayer(): PendingIntent? { + val deepLinkIntent = Intent( + Intent.ACTION_VIEW, + WearPlayerRoute.deepLinkUriPattern.toUri(), + context, + WearMainActivity::class.java + ) + + val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(deepLinkIntent) + getPendingIntent( + 0, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + } + return deepLinkPendingIntent + } +} diff --git a/app-wear-os/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app-wear-os/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/app-wear-os/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app-wear-os/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app-wear-os/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/app-wear-os/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app-wear-os/src/main/res/mipmap-hdpi/ic_launcher.webp b/app-wear-os/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..9690d1ed91994d744fcfc0f12e925da46a93e0dd GIT binary patch literal 2172 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 literal 0 HcmV?d00001 diff --git a/app-wear-os/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app-wear-os/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..d8f4354d29c3f24447e362dc7eb76bca22975d78 GIT binary patch literal 3440 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 Smke?ClXc)QoBn^(D+mCeEx;fE literal 0 HcmV?d00001 diff --git a/app-wear-os/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app-wear-os/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..f7e3d57e0a007546ec2fb70971ab1a5fee2ac3a8 GIT binary patch literal 3786 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 AZU6uP literal 0 HcmV?d00001 diff --git a/app-wear-os/src/main/res/mipmap-mdpi/ic_launcher.webp b/app-wear-os/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..a9b2e2b5fb18a7dccb61314c229a24d3fb69f8a0 GIT binary patch literal 1448 zcmV;Z1y}k~Nk&GX1pok7MM6+kP&iDJ1pojqFTe{B)rNtzZBqZTzk7&?34o}KKI_7j zBuQ~KVy4}(^#6Z)I2$3qJ>l8H9aG>jJh+Ppw*dfEmXu1MFu=MRjltkInS$=bG^ zk+fBuv~72n+O}fr1D1YlAUOo(_Ek06DEsxcJk$s^CW z!nP}*5sERz1K#sRT>oAmD2hk?caoy*kpZV_Y2RHmQgJexLnMcnBUS1%32984->r)HQptI4sASJk7hYQ|pIPoU-*q*=6Dk!AsPE|_(>(}87y5v3@AapOmO9Odt4 zbc>Jc#x+gTUN=smj(MBS)~8wcRkQnM{xVp1@w+zMI%dp&U)Z%-c}OI`DgOWepZumw zepe=cs8Zfl1rOrB=%!hln=yMKp#)?)`PvX{9yR5sjGdN!+1r`KSi}Y%huCChr2Rp8 z*3-Q0sCj=6!FIT68L!ImI5L{j^%R?c+5S@y>%c@8Cdd3mrY}F;wO2iV1`G#*Z~(^B z@1IS}_w`I<9mOYsp%?=L9Kv7;85$@)of#>t55In%!4Z&vV&PO+< zM`JV!6h^WuV-VAkJ%iliG22akZ;};Ka6DoK527`k$K@0d;h8ke1^@v-cmBIB)G9rE z>(2If=6Z6n3|+#6(uz=X_j{(sLJC1kRS{}T#vcQyiG0oY%D z=D=?0-g|eEXJUnKVt!x(8&fJM(vHb`iZ@_PnV24)ROua8?#+7}+;-H0*&RbZ4nk;v zabri-8_x|C8%J4X&~rrt>ipxE?jS&Id06K)h=AuF5WQYZ<XPdk?{d*&~WI%=qWzE87w&DDr{<~Xt7 zt?sK501yXIy>@rhROp;OFW7#`-g^ssqXJXe>b(E(6nfskK5Cys9rF)N#R-cyltA>9 z@p3p}$Zhr1GhR>!1`^o1j-s#lEOOr9{y*C}dwKTsc`WEt*hUYKL#eu}zcr!dADg|+ zoEOW)$aMZQaqMtm_h$SJ9rTAn)3}tXpU^By zLG5$4-NEJVr5jqrhpKoH0E|!oIeV`u$DgGePdUeLp=>&c%LEa#tb6O9ZF%2z@B{54 z5?FO#mBF;3z2nB*N&Vv0w%J?FQzuNh^P2hFEz_^t=WJUdE8d@dP9%7#dUBXNa Cvfvj0 literal 0 HcmV?d00001 diff --git a/app-wear-os/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app-wear-os/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..6be0df8d6ea7c809f64e61ea21356d17038b8acb GIT binary patch literal 2522 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 literal 0 HcmV?d00001 diff --git a/app-wear-os/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app-wear-os/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..97c5b37db9cbb7409fc80baf557af27d54e6536f GIT binary patch literal 3000 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 oPymt`W!XTJ1pZ)DF3>UgdF#*b21(~C1 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 g7A>}|(Z4aq7%hA>i>(kVy# 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 literal 0 HcmV?d00001 diff --git a/app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..39f62427d733cdacd39f040893dad01e1d9adac7 GIT binary patch literal 5532 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�B8^3n-?h;;=Y0(zKdJ03c1r3sYIZnl%6*J2|6Sv)M=-|7O-$vnFCr z_Bh%?GZ@3hN*e=!M?8*(2HeuTT;8_^e`hi~+}Sz;s^l-quNafA0x2L>TSc%hSu=^G zu>=tv0e}}bymME>g_6b?dus^U9cHr}0F_`p-u~IfL^J9E&gRlu)Q{PiJ8vs<11*JHXcHs~tfCYX;{mT(eZ_KdS3QWAQ^plj2d>!SYkw~$O z0B}%qQE-d*?o$F3s*psij$y_eabqh11bCoG+qP^95dgx}#Vl_*$fSZS1&N4~r2-`! z9?O*wrD_@gCQZ<_696s(qn2#~=qdR|`9gGT-a0f3P;@==YM*-3TC~Ay-{uqvdxh&@ z?ryL;5!IAd$cx?c54aYt0x!KDL zFxvXS;?7b-OV1qh>Jb`&zX3=kiT@?Hs?Q!Ze?#+9{_{uc|E&zZ`2gv?3lPpGZ1;Nh z*6DNa+OLj6ojkXw=CS7Y?FtWZFv4g=1;~&9YbT5)#2FGe^PVWmu%&1>Tf805hQsWSfUtqz*yk&j5 z=&U7xRAE&K9Hl(eIJr+)NRNWs&>r-ccKOc&TxQ+KN>#@5V8k1mj=G~- zb)f9vppxw_DG~rn$R8>%GCF%g$fgNE1OVykJ2XZcV@wPv!CzF_&Dpk0R0`^{rJPIP zuxUp@0a{M&Zpypoe?9>VSQv}4Dhdq(u;7|)PFG&K_i?RAAHP~VoYhMuI0FDOYI`yM zyME9&%n>_x;ne~N?HGzqOu#^bDJN8R&tq;|z}^icg@7NJf8Exn*BRaWgem1=+tIa_ zhjY**F~KkMk8XbJmDOAD>HrY3f=W9ACK*Jz%)FBoi_@Dm034tV4wDnaR9y`{n&0%W z7QT=?S%?0~%=mBhosIy!&`>E}J-K&JaipnX`*ix?9lp~MZb~3XP3&OS0RSf!uucT@ zDgl!a6%M<6#4@ob=A5$y33d0lDMfHdgKWCBk*UgcIfk(H;d_1xAh#Av+l9cxMIgDz zR2sla8_7&nUg3=#ZD|WIp(X|bNLEo|x{4QsRIyegS}JkGix%Si;{2e5RKi%|K-F?V z$S|6e<}Svdwh~u}D~mHA6#z}D)sO{R065TWwQ3XYXtva7fFQj&1fX-$yB?p{HjApc zrG%8s%gdX};vC1_x*-5>?4OTU++Lt#-2s0MfHjqx#PP-vj6aOGb&3-WTF*Yjl086n z9}@G2YZuPe4Qlp=XpjP+p%)%x0=}Lh6CVNOsZEff8-SLUG66Dm*bsoC1=|=UqB4F7 zqKPH|G^ZeesZ@=oTnM%l!M^=nbNl_sS=0IN?J6*BJIoY-sSFrL zvS~fBM}L=QkN)i6+lyp7FwksB2DR^44%a3Jhl3V`5ZSu1O+?ZL7!zoxQ6Oaddg`$G z1DcF8TTJVk3Kve228UBPj2nqnm4_s#5R`i01oG&0w9ed*vz}K?zb%bm6)N&iso#OF7}M3>|$Cl zanb=3#i@X!z%+Q4zw^PKE%0wrTK&N)L6K`jr>0S5$;5h_8UKXS!$Gl1eSP~LHO=y22t z#KSq;13)@SgO(QT02xR?sf?>?tM4dNkV1%Z|cQmcrd*xZK0wd=Mpa^`O^TzRC*8unna!^g6 zj)?@Vpt%NVMNkloC1|l}&G^r}d@oSn{R>nT*G*Pu-Bc=^A=4`65aZ;VA eq;cHUXv}8D=#^BeQi5hQo|a1GZ$K5_KVSo7c1D)~ literal 0 HcmV?d00001 diff --git a/app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..588dd63edc8f647fe878791a3ca0ba18b52abe60 GIT binary patch literal 4388 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 literal 0 HcmV?d00001 diff --git a/app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app-wear-os/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1dad85bf72f3f83c8212e7e59424e7734f965992 GIT binary patch literal 8966 zcmV+hBl+A?Nk&HgA^-qaMM6+kP&iETA^-p{zrZgLO*m}ZMv@>;_CC4)hG*{UAfo>h zz?XOaI3KO`(R{EzyUpaZ7Bq(n%pBGNm4YU~43JZc)ox5Sh$=|q7dfC&fFuGv)MHKo zw6$7eKB6ju`$t&zjW49O=H2TQw6@A_Udg7>Bz^Pbaj1KmL!ROBcmRld13aHkn8$3b zR`sxe`wT!uV_>btJ~P9IYpun3X67^A-)^_9TNfYSZUtz|c>y`LZB^02{npPvIwGJB z?mg=I_}q;o+p5wy;dfqL?1k)qRSvK4&~YQlPF112Ga>-;kA$NeJ;C>O)&B{=ucTD| zk?OIC=&%7uhjbW@M?%<)55uv{1e+#lD&q18HnvCrY!q{{8-zz9l~n}*=9N>`Z(p7s z7$2xq1w|oi4yJhWLw9P)RKn^;TaB<%iZnm@9BVfhnRqDHN?B2ncbYpX+b9 z?dI{UM?}TO9Gi5kY|GqiK=BB=yF@%EZmy%MNryjwcuqDLY-L3BJG zONb?++jMk!0-$0;0r+~xueAo_wrwOy{g<6{W_JG(F#-IgCc2?G6U&hb!2t=umWg=_ z+aq>JSc{CUCq_V4MCOvXkg|Xxe~Q`K_C%5-eKODNqGs-1&1&vp?*B5Fxw(6)Bi!Az z2u}x@d9DUtE3o*Zz@)-&tiXqjBuQ-=7xVuol7YOJ+BT9RlOj_3FY||`c5Qnh*?Qk3 z)vCtWvu)e9ZQDQRe6nqA8)JLsj2dINyQ`9S0(@cHzqoC-ZNLA2W+Yi)I}RsxnVC7I zG#As&%*^+_%FN8n%zQC3UuDK*bHgzUXXd}4zee&XSwAA*9o^1V&wa&p3U02(&bDpm zCrRi1{{H2vv2EM7J^4EFMw>aa8BVnO0rmsfw$1ibO_#d=_5Zz(C)>7F+p%pcmp-(n zv}L@RnYWLbDFoZK(+B} zB3@=}6OF?{`wq?x`UYEveTTinmf<~`fDR)ed*aWC^~CnX_e6d}SQs3xwF-P0skShA z_!D%IyerhY@kWN5lc!JQQPz7E#?(EGm9Mn@~_4?dl263O~oOV`!OTPP%`bQr%vYyYVe)evcN|Mx6I&tHS{ZI@vp^89 zGM~S~?;?n>eB$=!uu{#i1vO?6wV985zC)O<*TQw>C+Hs}9*B7YVx7$x z#yT6()NyW)&RTsH>WcRqPFxrB1jIU;_|PZ8wNsVj+j zVjW;c-+)+?n9o{A#?Y})Ok)z9t5_$*^iRR#LKU-x6U>#Y@Z~x)uHPo*3lPW^HDsoL zi_}qhg)~uskmjm-B~kDS>0@EYctje4%$0SJNoyc*41!F`mk2Uf*Fi$hq!J?#yeAx7 z?GN4~24`pZjIWiMGr}HLn1sodU$LMefe23*Fw{ta23_IvETG7siA6@f!?4AKh;YG> z8hv1;I}%`V5aRijm>80vMDQKr0=Ts+0Nr)Pq645yepvAcGLgi13?;Z{M2vC4GArx1 z?|zob1o|P-vz0-;dvGmg8|2?C`00I&n z2o@JOghnONEfi5)2tlHm8eFu(C);^V2DrWN|MS)@h#&j%+|fAA;3D0W5|-&HzdM_s z{NQ;QPfct2?GHTs?+@Jn+4nxG9kYK|c~1dECDDoyQ5y6#hzWuV2lxo!T7?MFN{Mlr z>Qd0lG^-fU;s8?8v;=}ou&0jF1x2P+bP$|>4?GH3XEJ?1sqqq;45U>Pq|T#Q0XtEduiKMN zj}P;sUo>@GDKhmv{d`t>*;5z5BC3AN7(x{kLXddHtzCiz9j*bC5-^eezbCJfl9nQs zs|<8y6N1DWZi32b>?sm{8W-1*N9*$dCe3K&=xeL5=<6tH)I^iCrRd0jMmp08ph8-) zqmk*|@s@I0{E}YrmJ&53%LD6F8#30H?=2^eQZ1PSNa$gc)DbbVmlQw^(T$5J`VC$| zLKYH2MB*QAQGgWS*v0usGBi@(aqv3mO+~*rz<=hQGSy&-l|8C_%=;9U_@H))~`iUz( z`~vvw9w-H&mdyKK^N#6qm7cE=LeOPMQMi35Qz|KRK9K-4u9icVgCScOsr((kN4=EbB9Zx&Ta@QB-UP}pQaU3evlFsF zH3tttx25MB5xHHcP8)s})dt0$2xl6k$>IrkKYQPGnEnDDCE*pEp%ZDei474{Rk14IZ}d% zK{5e^No9+PqAC3SKm4}P1-l-3W8@nz#VCYWzQu9-0v57%TxF&BkpE^3z*~Iv*8YWf zNb=AC|Gf3#U%v9y!WaGK`4(F~^S&SdX8iq)GmT;Zhq$mV8!p~ff#n5o_pQC|!>7NMbHDz4i_xFH`sRPd&`vcL4aBzH5*xqGR)Io0 zUvtMTORl>poj^CXdO$guG4E~ zM=r0cH=q9MFWdKyNAJBBcBbowmBfy3FL_M zqZF&kp{33pE56Gr3x1w(W~Xqs(v})!t6z=)!ltW6q!UhyT+b)v+0BXaO!B z@jy@lI<^W&2zC~MKVX4ah6muLOMA556`#rSs%>njF6GzxK{xls8mpWJs>9V>=A3fN zCGWlPf_J=g@aMA!Z|=titpIWmQzt;o{|+kz3Vsedvr)JOm1R>*|~RHuJbG9!{O@OdM;OG@T?9v&G4Drc|hi5 z)mLKxhgb+TcdQqhkb<2-(03>hODMcH>4CHCI?)>2EuN(4qABNmUfmnX@g3f=14N@@ zPFL0GVt4R!dxtv%F0FE9O%Xb&n(T_qBEVTthY;eKfW+K4m|?+Bvo|agh4^v?mLr-K zNHp2S-bX39fZddSasS3 zj#+}80zym7V)&ct>LvAhk_fKOsf`18Dl@FhYa@x~ab~w#BpnDjAEuBx`ynl|qW;euM5W!qStUkUoD7v!?LwcW!Vr+?BrNX}UL zjhigz{CdwJn8&vI+xEumc5|u-no=36`jdRFT6>HHl5oGmH2WwEZc!+lgOKIag$r}& zDXD{moWIoS$(c`o?}1u7hBN#A z`y^^{)D=tCa=&?YK+Z0_dcw=df(XiZ%BY?irfvdamCzHDEVz#Wi#!Ak;gIbhlW&d> zivofI{}b2&8Bp?UW2da2emdcXzq$3#Z1VSl&s^ZX@L~PAAIT^G;@|Y0@M}-&^FHi; z@CEtoZ_OW{4&MDyLAofP%nSGCX0BY_*BV4xj9OpU<0EZ7qHN99YOQSHEAKYH_=ueJ z(0xzN=e}?LQ}B_IP3pd_`nezCedU~c_qVejx}^TUBGmTsLXlrBEU`imnLL3)9oGaC zd1a6Rz$D+!mcPO-MJnWlx5*@AI?|J~ugZLqq>))f4iS5hwXLTu0Tj#h9!j6z))VY9 z){`|dKhH}`X6DsHn@k8jnbjw1SS5v3y_n639KnvO2ux()5;njy00<4y&4J{Q7u2~Q zmetJhCI2E*=08smeJEE3>yoF8u&+v^m1|{93y@jn-%py~GKbeZ>InT?G+x`P)G#H2 zj#W$CVP%$*tau5eBUNw&JI0=chjIg|L^D5r%>pteMP5C~FuZkwF(Ilbwdk8zIHG99 zVTs;xoZmi^8E~MtFOO!OJ~aM6f;;KVvlNy$-#*zGVgZn9E3L8Nty4o#BEPq;TQDvc2#XKdCmy9Xs_1Z|0FR+UpuK_yB$?6-o_b#&LyO zP)${fSu^cnWA^x*!%jQe^knW`ogk`Blfo5%PX(&GYFnfE7O(**GsA=s=%aWBFR8na z&p2|hoHuZeG3X(E%wj2l zcAHuau>aVdvF&U8;9*1Z|mWQ*6%!N`*W+Rb(bA4xp^2?Q^ky7m_9t?`%q$p z_@0l-s;Tc3e}lbGdrbHr4n2^9`QfsxuW&C8r_Suh=f)fB|t>5{sBMFc7{obty zl9N;-yd_e)2VZ8a;RhFr-bf&lr1>#4*qj&&10k8K7TV>^)a*Y&>e?(X%So z{B5oK)5xE=zfYG^P|ZugrtR6$hmWnSN4uJ4(}@#Mou11uv95ePTd*?&;1DB`NW?fg z*b*1f0cdhiUoruk{xs`0>2meoXZ2Rfc(X;}+YO&w?lalYt)|NCykAUgtjbZgV|j>k zCblC=93vEeBr3uKh^Pu8G12A|n@I-HoF_;@&_99I$W(OaB4YOhX~z&DfjaOtO{H;At3OUWTQTO<@{aeW3@=Mu`j_Nt3^~y*UF~94fqL zR8Wn05eSS--yjT9W_sMZkdlb*HIb$%16?vhVU7)l=*PtcOhM^2-}KY7@YzAp#Z?~5 zJYx1U5P^t3K{bv+sMyhmqqIYLI79D84!D_R5C5BQjp`1UskCnLF6Psl_ph_B*`!Y zEt$S8qtS>L0fM(o2ZQau-`W$cT(c$C1mKacAv0Ti=wnyVRLN$xT?oC#>buL zrbv-cElFo{6}-E56ixR;MYIQTLKSL9YbNQz{APK;*n$*75I(`eGQ*;05{7U{;S8e! z=mkLsLy1v(39C_9Et=9LJw{KnP!VwywcXL32ryKW^(3>&_TfTdA1T zGT+|$aHq62Fu#A%HMcjOZ~}Igk9QrYbdOD^p|z7X&)1}(ddz+QIVdXAx&p*N&;Kn1 z93QlMM!qHn5^-%JM5Plz&;oGUe(K4`{A35p&0ad-Gu_9KZ53KTLgM!YX;%nZdVFuv z(}avu1Z$ZZ0W}78$c(yV8bz|6LPXnBCrp9E%SbQiDwKWF)b^B%Ax0)~aEgWt8l_9gGs-Lt0Wp>- zYE$DBVp@MscTY`GSpvcZpS-B-tdT8S{-UW+xwJBj$DYPkZZ*YoHi!4Q3k8Z>dRqRI zMj?d}0y$}A&AdjiC@hB$BMb zX+o{*n-qybfdv7I1ivkG=nPV05@52LF!sRMBy^+OsK7)JeiO#rrM(X<#fi~-Uazf% zz*8b(n*NA4Q%cdAB58TeU*Ea7d~k85KR&bwOfFw-3hZ~e{vrRf70)@IbM2@$xwR9M znK!o_#;4odjg^0C71&zv20#atL=R-@?r5GSLBYu()Z}%`GX!e|DO?C9O~BS6XaLZd zTr?RbJlf}hEnGO4P6RaAm7zLab61I97{KembAXatUG8})jQZ5hV7t;YoS4P{jR1)R zzyzptLTXHcWtN%Cz7)i8;spboH%t|SA(-vczzES}egjV;qcE3Y`0#M4vUdX3$5%`m zHjCTmhN)r5>IbHsUGo!f`QC=KHw~P=k6d#U*!Az@96S|&J83pHuHQam>#lI&(o+vU z{L2vdbAf*c_JDv46edzOB-6qU>_}lX_X^ewBUGc<>KVdJ$UWWAOG$%ZLv8sm>%fJ*)a^*=Kw6YH-s}% zh*q3RTZT?+?ctwBlLC&nsBbM5(Tv&}Dc75<_4ek^2dJ!p2zARSVfbdwZNrxSp7#hU z4jqo^g%KLyX%vu1B(ydO5>z2+W7#QUf~+8rGLYzjp2|Ib1lngZAcVL4m;u3CaWKRy zQ$5k_nl*-Dz9_4vR<+m~eiZonECIY&Yogh+lS@yp1067cHZS-5g3tnd9>B!!dEHY? z44~9(4HpYAq&MQGR^qs1N-P zgsq_E4}f#`*9S0JZ45L)^M?4IZiFHzF+yP6Q6;nl2r6gkj`LqSuzZzHzF1pU0GL{v zcU`He{eP|v$`csQSnj9{M^H!;T*wU_9n%xwV&?Dl`UyIs8Ds!pW~C`QGep6JbAwkH zg115fVq#10bCwHR6Ig}? zvTP~sn##ZBEq2NN`L_q?=MIEFad}V|tc&(=FT5qn29HJbB z2NRBy9@{28&LB8MS@}QLKJNZb_iS4iVxEtiznH(coFEcef_tvjrer^;vtPq5QV5sg za*W5^dE7l)#uOp)G5Ytr5ny{=pmE{`XbIrb7DU4H8cu#8ZMiM77;K*&H-EOw^TmRMtwXnKwFiJ7 zR(EKEQ?AR3t$jrnIyx_u*uy%k7TgERv$1W+2yiI}Uz5NJt~Lw+Ft#C~5)$GX1^mBI zgg}fL>;tk>u;_k;+r z8MXl;B9Tb+FI?9JF_4HwgaE+9Iv7C91U_DfYY{9Wp7A$=17jFOgN!#bv7TRa?vhY`_Y3-t_&`b2bbj9qxiM0*-~`o9!v zjR+VKT13E(icHs%VJ*fi+Sf~ux2HUah(<7?5iMG4jx7y6l_ItX5#trNMZ`t*milmO zEn18U!~Dmv+O)1u>lSERs(1X= zQ`KWWeim&UTFj46@6|08?UPj)>pHd8f^m+BXzMyn>r)Ur-fj7DPx0H2pSdB}z#7ir z?hhi@Qye2Kw?mdFqecR<4)OawIpkD1Y)q8)#4 zsf@NoJc*>iwKc}v0Dg#j_y%%Mzvy~OI6G$64oJW%&>lcU2%do@BYQgbyxsF~hgXnL zTkz@a>agvIQ3D&m;BXJyfb>l4XnKY98@6^VPUI$Fo|^#df+CupE0LnL1K=WoX8fg1kCdc z5oZx-SeAuMC^I0unbw|$9W8q%h}eQ{62>$=P17{3)izhuF9?LV-2}hg=I2n@&bVG- zN@ALrk;w1JNu(zn!cGF1f%!!lfQ5xbtS6co>FLk(i1tj^9bJjAP!ZmOt1#A8nA7w$ zt?SyF&6V{70>N*$+kBhr=ks&08B;U%Z;&bq-|0wCWQmkd5_-m(uq1d!%n2kzLMF_N z6v;$96BR)*o*C*H?dcPNd&Uych9IJDp;Z`doz`iZ)~T)0f@^Fb9=2#LT5Ms$;>vRX zFkshDHaI@)u+%D`wVve1d!mU%ivR-Z7NEt)&9&jMO?Aaw@8D3wG~>5w6&xX+1424D z)iHk}OxD&~5S74SySj4yBce3|z|`t$+tn4QKm%H|h=>*q0>Id=HjbA8h-gF*9H>4F z1_I)6JAkpNwlOb_K}67qsIGQBfWZU=2U + + #141414 + \ No newline at end of file diff --git a/app-wear-os/src/main/res/values/strings.xml b/app-wear-os/src/main/res/values/strings.xml new file mode 100644 index 00000000..e20a8c02 --- /dev/null +++ b/app-wear-os/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + 드로이드나이츠 2023 for wear os + \ No newline at end of file diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt index 7b91192d..027c4157 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt @@ -1,5 +1,6 @@ package com.droidknights.app2023.core.playback.playstate +import android.net.Uri import androidx.media3.common.C data class PlaybackState( @@ -9,5 +10,8 @@ data class PlaybackState( val position: Long = C.TIME_UNSET, val duration: Long = C.TIME_UNSET, val speed: Float = 1F, - val aspectRatio: Float = 16F / 9F + val aspectRatio: Float = 16F / 9F, + val title: String ?= null, + val artist: String? = null, + val artworkUri: Uri? = null, ) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt index 62f654a7..40e69e90 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt @@ -79,7 +79,10 @@ internal class PlaybackStateListener @Inject constructor( speed = player.playbackParameters.speed, aspectRatio = with(player.videoSize) { if (height == 0 || width == 0) 16F / 9F else width * pixelWidthHeightRatio / height - } + }, + title = player.currentMediaItem?.mediaMetadata?.title?.toString(), + artist = player.currentMediaItem?.mediaMetadata?.artist?.toString(), + artworkUri = player.currentMediaItem?.mediaMetadata?.artworkUri ) } } \ No newline at end of file diff --git a/feature/wear-main/.gitignore b/feature/wear-main/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/wear-main/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/wear-main/build.gradle.kts b/feature/wear-main/build.gradle.kts new file mode 100644 index 00000000..3871b68d --- /dev/null +++ b/feature/wear-main/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("droidknights.android.feature") +} + +android { + namespace = "com.droidknights.app2023.feature.wearmain" +} + +dependencies { + implementation(projects.feature.wearSession) + implementation(projects.feature.wearPlayer) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.androidx.lifecycle.viewModelCompose) + + implementation(libs.androidx.compose.wear.foundation) + implementation(libs.androidx.compose.wear.material) + implementation(libs.androidx.compose.wear.navigation) +} diff --git a/feature/wear-main/src/main/AndroidManifest.xml b/feature/wear-main/src/main/AndroidManifest.xml new file mode 100644 index 00000000..860cdb04 --- /dev/null +++ b/feature/wear-main/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainActivity.kt b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainActivity.kt new file mode 100644 index 00000000..2e993d5d --- /dev/null +++ b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainActivity.kt @@ -0,0 +1,19 @@ +package com.droidknights.app2023.feature.wearmain + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class WearMainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + KnightsTheme(darkTheme = true) { + WearMainScreen() + } + } + } +} diff --git a/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainNavigator.kt b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainNavigator.kt new file mode 100644 index 00000000..51f252d7 --- /dev/null +++ b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainNavigator.kt @@ -0,0 +1,25 @@ +package com.droidknights.app2023.feature.wearmain + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import androidx.wear.compose.navigation.rememberSwipeDismissableNavController +import com.droidknights.app2023.feature.wearplayer.navigation.navigateWearPlayer +import com.droidknights.app2023.feature.wearsession.navigation.WearSessionRoute + +internal class MainNavigator( + val navController: NavHostController, +) { + val startDestination = WearSessionRoute.route + + fun navigateWearPlayer(sessionId: String) { + navController.navigateWearPlayer(sessionId) + } +} + +@Composable +internal fun rememberMainNavigator( + navController: NavHostController = rememberSwipeDismissableNavController(), +): MainNavigator = remember(navController) { + MainNavigator(navController) +} diff --git a/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainScreen.kt b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainScreen.kt new file mode 100644 index 00000000..244e4c4b --- /dev/null +++ b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainScreen.kt @@ -0,0 +1,26 @@ +package com.droidknights.app2023.feature.wearmain + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.wear.compose.navigation.SwipeDismissableNavHost +import com.droidknights.app2023.feature.wearplayer.navigation.wearPlayerNavGraph +import com.droidknights.app2023.feature.wearsession.navigation.wearSessionNavGraph + +@Composable +internal fun WearMainScreen( + navigator: MainNavigator = rememberMainNavigator() +) { + Surface(color = MaterialTheme.colorScheme.background) { + // https://developer.android.com/training/wearables/compose/navigation + SwipeDismissableNavHost( + navController = navigator.navController, + startDestination = navigator.startDestination, + ) { + wearSessionNavGraph( + onSessionClick = { navigator.navigateWearPlayer(it.id) }, + ) + wearPlayerNavGraph() + } + } +} diff --git a/feature/wear-player/.gitignore b/feature/wear-player/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/wear-player/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/wear-player/build.gradle.kts b/feature/wear-player/build.gradle.kts new file mode 100644 index 00000000..eb2c7cfd --- /dev/null +++ b/feature/wear-player/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("droidknights.android.feature") +} + +android { + namespace = "com.droidknights.app2023.feature.wearplayer" +} + +dependencies { + implementation(projects.core.playback) + implementation(libs.androidx.compose.wear.foundation) + implementation(libs.androidx.compose.wear.material) + implementation(libs.androidx.compose.wear.navigation) +} diff --git a/feature/wear-player/src/main/AndroidManifest.xml b/feature/wear-player/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9a40236b --- /dev/null +++ b/feature/wear-player/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerScreen.kt b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerScreen.kt new file mode 100644 index 00000000..ae90aa8b --- /dev/null +++ b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerScreen.kt @@ -0,0 +1,201 @@ +package com.droidknights.app2023.feature.wearplayer + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FastForward +import androidx.compose.material.icons.filled.FastRewind +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.droidknights.app2023.core.designsystem.component.NetworkImage +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme + +@Composable +internal fun WearPlayerScreen( + viewModel: WearPlayerViewModel = hiltViewModel(), +) { + val playerUiState by viewModel.playerUiState.collectAsStateWithLifecycle() + + CompositionLocalProvider( + LocalContentColor provides Color.White + ) { + PlayerContent( + uiState = playerUiState, + onRewindButtonClick = viewModel::rewind, + onPlayPauseButtonClick = viewModel::playPause, + onFastForwardButtonClick = viewModel::fastForward, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun PlayerContent( + uiState: WearPlayerUiState, + onRewindButtonClick: () -> Unit, + onPlayPauseButtonClick: () -> Unit, + onFastForwardButtonClick: () -> Unit, +) { + when (uiState) { + is WearPlayerUiState.Loading -> PlayerLoading() + is WearPlayerUiState.Success -> Box( + modifier = Modifier + .background(Color.Black) + .fillMaxSize() + ) { + NetworkImage( + modifier = Modifier.fillMaxSize(), + imageUrl = uiState.artworkUri?.toString() + ) + Column( + modifier = Modifier + .background(Color.Black.copy(alpha = 0.6F)) + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceAround, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = uiState.artist ?: "알 수 없음", + modifier = Modifier + .fillMaxWidth() + .basicMarquee() + .padding(horizontal = 16.dp), + style = KnightsTheme.typography.titleMediumR, + textAlign = TextAlign.Center + ) + Text( + text = uiState.title ?: "제목 없음", + modifier = Modifier + .fillMaxWidth() + .basicMarquee() + .padding(horizontal = 16.dp), + style = KnightsTheme.typography.titleLargeB, + textAlign = TextAlign.Center + ) + } + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RewindButton( + onClick = onRewindButtonClick + ) + PlayPauseButton( + isPlaying = uiState.isPlaying, + onClick = onPlayPauseButtonClick, + ) + FastForwardButton( + onClick = onFastForwardButtonClick + ) + } + PositionText(uiState.position, uiState.duration) + } + } + } +} + +@Composable +private fun PlayerLoading() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + +@Composable +internal fun PlayPauseButton( + isPlaying: Boolean, + onClick: () -> Unit +) { + IconButton(onClick = onClick) { + if (isPlaying) { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.Pause, + contentDescription = "일시 정지", + ) + } else { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.PlayArrow, + contentDescription = "재생", + ) + } + } +} + +@Composable +internal fun RewindButton( + onClick: () -> Unit +) { + IconButton(onClick = onClick) { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.FastRewind, + contentDescription = "되감기", + ) + } +} + +@Composable +internal fun FastForwardButton( + onClick: () -> Unit +) { + IconButton(onClick = onClick) { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.FastForward, + contentDescription = "빨리 감기", + ) + } +} + +@Composable +internal fun PositionText(position: Long, duration: Long) { + Text( + style = KnightsTheme.typography.bodyMediumR, + text = "${position.formatAsDuration()} / ${duration.formatAsDuration()}" + ) +} + +private fun Long.formatAsDuration(): String { + val hours = this / 1000 / 3600 + val minutes = ((this / 1000) % 3600) / 60 + val seconds = (this / 1000) % 60 + + return when { + hours > 0 -> String.format("%02d:%02d:%02d", hours, minutes, seconds) + else -> String.format("%02d:%02d", minutes, seconds) + } +} \ No newline at end of file diff --git a/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerUiState.kt b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerUiState.kt new file mode 100644 index 00000000..be3b8621 --- /dev/null +++ b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerUiState.kt @@ -0,0 +1,21 @@ +package com.droidknights.app2023.feature.wearplayer + +import android.net.Uri + +sealed interface WearPlayerUiState { + + object Loading : WearPlayerUiState + + data class Success( + val isPlaying: Boolean, + val hasPrevious: Boolean, + val hasNext: Boolean, + val position: Long, + val duration: Long, + val speed: Float, + val aspectRatio: Float, + val title: String?, + val artist: String?, + val artworkUri: Uri?, + ) : WearPlayerUiState +} \ No newline at end of file diff --git a/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerViewModel.kt b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerViewModel.kt new file mode 100644 index 00000000..09b39fcf --- /dev/null +++ b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerViewModel.kt @@ -0,0 +1,67 @@ +package com.droidknights.app2023.feature.wearplayer + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.droidknights.app2023.core.domain.usecase.GetCurrentPlayingSessionUseCase +import com.droidknights.app2023.core.domain.usecase.UpdateCurrentPlayingSessionUseCase +import com.droidknights.app2023.core.playback.PlayerController +import com.droidknights.app2023.core.playback.playstate.PlaybackStateManager +import com.droidknights.app2023.feature.wearplayer.navigation.WearPlayerRoute +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class WearPlayerViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + getCurrentPlayingSessionUseCase: GetCurrentPlayingSessionUseCase, + updateCurrentPlayingSessionUseCase: UpdateCurrentPlayingSessionUseCase, + private val playbackStateManager: PlaybackStateManager, + private val playerController: PlayerController, +) : ViewModel() { + private val _playerUiState = + MutableStateFlow(WearPlayerUiState.Loading) + val playerUiState: StateFlow = _playerUiState + + init { + viewModelScope.launch { + val sessionId = savedStateHandle.get(WearPlayerRoute.argumentName) + .takeIf { !it.isNullOrBlank() } + ?: getCurrentPlayingSessionUseCase() + ?: "1" // 처음부터 재생 + updateCurrentPlayingSessionUseCase(sessionId) + playerController.play() + } + viewModelScope.launch { + playbackStateManager.flow.collect { + _playerUiState.value = WearPlayerUiState.Success( + it.isPlaying, + it.hasPrevious, + it.hasNext, + it.position, + it.duration, + it.speed, + it.aspectRatio, + it.title, + it.artist, + it.artworkUri, + ) + } + } + } + + fun playPause() { + playerController.playPause() + } + + fun rewind() { + playerController.rewind() + } + + fun fastForward() { + playerController.fastForward() + } +} diff --git a/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/navigation/WearPlayerNavigation.kt b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/navigation/WearPlayerNavigation.kt new file mode 100644 index 00000000..5cfd4418 --- /dev/null +++ b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/navigation/WearPlayerNavigation.kt @@ -0,0 +1,37 @@ +package com.droidknights.app2023.feature.wearplayer.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.navArgument +import androidx.navigation.navDeepLink +import androidx.wear.compose.navigation.composable +import com.droidknights.app2023.feature.wearplayer.WearPlayerScreen + +fun NavController.navigateWearPlayer(sessionId: String) { + navigate(WearPlayerRoute.route(sessionId)) +} + +fun NavGraphBuilder.wearPlayerNavGraph() { + composable( + route = WearPlayerRoute.route("{${WearPlayerRoute.argumentName}}"), + arguments = listOf( + navArgument("sessionId") { + type = NavType.StringType + defaultValue = "" + } + ), + deepLinks = listOf( + navDeepLink { uriPattern = WearPlayerRoute.deepLinkUriPattern } + ), + ) { + WearPlayerScreen() + } +} + +object WearPlayerRoute { + const val route = "wear-player" + fun route(sessionId: String = ""): String = "$route?$argumentName=$sessionId" + const val argumentName = "sessionId" + const val deepLinkUriPattern = "droidknights://$route" +} diff --git a/feature/wear-session/.gitignore b/feature/wear-session/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/wear-session/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/wear-session/build.gradle.kts b/feature/wear-session/build.gradle.kts new file mode 100644 index 00000000..27029f0a --- /dev/null +++ b/feature/wear-session/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("droidknights.android.feature") +} + +android { + namespace = "com.droidknights.app2023.feature.wearsession" +} + +dependencies { + implementation(libs.kotlinx.immutable) + + implementation(libs.androidx.compose.wear.foundation) + implementation(libs.androidx.compose.wear.material) + implementation(libs.androidx.compose.wear.navigation) +} diff --git a/feature/wear-session/src/main/AndroidManifest.xml b/feature/wear-session/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/wear-session/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt new file mode 100644 index 00000000..967be1c7 --- /dev/null +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt @@ -0,0 +1,135 @@ +package com.droidknights.app2023.feature.wearsession + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.droidknights.app2023.core.designsystem.component.KnightsCard +import com.droidknights.app2023.core.designsystem.component.NetworkImage +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import com.droidknights.app2023.core.model.Level +import com.droidknights.app2023.core.model.Room +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.core.model.Speaker +import com.droidknights.app2023.core.model.Tag +import com.droidknights.app2023.core.model.Video +import kotlinx.datetime.LocalDateTime + +@Composable +internal fun SessionCard( + session: Session, + modifier: Modifier = Modifier, + onSessionClick: (Session) -> Unit = { }, +) { + if (session.video.isReady) { + KnightsCard( + modifier = modifier, + onClick = { onSessionClick(session) } + ) { + SessionCardContent(session = session) + } + } else { + KnightsCard( + modifier = modifier + ) { + SessionCardContent(session = session) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun SessionCardContent( + session: Session, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + NetworkImage( + imageUrl = session.speakers.firstOrNull()?.imageUrl ?: DefaultImageUrl, + modifier = Modifier + .size(36.dp) + .clip(CircleShape), + placeholder = painterResource(id = com.droidknights.app2023.core.ui.R.drawable.placeholder_speaker), + ) + Column( + modifier = Modifier.weight(1F), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + // 제목 + Text( + modifier = Modifier.basicMarquee(), + text = session.title, + style = KnightsTheme.typography.titleMediumB, + color = MaterialTheme.colorScheme.onPrimaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + // 발표자 + session.speakers + .joinToString(",") { it.name } + .takeIf { it.isNotBlank() } + ?.let { speakerName -> + Text( + modifier = Modifier.basicMarquee(), + text = speakerName, + style = KnightsTheme.typography.bodyMediumR, + color = MaterialTheme.colorScheme.onSecondaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + +} + +@Preview +@Composable +private fun SessionCardPreview() { + val fakeSession = Session( + id = "1", + title = "Jetpack Compose에 있는 것, 없는 것", + content = "", + speakers = listOf( + Speaker( + name = "안성용", + introduction = "안드로이드 개발자", + imageUrl = "https://picsum.photos/200", + ), + ), + level = Level.BASIC, + tags = listOf( + Tag("효율적인 코드베이스") + ), + startTime = LocalDateTime(2023, 9, 12, 16, 10, 0), + endTime = LocalDateTime(2023, 9, 12, 16, 45, 0), + room = Room.TRACK1, + video = Video.None + ) + + KnightsTheme { + SessionCard(fakeSession) + } +} + +private const val DefaultImageUrl = "https://raw.githubusercontent.com/workspace/media-samples/main/img/logo.jpg" diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionScreen.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionScreen.kt new file mode 100644 index 00000000..01080078 --- /dev/null +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionScreen.kt @@ -0,0 +1,133 @@ +package com.droidknights.app2023.feature.wearsession + +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.rotary.onRotaryScrollEvent +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.ScalingLazyListScope +import androidx.wear.compose.foundation.lazy.items +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import com.droidknights.app2023.core.model.Room +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.core.ui.RoomText +import kotlinx.collections.immutable.PersistentList +import kotlinx.coroutines.launch + +@Composable +internal fun WearSessionScreen( + onSessionClick: (Session) -> Unit, + wearSessionViewModel: WearSessionViewModel = hiltViewModel(), +) { + val wearSessionUiState by wearSessionViewModel.uiState.collectAsStateWithLifecycle() + + WearSessionContent( + wearSessionUiState = wearSessionUiState, + modifier = Modifier.fillMaxSize(), + onSessionClick = onSessionClick, + ) +} + +@Composable +private fun WearSessionContent( + wearSessionUiState: WearSessionUiState, + onSessionClick: (Session) -> Unit, + modifier: Modifier = Modifier, +) { + when (wearSessionUiState) { + WearSessionUiState.Loading -> Box(modifier, contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + is WearSessionUiState.Sessions -> { + val focusRequester = remember { FocusRequester() } + val coroutineScope = rememberCoroutineScope() + val listState = rememberScalingLazyListState() + // https://developer.android.com/training/wearables/compose/lists + ScalingLazyColumn( + modifier = modifier + // https://developer.android.com/training/wearables/compose/rotary-input#scroll + .onRotaryScrollEvent { event -> + coroutineScope.launch { + listState.scrollBy(event.verticalScrollPixels) + } + true + } + .focusRequester(focusRequester) + .focusable(), + state = listState, + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + sessionItems( + items = wearSessionUiState.sessions, + onSessionClick = onSessionClick, + ) + } + LaunchedEffect(Unit) { focusRequester.requestFocus() } + } + } +} + +private fun ScalingLazyListScope.sessionItems( + items: PersistentList, + onSessionClick: (Session) -> Unit, +) { + items.groupBy { it.room }.entries.forEach { (room, sessions) -> + item("room-title-${room.name}") { + RoomTitle( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + room = room + ) + } + items(sessions, key = { session -> "session-item-$session" }) { session -> + SessionCard( + modifier = Modifier.fillMaxWidth(), + session = session, + onSessionClick = onSessionClick + ) + } + } +} + +@Composable +private fun RoomTitle( + modifier: Modifier = Modifier, + room: Room, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + RoomText( + room = room, + style = KnightsTheme.typography.titleLargeB, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Spacer(modifier = Modifier.height(8.dp)) + Divider(thickness = 2.dp, color = MaterialTheme.colorScheme.onPrimaryContainer) + } +} diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionUiState.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionUiState.kt new file mode 100644 index 00000000..c10575ea --- /dev/null +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionUiState.kt @@ -0,0 +1,12 @@ +package com.droidknights.app2023.feature.wearsession + +import com.droidknights.app2023.core.model.Session +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +sealed interface WearSessionUiState { + object Loading : WearSessionUiState + data class Sessions( + val sessions: PersistentList = persistentListOf(), + ) : WearSessionUiState +} diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionViewModel.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionViewModel.kt new file mode 100644 index 00000000..5df9c458 --- /dev/null +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionViewModel.kt @@ -0,0 +1,34 @@ +package com.droidknights.app2023.feature.wearsession + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.droidknights.app2023.core.domain.usecase.GetSessionsUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class WearSessionViewModel @Inject constructor( + private val getSessionsUseCase: GetSessionsUseCase, +) : ViewModel() { + + private val _errorFlow = MutableSharedFlow() + val errorFlow: SharedFlow get() = _errorFlow + + val uiState: StateFlow = flow { emit(getSessionsUseCase().toPersistentList()) } + .map(WearSessionUiState::Sessions) + .catch { throwable -> _errorFlow.emit(throwable) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = WearSessionUiState.Loading + ) +} diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/navigation/WearSessionNavigation.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/navigation/WearSessionNavigation.kt new file mode 100644 index 00000000..7ae8edf3 --- /dev/null +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/navigation/WearSessionNavigation.kt @@ -0,0 +1,23 @@ +package com.droidknights.app2023.feature.wearsession.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.wear.compose.navigation.composable +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.feature.wearsession.WearSessionScreen + +fun NavController.navigateWearSession() { + navigate(WearSessionRoute.route) +} + +fun NavGraphBuilder.wearSessionNavGraph( + onSessionClick: (Session) -> Unit, +) { + composable(WearSessionRoute.route) { + WearSessionScreen(onSessionClick = onSessionClick) + } +} + +object WearSessionRoute { + const val route: String = "wear-session" +} diff --git a/feature/wear-session/src/main/res/values/strings.xml b/feature/wear-session/src/main/res/values/strings.xml new file mode 100644 index 00000000..106c8f37 --- /dev/null +++ b/feature/wear-session/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + 세션 목록 + + 카테고리 + HH:mm 발표 + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8696676d..4e526ccf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,6 +73,7 @@ androidx-compose-tv-material = { group = "androidx.tv", name = "tv-material", ve play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "play-services-wearable" } androidx-compose-wear-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "androidXComposeWear" } androidx-compose-wear-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "androidXComposeWear" } +androidx-compose-wear-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "androidXComposeWear" } androidx-media3-player = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "androidxMedia3" } androidx-media3-player-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "androidxMedia3" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3dfdde0a..e304c877 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,7 @@ include( ":app", ":app-automotive", ":app-tv", + ":app-wear-os", ":core:designsystem", ":core:data", @@ -39,5 +40,8 @@ include( ":feature:bookmark", ":feature:player", ":feature:tv-main", - ":feature:tv-session" + ":feature:tv-session", + ":feature:wear-main", + ":feature:wear-player", + ":feature:wear-session", ) From 93c5e6aba73f78f42e025c527548c4e3586b6e71 Mon Sep 17 00:00:00 2001 From: workspace Date: Sat, 2 Sep 2023 18:43:46 +0900 Subject: [PATCH 28/42] lint fix --- .../app2023/core/playback/PlayerController.kt | 4 ++-- .../app2023/core/playback/di/PlaybackModule.kt | 2 +- .../app2023/core/playback/playstate/PlaybackState.kt | 2 +- .../core/playback/playstate/PlaybackStateManager.kt | 1 - .../app2023/core/playback/session/MediaItemProvider.kt | 3 +-- .../droidknights/app2023/feature/player/PlayerScreen.kt | 6 ++++-- .../app2023/feature/tvmain/TvMainNavigator.kt | 8 ++++---- .../droidknights/app2023/feature/tvmain/TvMainScreen.kt | 2 +- .../app2023/feature/wearmain/WearMainNavigator.kt | 8 ++++---- .../app2023/feature/wearmain/WearMainScreen.kt | 2 +- .../app2023/feature/wearplayer/WearPlayerScreen.kt | 5 +++-- .../app2023/feature/wearsession/WearSessionCard.kt | 1 - 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt index 278c650e..f09fbd02 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt @@ -96,8 +96,8 @@ class PlayerController @Inject constructor( return true } - private fun MediaController.currentSessionId(): String? - = (currentMediaItem?.mediaId?.toMediaIdOrNull() as? MediaId.Session)?.id + private fun MediaController.currentSessionId(): String? = + (currentMediaItem?.mediaId?.toMediaIdOrNull() as? MediaId.Session)?.id private inline fun executeAfterPrepare(crossinline action: suspend (MediaController) -> Unit) { scope.launch { diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/di/PlaybackModule.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/di/PlaybackModule.kt index 378ba06f..8a92f0e6 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/di/PlaybackModule.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/di/PlaybackModule.kt @@ -34,7 +34,7 @@ internal object PlaybackModule { fun player( service: Service, playbackStateListener: PlaybackStateListener, - ) : Player { + ): Player { val dataSourceFactory = DefaultDataSource.Factory(service) val mediaSourceFactory = DashMediaSource.Factory( DefaultDashChunkSource.Factory( diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt index 027c4157..39fe2bb2 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackState.kt @@ -11,7 +11,7 @@ data class PlaybackState( val duration: Long = C.TIME_UNSET, val speed: Float = 1F, val aspectRatio: Float = 16F / 9F, - val title: String ?= null, + val title: String? = null, val artist: String? = null, val artworkUri: Uri? = null, ) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateManager.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateManager.kt index f17364fa..1f2577f5 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateManager.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateManager.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject import javax.inject.Singleton - @Singleton class PlaybackStateManager @Inject constructor() { private val _playbackState = MutableStateFlow(PlaybackState()) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt index ce25ff91..5590f08e 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt @@ -132,7 +132,6 @@ class MediaItemProvider @Inject constructor( .filter { session -> session.tags.any { it.name == tag.name } } .map(::mediaItem) - fun mediaItem(session: Session): MediaItem = MediaItem( title = session.title, description = session.content, @@ -147,7 +146,7 @@ class MediaItemProvider @Inject constructor( artist = session.speakers.joinToString(",") { it.name }, ) - suspend fun currentMediaItemsOrKeynote() : MediaItemsWithStartPosition { + suspend fun currentMediaItemsOrKeynote(): MediaItemsWithStartPosition { val currentPlayingSessionId = sessionRepository.getCurrentPlayingSessionId().first() ?: "1" val session = sessionRepository.getSession(currentPlayingSessionId) diff --git a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerScreen.kt b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerScreen.kt index 5efcaaa3..e628ae52 100644 --- a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerScreen.kt +++ b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import java.util.Locale @Composable internal fun PlayerScreen( @@ -209,10 +210,11 @@ private fun Long.formatAsDuration(): String { val seconds = (this / 1000) % 60 return when { - hours > 0 -> String.format("%02d:%02d:%02d", hours, minutes, seconds) - else -> String.format("%02d:%02d", minutes, seconds) + hours > 0 -> String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds) + else -> String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) } } + @Composable internal fun PositionSeekBar( modifier: Modifier = Modifier, diff --git a/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainNavigator.kt b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainNavigator.kt index 624fafd6..0b5fb112 100644 --- a/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainNavigator.kt +++ b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainNavigator.kt @@ -7,7 +7,7 @@ import androidx.navigation.compose.rememberNavController import com.droidknights.app2023.feature.player.navigation.navigatePlayer import com.droidknights.app2023.feature.tvsession.navigation.TvSessionRoute -internal class MainNavigator( +internal class TvMainNavigator( val navController: NavHostController, ) { val startDestination = TvSessionRoute.route @@ -22,8 +22,8 @@ internal class MainNavigator( } @Composable -internal fun rememberMainNavigator( +internal fun rememberTvMainNavigator( navController: NavHostController = rememberNavController(), -): MainNavigator = remember(navController) { - MainNavigator(navController) +): TvMainNavigator = remember(navController) { + TvMainNavigator(navController) } diff --git a/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainScreen.kt b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainScreen.kt index d82ff354..ea9254b0 100644 --- a/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainScreen.kt +++ b/feature/tv-main/src/main/kotlin/com/droidknights/app2023/feature/tvmain/TvMainScreen.kt @@ -12,7 +12,7 @@ import com.droidknights.app2023.feature.tvsession.navigation.tvSessionNavGraph @OptIn(ExperimentalTvMaterial3Api::class) @Composable internal fun TvMainScreen( - navigator: MainNavigator = rememberMainNavigator() + navigator: TvMainNavigator = rememberTvMainNavigator() ) { Surface( colors = NonInteractiveSurfaceDefaults.colors( diff --git a/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainNavigator.kt b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainNavigator.kt index 51f252d7..948d2422 100644 --- a/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainNavigator.kt +++ b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainNavigator.kt @@ -7,7 +7,7 @@ import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import com.droidknights.app2023.feature.wearplayer.navigation.navigateWearPlayer import com.droidknights.app2023.feature.wearsession.navigation.WearSessionRoute -internal class MainNavigator( +internal class WearMainNavigator( val navController: NavHostController, ) { val startDestination = WearSessionRoute.route @@ -18,8 +18,8 @@ internal class MainNavigator( } @Composable -internal fun rememberMainNavigator( +internal fun rememberWearMainNavigator( navController: NavHostController = rememberSwipeDismissableNavController(), -): MainNavigator = remember(navController) { - MainNavigator(navController) +): WearMainNavigator = remember(navController) { + WearMainNavigator(navController) } diff --git a/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainScreen.kt b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainScreen.kt index 244e4c4b..25d7845e 100644 --- a/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainScreen.kt +++ b/feature/wear-main/src/main/kotlin/com/droidknights/app2023/feature/wearmain/WearMainScreen.kt @@ -9,7 +9,7 @@ import com.droidknights.app2023.feature.wearsession.navigation.wearSessionNavGra @Composable internal fun WearMainScreen( - navigator: MainNavigator = rememberMainNavigator() + navigator: WearMainNavigator = rememberWearMainNavigator() ) { Surface(color = MaterialTheme.colorScheme.background) { // https://developer.android.com/training/wearables/compose/navigation diff --git a/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerScreen.kt b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerScreen.kt index ae90aa8b..617d62f7 100644 --- a/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerScreen.kt +++ b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerScreen.kt @@ -33,6 +33,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.droidknights.app2023.core.designsystem.component.NetworkImage import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import java.util.Locale @Composable internal fun WearPlayerScreen( @@ -195,7 +196,7 @@ private fun Long.formatAsDuration(): String { val seconds = (this / 1000) % 60 return when { - hours > 0 -> String.format("%02d:%02d:%02d", hours, minutes, seconds) - else -> String.format("%02d:%02d", minutes, seconds) + hours > 0 -> String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds) + else -> String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) } } \ No newline at end of file diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt index 967be1c7..9e9a3582 100644 --- a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt @@ -100,7 +100,6 @@ private fun SessionCardContent( } } } - } @Preview From 7d9204d899004c7b001fd1a4adb74721980fa591 Mon Sep 17 00:00:00 2001 From: workspace Date: Thu, 14 Sep 2023 17:43:19 +0900 Subject: [PATCH 29/42] =?UTF-8?q?GithubRawApi=20url=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/droidknights/app2023/core/data/api/GithubRawApi.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt index 4b18d5f6..43823ffc 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt @@ -6,9 +6,9 @@ import retrofit2.http.GET internal interface GithubRawApi { - @GET("/workspace/DroidKnights2023_App/media3/core/data/src/main/assets/sponsors.json") + @GET("/droidknights/DroidKnights2023_App/reference-media3/core/data/src/main/assets/sponsors.json") suspend fun getSponsors(): List - @GET("/workspace/DroidKnights2023_App/media3/core/data/src/main/assets/sessions.json") + @GET("/droidknights/DroidKnights2023_App/reference-media3/core/data/src/main/assets/sessions.json") suspend fun getSessions(): List } From ef0a7036f5bf8caf7a5c77f6a34470d459575e3d Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 06:50:15 +0900 Subject: [PATCH 30/42] =?UTF-8?q?Application=20->=20@ApplicationContext?= =?UTF-8?q?=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/droidknights/app2023/automotive/di/AndroidModule.kt | 4 ++-- .../main/java/com/droidknights/app2023/tv/di/AndroidModule.kt | 3 ++- .../java/com/droidknights/app2023/wear/di/AndroidModule.kt | 3 ++- .../main/java/com/droidknights/app2023/di/AndroidModule.kt | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/di/AndroidModule.kt b/app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/di/AndroidModule.kt index 24373bab..28392280 100644 --- a/app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/di/AndroidModule.kt +++ b/app-automotive/src/main/kotlin/com/droidknights/app2023/automotive/di/AndroidModule.kt @@ -1,19 +1,19 @@ package com.droidknights.app2023.automotive.di -import android.app.Application import android.app.PendingIntent import android.content.Context import com.droidknights.app2023.core.playback.session.SessionActivityIntentProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) internal object AndroidModule { @Provides - fun provideContext(app: Application): Context = app + fun provideContext(@ApplicationContext context: Context): Context = context @Provides fun toPlayerIntentProvider(): SessionActivityIntentProvider = diff --git a/app-tv/src/main/java/com/droidknights/app2023/tv/di/AndroidModule.kt b/app-tv/src/main/java/com/droidknights/app2023/tv/di/AndroidModule.kt index 1863757d..1e356f74 100644 --- a/app-tv/src/main/java/com/droidknights/app2023/tv/di/AndroidModule.kt +++ b/app-tv/src/main/java/com/droidknights/app2023/tv/di/AndroidModule.kt @@ -7,13 +7,14 @@ import com.droidknights.app2023.tv.misc.SessionActivityIntentProviderImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) internal object AndroidModule { @Provides - fun provideContext(app: Application): Context = app + fun provideContext(@ApplicationContext context: Context): Context = context @Provides fun toPlayerIntentProvider( diff --git a/app-wear-os/src/main/java/com/droidknights/app2023/wear/di/AndroidModule.kt b/app-wear-os/src/main/java/com/droidknights/app2023/wear/di/AndroidModule.kt index a2537e4b..81c485b8 100644 --- a/app-wear-os/src/main/java/com/droidknights/app2023/wear/di/AndroidModule.kt +++ b/app-wear-os/src/main/java/com/droidknights/app2023/wear/di/AndroidModule.kt @@ -7,13 +7,14 @@ import com.droidknights.app2023.wear.misc.SessionActivityIntentProviderImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) internal object AndroidModule { @Provides - fun provideContext(app: Application): Context = app + fun provideContext(@ApplicationContext context: Context): Context = context @Provides fun toPlayerIntentProvider( diff --git a/app/src/main/java/com/droidknights/app2023/di/AndroidModule.kt b/app/src/main/java/com/droidknights/app2023/di/AndroidModule.kt index 4f9be928..5bb85b83 100644 --- a/app/src/main/java/com/droidknights/app2023/di/AndroidModule.kt +++ b/app/src/main/java/com/droidknights/app2023/di/AndroidModule.kt @@ -7,13 +7,14 @@ import com.droidknights.app2023.misc.SessionActivityIntentProviderImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) internal object AndroidModule { @Provides - fun provideContext(app: Application): Context = app + fun provideContext(@ApplicationContext context: Context): Context = context @Provides fun toPlayerIntentProvider( From 99b5a04d984f4719a964c77b2b634bd978082f3c Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 06:51:57 +0900 Subject: [PATCH 31/42] =?UTF-8?q?wear,=20tv=20SessionCardPreview=EB=A5=BC?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20data=EC=97=90=20isBookmarked=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../droidknights/app2023/feature/tvsession/TvSessionCard.kt | 3 ++- .../app2023/feature/wearsession/WearSessionCard.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt index 52f02891..7e876032 100644 --- a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt @@ -156,7 +156,8 @@ private fun SessionCardPreview() { startTime = LocalDateTime(2023, 9, 12, 16, 10, 0), endTime = LocalDateTime(2023, 9, 12, 16, 45, 0), room = Room.TRACK1, - video = Video.None + video = Video.None, + isBookmarked = true, ) KnightsTheme { diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt index 9e9a3582..58a4a0eb 100644 --- a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt @@ -123,7 +123,8 @@ private fun SessionCardPreview() { startTime = LocalDateTime(2023, 9, 12, 16, 10, 0), endTime = LocalDateTime(2023, 9, 12, 16, 45, 0), room = Room.TRACK1, - video = Video.None + video = Video.None, + isBookmarked = true ) KnightsTheme { From 35a16d271b435bdefea26f7588e9fcda7345aa96 Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 06:54:25 +0900 Subject: [PATCH 32/42] =?UTF-8?q?PlaybackStateListener=EC=97=90=EC=84=9C?= =?UTF-8?q?=20player=EB=A5=BC=20attach=ED=95=A0=20=EB=95=8C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=EB=90=98=EB=8A=94=20=EA=B8=B0=EC=A1=B4=20job=EC=9D=84?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app2023/core/playback/playstate/PlaybackStateListener.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt index 40e69e90..31dda48c 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/playstate/PlaybackStateListener.kt @@ -4,6 +4,7 @@ import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.VideoSize import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map @@ -17,12 +18,14 @@ internal class PlaybackStateListener @Inject constructor( ) : Player.Listener { private lateinit var player: Player + private var job: Job? = null fun attachTo(player: Player) { this.player = player player.addListener(this) - scope.launch { + job?.cancel() + job = scope.launch { playbackStateManager.flow .map { it.isPlaying } .collectLatest { isPlaying -> From 9955684301d6f7f4e36e0cc89545a50297a219ad Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 06:56:01 +0900 Subject: [PATCH 33/42] =?UTF-8?q?GetBookmarkedSessionsUseCaseTest=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt b/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt index 8c00b3c5..dbd9ea35 100644 --- a/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt +++ b/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt @@ -5,6 +5,7 @@ import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag +import com.droidknights.app2023.core.model.Video import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.collections.shouldBeSortedWith import io.kotest.matchers.collections.shouldContainAll @@ -60,6 +61,7 @@ internal class GetBookmarkedSessionsUseCaseTest : BehaviorSpec() { room = Room.TRACK1, startTime = LocalDateTime(2023, 10, 5, 11, 0), endTime = LocalDateTime(2023, 10, 5, 11, 50), + video = Video.None, isBookmarked = false ), Session( @@ -72,6 +74,7 @@ internal class GetBookmarkedSessionsUseCaseTest : BehaviorSpec() { room = Room.TRACK1, startTime = LocalDateTime(2023, 10, 5, 9, 0), endTime = LocalDateTime(2023, 10, 5, 9, 50), + video = Video.None, isBookmarked = false ), Session( @@ -84,6 +87,7 @@ internal class GetBookmarkedSessionsUseCaseTest : BehaviorSpec() { room = Room.TRACK1, startTime = LocalDateTime(2023, 10, 5, 10, 0), endTime = LocalDateTime(2023, 10, 5, 10, 50), + video = Video.None, isBookmarked = false ) ) From 8f4377c0380f7695466a7b75b7a27d1d07c02dc7 Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 07:43:15 +0900 Subject: [PATCH 34/42] =?UTF-8?q?=EC=9E=AC=EC=83=9D=20=EC=A4=91=EC=9D=B8?= =?UTF-8?q?=20Session=EC=9D=84=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B4=80=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlaybackPreferencesDataSource를 interface로 분리 - SessionRepository method rename 및 return type 변경 --- .../app2023/core/data/di/DataModule.kt | 6 ++++ .../repository/DefaultSessionRepository.kt | 16 ++++++----- .../core/data/repository/SessionRepository.kt | 4 +-- .../DefaultPlaybackPreferencesDataSource.kt | 28 +++++++++++++++++++ .../PlaybackPreferencesDataSource.kt | 28 +++---------------- .../core/datastore/model/PlaybackData.kt | 5 ---- .../GetCurrentPlayingSessionUseCase.kt | 6 ++-- .../UpdateCurrentPlayingSessionUseCase.kt | 2 +- .../app2023/core/playback/PlayerController.kt | 13 +++++---- .../playback/session/MediaItemProvider.kt | 3 +- .../app2023/feature/player/PlayerViewModel.kt | 2 +- .../feature/wearplayer/WearPlayerViewModel.kt | 2 +- 12 files changed, 64 insertions(+), 51 deletions(-) create mode 100644 core/datastore/src/main/java/com/droidknights/app2023/core/datastore/datasource/DefaultPlaybackPreferencesDataSource.kt delete mode 100644 core/datastore/src/main/java/com/droidknights/app2023/core/datastore/model/PlaybackData.kt diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/di/DataModule.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/di/DataModule.kt index e1891fd2..f6e4f4e3 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/di/DataModule.kt @@ -11,6 +11,7 @@ import com.droidknights.app2023.core.data.repository.DefaultSponsorRepository import com.droidknights.app2023.core.data.repository.SessionRepository import com.droidknights.app2023.core.data.repository.SettingsRepository import com.droidknights.app2023.core.data.repository.SponsorRepository +import com.droidknights.app2023.core.datastore.datasource.DefaultPlaybackPreferencesDataSource import com.droidknights.app2023.core.datastore.datasource.DefaultSessionPreferencesDataSource import com.droidknights.app2023.core.datastore.datasource.PlaybackPreferencesDataSource import com.droidknights.app2023.core.datastore.datasource.SessionPreferencesDataSource @@ -36,6 +37,11 @@ internal abstract class DataModule { repository: DefaultSettingsRepository, ): SettingsRepository + @Binds + abstract fun bindPlaybackLocalDataSource( + dataSource: DefaultPlaybackPreferencesDataSource, + ): PlaybackPreferencesDataSource + @Binds abstract fun bindSessionLocalDataSource( dataSource: DefaultSessionPreferencesDataSource, diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepository.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepository.kt index b53f49d0..b3254956 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepository.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepository.kt @@ -1,23 +1,24 @@ package com.droidknights.app2023.core.data.repository import com.droidknights.app2023.core.data.api.GithubRawApi -import com.droidknights.app2023.core.datastore.datasource.SessionPreferencesDataSource import com.droidknights.app2023.core.data.mapper.toData import com.droidknights.app2023.core.datastore.datasource.PlaybackPreferencesDataSource +import com.droidknights.app2023.core.datastore.datasource.SessionPreferencesDataSource import com.droidknights.app2023.core.model.Session import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject internal class DefaultSessionRepository @Inject constructor( private val githubRawApi: GithubRawApi, - private val preferencesDataSource: PlaybackPreferencesDataSource, + private val playbackDataSource: PlaybackPreferencesDataSource, private val sessionDataSource: SessionPreferencesDataSource, ) : SessionRepository { private var cachedSessions: List = emptyList() + private val currentPlayingSessionId: Flow = playbackDataSource.currentPlayingSessionId private val bookmarkIds: Flow> = sessionDataSource.bookmarkedSession override suspend fun getSessions(): List { @@ -51,10 +52,11 @@ internal class DefaultSessionRepository @Inject constructor( ) } - override fun getCurrentPlayingSessionId(): Flow = - preferencesDataSource.playbackData.map { it?.currentSessionId } + override suspend fun getCurrentPlayingSession(): Session? { + return currentPlayingSessionId.firstOrNull()?.let { getSession(it) } + } - override suspend fun updateCurrentPlayingSessionId(sessionId: String) { - preferencesDataSource.updateCurrentSessionId(sessionId) + override suspend fun updateCurrentPlayingSession(sessionId: String) { + playbackDataSource.updateCurrentPlayingSession(sessionId) } } diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/repository/SessionRepository.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/repository/SessionRepository.kt index 00968be1..b6337bae 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/repository/SessionRepository.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/repository/SessionRepository.kt @@ -13,7 +13,7 @@ interface SessionRepository { suspend fun bookmarkSession(sessionId: String, bookmark: Boolean) - fun getCurrentPlayingSessionId(): Flow + suspend fun getCurrentPlayingSession(): Session? - suspend fun updateCurrentPlayingSessionId(sessionId: String) + suspend fun updateCurrentPlayingSession(sessionId: String) } diff --git a/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/datasource/DefaultPlaybackPreferencesDataSource.kt b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/datasource/DefaultPlaybackPreferencesDataSource.kt new file mode 100644 index 00000000..e817a74d --- /dev/null +++ b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/datasource/DefaultPlaybackPreferencesDataSource.kt @@ -0,0 +1,28 @@ +package com.droidknights.app2023.core.datastore.datasource + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Named + +class DefaultPlaybackPreferencesDataSource @Inject constructor( + @Named("playback") private val dataStore: DataStore +) : PlaybackPreferencesDataSource { + object PreferencesKey { + val CURRENT_SESSION_ID = stringPreferencesKey("CURRENT_SESSION_ID") + } + + override val currentPlayingSessionId: Flow = dataStore.data.map { preferences -> + preferences[PreferencesKey.CURRENT_SESSION_ID] + } + + override suspend fun updateCurrentPlayingSession(sessionId: String) { + dataStore.edit { preferences -> + preferences[PreferencesKey.CURRENT_SESSION_ID] = sessionId + } + } +} diff --git a/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/datasource/PlaybackPreferencesDataSource.kt b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/datasource/PlaybackPreferencesDataSource.kt index 28d15fe2..141a7494 100644 --- a/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/datasource/PlaybackPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/datasource/PlaybackPreferencesDataSource.kt @@ -1,28 +1,8 @@ package com.droidknights.app2023.core.datastore.datasource -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import com.droidknights.app2023.core.datastore.model.PlaybackData -import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Named +import kotlinx.coroutines.flow.Flow -class PlaybackPreferencesDataSource @Inject constructor( - @Named("playback") private val dataStore: DataStore -) { - object PreferencesKey { - val CURRENT_SESSION_ID = stringPreferencesKey("CURRENT_SESSION_ID") - } - - val playbackData = dataStore.data.map { preferences -> - preferences[PreferencesKey.CURRENT_SESSION_ID]?.let(::PlaybackData) - } - - suspend fun updateCurrentSessionId(sessionId: String) { - dataStore.edit { preferences -> - preferences[PreferencesKey.CURRENT_SESSION_ID] = sessionId - } - } +interface PlaybackPreferencesDataSource { + val currentPlayingSessionId: Flow + suspend fun updateCurrentPlayingSession(sessionId: String) } diff --git a/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/model/PlaybackData.kt b/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/model/PlaybackData.kt deleted file mode 100644 index 2dd08f7f..00000000 --- a/core/datastore/src/main/java/com/droidknights/app2023/core/datastore/model/PlaybackData.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.droidknights.app2023.core.datastore.model - -data class PlaybackData( - val currentSessionId: String -) \ No newline at end of file diff --git a/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/GetCurrentPlayingSessionUseCase.kt b/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/GetCurrentPlayingSessionUseCase.kt index ecd0741f..fc29b0fe 100644 --- a/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/GetCurrentPlayingSessionUseCase.kt +++ b/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/GetCurrentPlayingSessionUseCase.kt @@ -1,13 +1,13 @@ package com.droidknights.app2023.core.domain.usecase import com.droidknights.app2023.core.data.repository.SessionRepository -import kotlinx.coroutines.flow.firstOrNull +import com.droidknights.app2023.core.model.Session import javax.inject.Inject class GetCurrentPlayingSessionUseCase @Inject constructor( private val sessionRepository: SessionRepository, ) { - suspend operator fun invoke(): String? { - return sessionRepository.getCurrentPlayingSessionId().firstOrNull() + suspend operator fun invoke(): Session? { + return sessionRepository.getCurrentPlayingSession() } } diff --git a/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/UpdateCurrentPlayingSessionUseCase.kt b/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/UpdateCurrentPlayingSessionUseCase.kt index 775361ed..22d21b4d 100644 --- a/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/UpdateCurrentPlayingSessionUseCase.kt +++ b/core/domain/src/main/java/com/droidknights/app2023/core/domain/usecase/UpdateCurrentPlayingSessionUseCase.kt @@ -8,6 +8,6 @@ class UpdateCurrentPlayingSessionUseCase @Inject constructor( ) { suspend operator fun invoke(sessionId: String) { - return sessionRepository.updateCurrentPlayingSessionId(sessionId) + return sessionRepository.updateCurrentPlayingSession(sessionId) } } diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt index f09fbd02..c57b5ef0 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first import kotlinx.coroutines.guava.asDeferred import kotlinx.coroutines.launch import javax.inject.Inject @@ -84,14 +83,18 @@ class PlayerController @Inject constructor( } private suspend fun maybePrepare(controller: MediaController): Boolean { - val sessionId = sessionRepository.getCurrentPlayingSessionId().first() ?: return false - if (controller.currentSessionId() == sessionId && + val currentPlayingSessionId = sessionRepository.getCurrentPlayingSession()?.id ?: return false + if (controller.currentSessionId() == currentPlayingSessionId && controller.playbackState in listOf(Player.STATE_READY, Player.STATE_BUFFERING) ) { return true } - val session = runCatching { sessionRepository.getSession(sessionId) }.getOrNull() ?: return false - controller.setMediaItem(mediaItemProvider.mediaItem(session)) + val currentPlayingSession = runCatching { + sessionRepository.getSession(currentPlayingSessionId) + } + .getOrNull() + ?: return false + controller.setMediaItem(mediaItemProvider.mediaItem(currentPlayingSession)) controller.prepare() return true } diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt index 5590f08e..14bb75d4 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt @@ -9,7 +9,6 @@ import com.droidknights.app2023.core.data.repository.SessionRepository import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.playback.R -import kotlinx.coroutines.flow.first import javax.inject.Inject class MediaItemProvider @Inject constructor( @@ -147,7 +146,7 @@ class MediaItemProvider @Inject constructor( ) suspend fun currentMediaItemsOrKeynote(): MediaItemsWithStartPosition { - val currentPlayingSessionId = sessionRepository.getCurrentPlayingSessionId().first() ?: "1" + val currentPlayingSessionId = sessionRepository.getCurrentPlayingSession()?.id ?: "1" val session = sessionRepository.getSession(currentPlayingSessionId) return MediaItemsWithStartPosition( diff --git a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerViewModel.kt b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerViewModel.kt index 741170ab..56eb63fe 100644 --- a/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerViewModel.kt +++ b/feature/player/src/main/kotlin/com/droidknights/app2023/feature/player/PlayerViewModel.kt @@ -30,7 +30,7 @@ class PlayerViewModel @Inject constructor( viewModelScope.launch { val sessionId = savedStateHandle.get(PlayerRoute.argumentName) .takeIf { !it.isNullOrBlank() } - ?: getCurrentPlayingSessionUseCase() + ?: getCurrentPlayingSessionUseCase()?.id ?: "1" // 처음부터 재생 updateCurrentPlayingSessionUseCase(sessionId) playerController.play() diff --git a/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerViewModel.kt b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerViewModel.kt index 09b39fcf..22027d30 100644 --- a/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerViewModel.kt +++ b/feature/wear-player/src/main/kotlin/com/droidknights/app2023/feature/wearplayer/WearPlayerViewModel.kt @@ -30,7 +30,7 @@ class WearPlayerViewModel @Inject constructor( viewModelScope.launch { val sessionId = savedStateHandle.get(WearPlayerRoute.argumentName) .takeIf { !it.isNullOrBlank() } - ?: getCurrentPlayingSessionUseCase() + ?: getCurrentPlayingSessionUseCase()?.id ?: "1" // 처음부터 재생 updateCurrentPlayingSessionUseCase(sessionId) playerController.play() From eb2781ba2a5f067facaf2f0d1a6bd7ff08f6117d Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 15:27:54 +0900 Subject: [PATCH 35/42] fix: DefaultSessionRepositoryTest --- .../fake/FakePlaybackPreferencesDataSource.kt | 14 ++++++++ .../DefaultSessionRepositoryTest.kt | 32 +++++++++++++++++-- .../domain/usecase/FakeSessionRepository.kt | 8 +++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 core/data/src/test/java/com/droidknights/app2023/core/data/datastore/fake/FakePlaybackPreferencesDataSource.kt diff --git a/core/data/src/test/java/com/droidknights/app2023/core/data/datastore/fake/FakePlaybackPreferencesDataSource.kt b/core/data/src/test/java/com/droidknights/app2023/core/data/datastore/fake/FakePlaybackPreferencesDataSource.kt new file mode 100644 index 00000000..501e9f32 --- /dev/null +++ b/core/data/src/test/java/com/droidknights/app2023/core/data/datastore/fake/FakePlaybackPreferencesDataSource.kt @@ -0,0 +1,14 @@ +package com.droidknights.app2023.core.data.datastore.fake + +import com.droidknights.app2023.core.datastore.datasource.PlaybackPreferencesDataSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakePlaybackPreferencesDataSource : PlaybackPreferencesDataSource { + private val _currentPlayingSessionId = MutableStateFlow(null) + override val currentPlayingSessionId: Flow = _currentPlayingSessionId + + override suspend fun updateCurrentPlayingSession(sessionId: String) { + _currentPlayingSessionId.value = sessionId + } +} diff --git a/core/data/src/test/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepositoryTest.kt b/core/data/src/test/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepositoryTest.kt index 49b63463..c265b007 100644 --- a/core/data/src/test/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepositoryTest.kt +++ b/core/data/src/test/java/com/droidknights/app2023/core/data/repository/DefaultSessionRepositoryTest.kt @@ -2,10 +2,12 @@ package com.droidknights.app2023.core.data.repository import app.cash.turbine.test import com.droidknights.app2023.core.data.api.fake.FakeGithubRawApi +import com.droidknights.app2023.core.data.datastore.fake.FakePlaybackPreferencesDataSource import com.droidknights.app2023.core.data.datastore.fake.FakeSessionPreferencesDataSource import com.droidknights.app2023.core.model.Level import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.core.model.Video import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import kotlinx.datetime.LocalDateTime @@ -15,7 +17,8 @@ internal class DefaultSessionRepositoryTest : StringSpec() { init { val repository: SessionRepository = DefaultSessionRepository( githubRawApi = FakeGithubRawApi(), - sessionDataSource = FakeSessionPreferencesDataSource() + sessionDataSource = FakeSessionPreferencesDataSource(), + playbackDataSource = FakePlaybackPreferencesDataSource() ) "역직렬화 테스트" { val expected = Session( @@ -28,7 +31,11 @@ internal class DefaultSessionRepositoryTest : StringSpec() { room = Room.ETC, startTime = LocalDateTime(2023, 9, 12, 10, 45), endTime = LocalDateTime(2023, 9, 12, 11, 0), - isBookmarked = false + video = Video( + manifestUrl = "https://workspace.github.io/media-samples/sample/output.mpd", + thumbnailUrl = "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + ), + isBookmarked = false, ) val actual = repository.getSessions() actual.first() shouldBe expected @@ -65,5 +72,26 @@ internal class DefaultSessionRepositoryTest : StringSpec() { awaitItem() shouldBe setOf("1") } } + + "현재 재생 중인 세션 업데이트 테스트" { + repository.getCurrentPlayingSession() shouldBe null + repository.updateCurrentPlayingSession("1") + repository.getCurrentPlayingSession() shouldBe Session( + id = "1", + title = "Keynote", + content = "", + speakers = emptyList(), + level = Level.ETC, + tags = emptyList(), + room = Room.ETC, + startTime = LocalDateTime(2023, 9, 12, 10, 45), + endTime = LocalDateTime(2023, 9, 12, 11, 0), + video = Video( + manifestUrl = "https://workspace.github.io/media-samples/sample/output.mpd", + thumbnailUrl = "https://workspace.github.io/media-samples/sample/thumbnail.jpg" + ), + isBookmarked = false, + ) + } } } diff --git a/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/FakeSessionRepository.kt b/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/FakeSessionRepository.kt index e6c13acd..e2e27189 100644 --- a/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/FakeSessionRepository.kt +++ b/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/FakeSessionRepository.kt @@ -25,4 +25,12 @@ internal class FakeSessionRepository( override suspend fun bookmarkSession(sessionId: String, bookmark: Boolean) { return } + + override suspend fun getCurrentPlayingSession(): Session? { + return null + } + + override suspend fun updateCurrentPlayingSession(sessionId: String) { + return + } } From d905dc49f609ad08f3fa2aff221faa6293284ef4 Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 16:19:09 +0900 Subject: [PATCH 36/42] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20un?= =?UTF-8?q?derscore=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app2023/core/playback/PlayerController.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt index c57b5ef0..769fe641 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt @@ -25,7 +25,7 @@ class PlayerController @Inject constructor( private val mediaItemProvider: MediaItemProvider, ) { - private var _controller: Deferred = newControllerAsync() + private var controllerDeferred: Deferred = newControllerAsync() private fun newControllerAsync() = MediaController .Builder(context, SessionToken(context, ComponentName(context, PlaybackService::class.java))) @@ -33,16 +33,16 @@ class PlayerController @Inject constructor( .asDeferred() @OptIn(ExperimentalCoroutinesApi::class) - private val controller: Deferred + private val activeControllerDeferred: Deferred get() { - if (_controller.isCompleted) { - val completedController = _controller.getCompleted() + if (controllerDeferred.isCompleted) { + val completedController = controllerDeferred.getCompleted() if (!completedController.isConnected) { completedController.release() - _controller = newControllerAsync() + controllerDeferred = newControllerAsync() } } - return _controller + return controllerDeferred } private val scope = CoroutineScope(Dispatchers.Main.immediate) @@ -111,9 +111,9 @@ class PlayerController @Inject constructor( } } - suspend fun awaitConnect(): MediaController? { + private suspend fun awaitConnect(): MediaController? { return try { - controller.await() + activeControllerDeferred.await() } catch (e: Exception) { if (e is CancellationException) throw e null From e788ab605cd858487ffe5a91d7efce26feb4d9b3 Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 16:22:19 +0900 Subject: [PATCH 37/42] =?UTF-8?q?try=20catch=20=EA=B5=AC=EB=AC=B8=EC=9D=84?= =?UTF-8?q?=20runCatching=EC=9C=BC=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../droidknights/app2023/core/playback/PlayerController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt index 769fe641..f579d543 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/PlayerController.kt @@ -112,9 +112,9 @@ class PlayerController @Inject constructor( } private suspend fun awaitConnect(): MediaController? { - return try { + return runCatching { activeControllerDeferred.await() - } catch (e: Exception) { + }.getOrElse { e -> if (e is CancellationException) throw e null } From 649e73c7a3d51d32b04989d865e9ce621a572cb0 Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 16:44:18 +0900 Subject: [PATCH 38/42] =?UTF-8?q?Session=EC=9D=98=20video=EB=A5=BC=20non-n?= =?UTF-8?q?ull=20->=20nullable=20type=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app2023/core/data/mapper/SessionMapper.kt | 18 +++++++++++++----- .../GetBookmarkedSessionsUseCaseTest.kt | 7 +++---- .../droidknights/app2023/core/model/Session.kt | 2 +- .../droidknights/app2023/core/model/Video.kt | 8 +------- .../core/playback/session/MediaItemProvider.kt | 2 +- .../app2023/feature/session/SessionCard.kt | 2 +- .../feature/session/SessionDetailScreen.kt | 6 +++--- .../app2023/feature/tvsession/TvSessionCard.kt | 4 ++-- .../feature/wearsession/WearSessionCard.kt | 4 ++-- 9 files changed, 27 insertions(+), 26 deletions(-) diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt index 05026928..e8bf328d 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/mapper/SessionMapper.kt @@ -22,7 +22,7 @@ internal fun SessionResponse.toData(): Session = Session( room = this.room?.toData() ?: Room.ETC, startTime = this.startTime, endTime = this.endTime, - video = this.video?.toData() ?: Video.None, + video = this.video?.toData(), isBookmarked = false, ) @@ -46,7 +46,15 @@ internal fun SpeakerResponse.toData(): Speaker = Speaker( imageUrl = this.imageUrl ) -internal fun VideoResponse.toData(): Video = Video( - manifestUrl = this.manifestUrl, - thumbnailUrl = this.thumbnailUrl -) +internal fun VideoResponse.toData(): Video? = + if ( + manifestUrl.isNotBlank() && + thumbnailUrl.isNotBlank() + ) { + Video( + manifestUrl = this.manifestUrl, + thumbnailUrl = this.thumbnailUrl + ) + } else { + null + } diff --git a/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt b/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt index dbd9ea35..c95b8eaa 100644 --- a/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt +++ b/core/domain/src/test/java/com/droidknights/app2023/core/domain/usecase/GetBookmarkedSessionsUseCaseTest.kt @@ -5,7 +5,6 @@ import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag -import com.droidknights.app2023.core.model.Video import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.collections.shouldBeSortedWith import io.kotest.matchers.collections.shouldContainAll @@ -61,7 +60,7 @@ internal class GetBookmarkedSessionsUseCaseTest : BehaviorSpec() { room = Room.TRACK1, startTime = LocalDateTime(2023, 10, 5, 11, 0), endTime = LocalDateTime(2023, 10, 5, 11, 50), - video = Video.None, + video = null, isBookmarked = false ), Session( @@ -74,7 +73,7 @@ internal class GetBookmarkedSessionsUseCaseTest : BehaviorSpec() { room = Room.TRACK1, startTime = LocalDateTime(2023, 10, 5, 9, 0), endTime = LocalDateTime(2023, 10, 5, 9, 50), - video = Video.None, + video = null, isBookmarked = false ), Session( @@ -87,7 +86,7 @@ internal class GetBookmarkedSessionsUseCaseTest : BehaviorSpec() { room = Room.TRACK1, startTime = LocalDateTime(2023, 10, 5, 10, 0), endTime = LocalDateTime(2023, 10, 5, 10, 50), - video = Video.None, + video = null, isBookmarked = false ) ) diff --git a/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt b/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt index 87f2c250..8d87ff80 100644 --- a/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt +++ b/core/model/src/main/java/com/droidknights/app2023/core/model/Session.kt @@ -12,6 +12,6 @@ data class Session( val room: Room, val startTime: LocalDateTime, val endTime: LocalDateTime, - val video: Video, + val video: Video?, val isBookmarked: Boolean, ) diff --git a/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt b/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt index 1503f10a..9f666643 100644 --- a/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt +++ b/core/model/src/main/java/com/droidknights/app2023/core/model/Video.kt @@ -3,10 +3,4 @@ package com.droidknights.app2023.core.model data class Video( val manifestUrl: String, val thumbnailUrl: String, -) { - val isReady = manifestUrl.isNotBlank() && thumbnailUrl.isNotBlank() - - companion object { - val None = Video("", "") - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt index 14bb75d4..eae059d1 100644 --- a/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt +++ b/core/playback/src/main/kotlin/com/droidknights/app2023/core/playback/session/MediaItemProvider.kt @@ -137,7 +137,7 @@ class MediaItemProvider @Inject constructor( mediaId = MediaId.Session(session.id), browsable = false, isPlayable = true, - sourceUri = session.video.manifestUrl.takeIf { it.isNotBlank() }?.let(Uri::parse), + sourceUri = session.video?.manifestUrl?.let(Uri::parse), imageUri = session.speakers.firstOrNull() ?.imageUrl.takeIf { !it.isNullOrBlank() } ?.let(Uri::parse) diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt index f3569775..63bcb57d 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt @@ -212,7 +212,7 @@ private fun SessionCardPreview() { startTime = LocalDateTime(2023, 9, 12, 16, 10, 0), endTime = LocalDateTime(2023, 9, 12, 16, 45, 0), room = Room.TRACK1, - video = Video.None, + video = null, isBookmarked = false, ) diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt index 0c281ba6..f7c951b0 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt @@ -182,11 +182,11 @@ private fun SessionDetailContent( modifier = Modifier .padding(top = 16.dp) .fillMaxWidth(), - enabled = session.video.isReady, + enabled = session.video != null, onClick = onPlayButtonClick ) { Text( - if (session.video.isReady) "재생하기" else "영상 미제공 세션" + if (session.video != null) "재생하기" else "영상 미제공 세션" ) } } @@ -336,7 +336,7 @@ private val SampleSessionNoContent = Session( room = Room.TRACK1, startTime = LocalDateTime.parse("2023-09-12T11:00:00.000"), endTime = LocalDateTime.parse("2023-09-12T11:30:00.000"), - video = Video.None, + video = null, isBookmarked = true, ) diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt index 7e876032..7f73dea7 100644 --- a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt @@ -41,7 +41,7 @@ internal fun SessionCard( modifier: Modifier = Modifier, onSessionClick: (Session) -> Unit = { }, ) { - if (session.video.isReady) { + if (session.video != null) { KnightsCard( modifier = modifier, onClick = { onSessionClick(session) } @@ -156,7 +156,7 @@ private fun SessionCardPreview() { startTime = LocalDateTime(2023, 9, 12, 16, 10, 0), endTime = LocalDateTime(2023, 9, 12, 16, 45, 0), room = Room.TRACK1, - video = Video.None, + video = null, isBookmarked = true, ) diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt index 58a4a0eb..79e72800 100644 --- a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt @@ -36,7 +36,7 @@ internal fun SessionCard( modifier: Modifier = Modifier, onSessionClick: (Session) -> Unit = { }, ) { - if (session.video.isReady) { + if (session.video != null) { KnightsCard( modifier = modifier, onClick = { onSessionClick(session) } @@ -123,7 +123,7 @@ private fun SessionCardPreview() { startTime = LocalDateTime(2023, 9, 12, 16, 10, 0), endTime = LocalDateTime(2023, 9, 12, 16, 45, 0), room = Room.TRACK1, - video = Video.None, + video = null, isBookmarked = true ) From 29674eccd2b1e94f098a0221a1eb67b12525a08b Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 16:44:34 +0900 Subject: [PATCH 39/42] =?UTF-8?q?POST=5FNOTIFICATIONS=20permission=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 86180f47..6e7d4191 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + From aec471e15bd8d2a952bf18b69a00499e2c836801 Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 16:45:57 +0900 Subject: [PATCH 40/42] fix lint --- .../java/com/droidknights/app2023/feature/session/SessionCard.kt | 1 - .../com/droidknights/app2023/feature/tvsession/TvSessionCard.kt | 1 - .../droidknights/app2023/feature/wearsession/WearSessionCard.kt | 1 - 3 files changed, 3 deletions(-) diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt index 63bcb57d..1f415863 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt @@ -33,7 +33,6 @@ import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag -import com.droidknights.app2023.core.model.Video import kotlinx.datetime.LocalDateTime @Composable diff --git a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt index 7f73dea7..b2ba61ad 100644 --- a/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt +++ b/feature/tv-session/src/main/kotlin/com/droidknights/app2023/feature/tvsession/TvSessionCard.kt @@ -31,7 +31,6 @@ import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag -import com.droidknights.app2023.core.model.Video import com.droidknights.app2023.feature.tvsession.component.KnightsCard import kotlinx.datetime.LocalDateTime diff --git a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt index 79e72800..99e629af 100644 --- a/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt +++ b/feature/wear-session/src/main/kotlin/com/droidknights/app2023/feature/wearsession/WearSessionCard.kt @@ -27,7 +27,6 @@ import com.droidknights.app2023.core.model.Room import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag -import com.droidknights.app2023.core.model.Video import kotlinx.datetime.LocalDateTime @Composable From 2263e4ca8b106567356fd78733c7530a5785f456 Mon Sep 17 00:00:00 2001 From: Kimin Ryu Date: Wed, 11 Oct 2023 17:29:13 +0900 Subject: [PATCH 41/42] =?UTF-8?q?README=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index 1cd6ad0f..20be6c5b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,54 @@ +banner + +# DroidKnights2023 App with media3 + +2023년 9월 12일 드로이드나이츠에서 발표한 에서 소개한 데모 앱을 공개합니다. + +## 발표 자료 +https://speakerdeck.com/workspace93/jetpack-media3ro-joheun-kontenceu-sobi-gyeongheom-guhyeonhagi + +## Guide + +### Emulator 만들기 + +각 Configuration에 맞는 Emulator를 Android Studio Device Manager에서 생성. + +image + +### Desktop Head Unit Emulator 만들기 (Android Auto) + +공식 [가이드](https://developer.android.com/training/cars/testing/dhu)를 따라 `Desktop Head Unit Emulator(DHU)`를 설치. 모바일 에뮬레이터 또는 실기기가 연결된 상태에서 DHU 실행하면 Android Auto 활성화 + +image + +### Run Configurations + +실행 해보고 싶은 것과 Emulator를 고른 뒤 `Run` + +image + +- app (통상적인 모바일 앱, Android Auto) +- app-wear-os (워치 앱) +- app-tv (Android TV 앱) +- app-automotive (Android Automotive 앱) + +## Resources +### Youtube +- [Google I/O 2014 - Building great multi-media experiences on Android (18:29 ~)](https://www.youtube.com/watch?v=92fgcUNCHic&t=1108s) +- [Android Dev Summit 2021 - What’s next for AndroidX Media and ExoPlayer](https://www.youtube.com/watch?v=sTIBDcyCmCg) +### Android Developers - Media3 +- [Introducing Jetpack Media3](https://android-developers.googleblog.com/2021/10/jetpack-media3.html) +- [Media3 is ready to play!](https://android-developers.googleblog.com/2023/03/media3-is-ready-to-play.html) +- [Introduction to Jetpack Media3](https://developer.android.com/guide/topics/media/media3) +### Android Developers - Wear OS, TV, Auto +- [Using Jetpack Compose on Wear OS](https://developer.android.com/training/wearables/compose) +- [Use Jetpack Compose on Android TV](https://developer.android.com/training/tv/playback/compose) +- [Build media apps for cars](https://developer.android.com/training/cars/media) +### Github +- [Media3 Github](https://github.com/androidx/media) + +
+ DroidKnights2023 App ReadMe 원문 banner # DroidKnights2023 App @@ -101,3 +152,5 @@ - GitHub : [Contributors](https://github.com/droidknights/DroidKnights2023_App/graphs/contributors) - Designer : Eunbi Ko - Maintainer : [laco-dev](https://github.com/laco-dev), [wisemuji](https://github.com/wisemuji) + +
From bb6dc766150016a5c37cd3cdf2adb3c9009a1cc4 Mon Sep 17 00:00:00 2001 From: workspace Date: Wed, 11 Oct 2023 18:39:54 +0900 Subject: [PATCH 42/42] =?UTF-8?q?request=20endpoint=EB=A5=BC=20folk=20repo?= =?UTF-8?q?sitory=20url=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/droidknights/app2023/core/data/api/GithubRawApi.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt b/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt index 43823ffc..fd15fcd8 100644 --- a/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt +++ b/core/data/src/main/java/com/droidknights/app2023/core/data/api/GithubRawApi.kt @@ -6,9 +6,9 @@ import retrofit2.http.GET internal interface GithubRawApi { - @GET("/droidknights/DroidKnights2023_App/reference-media3/core/data/src/main/assets/sponsors.json") + @GET("/workspace/DroidKnights2023-app-with-media3/media3-main/core/data/src/main/assets/sponsors.json") suspend fun getSponsors(): List - @GET("/droidknights/DroidKnights2023_App/reference-media3/core/data/src/main/assets/sessions.json") + @GET("/workspace/DroidKnights2023-app-with-media3/media3-main/core/data/src/main/assets/sessions.json") suspend fun getSessions(): List }