diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/MediaDatabase.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/MediaDatabase.kt new file mode 100644 index 0000000000..650b44c1ba --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/MediaDatabase.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.google.android.horologist.mediasample.data.database.dao.MediaDownloadDao +import com.google.android.horologist.mediasample.data.database.dao.PlaylistDao +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntity +import com.google.android.horologist.mediasample.data.database.model.MediaEntity +import com.google.android.horologist.mediasample.data.database.model.PlaylistEntity +import com.google.android.horologist.mediasample.data.database.model.PlaylistMediaEntity + +@Database( + entities = [ + MediaDownloadEntity::class, + MediaEntity::class, + PlaylistEntity::class, + PlaylistMediaEntity::class, + ], + version = 1, + exportSchema = false +) +abstract class MediaDatabase : RoomDatabase() { + + abstract fun mediaDownloadDao(): MediaDownloadDao + + abstract fun playlistDao(): PlaylistDao +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/PlaylistDownloadDao.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/MediaDownloadDao.kt similarity index 63% rename from media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/PlaylistDownloadDao.kt rename to media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/MediaDownloadDao.kt index 7847af5945..515bfa8e0a 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/PlaylistDownloadDao.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/MediaDownloadDao.kt @@ -20,38 +20,38 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import com.google.android.horologist.mediasample.data.database.model.PlaylistDownloadEntity -import com.google.android.horologist.mediasample.data.database.model.PlaylistDownloadState +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntity +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntityStatus import kotlinx.coroutines.flow.Flow @Dao -interface PlaylistDownloadDao { +interface MediaDownloadDao { @Query( value = """ - SELECT * FROM playlist_download - WHERE playlistId = :playlistId + SELECT * FROM mediadownloadentity + WHERE mediaId in (:mediaIds) """ ) - fun getStream(playlistId: String): Flow> + fun getList(mediaIds: List): Flow> @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insert(playlistDownloadEntities: PlaylistDownloadEntity): Long + suspend fun insert(mediaDownloadEntity: MediaDownloadEntity): Long @Query( - value = """ - DELETE FROM playlist_download - WHERE mediaItemId = :mediaItemId """ + UPDATE mediadownloadentity + SET status = :status + WHERE mediaId = :mediaId + """ ) - suspend fun deleteByMediaItemId(mediaItemId: String) + suspend fun updateStatus(mediaId: String, status: MediaDownloadEntityStatus) @Query( - value = """ - UPDATE playlist_download - SET status = :status - WHERE mediaItemId = :mediaItemId + """ + DELETE FROM mediadownloadentity + WHERE mediaId = :mediaId """ ) - suspend fun updateStatusByMediaItemId(mediaItemId: String, status: PlaylistDownloadState) + suspend fun delete(mediaId: String) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/PlaylistDao.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/PlaylistDao.kt new file mode 100644 index 0000000000..a3e96ad8cf --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/dao/PlaylistDao.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.google.android.horologist.mediasample.data.database.model.MediaEntity +import com.google.android.horologist.mediasample.data.database.model.PlaylistEntity +import com.google.android.horologist.mediasample.data.database.model.PlaylistMediaEntity +import com.google.android.horologist.mediasample.data.database.model.PopulatedPlaylist +import kotlinx.coroutines.flow.Flow + +@Dao +interface PlaylistDao { + + @Query( + value = """ + SELECT COUNT(*) FROM playlistentity + """ + ) + suspend fun getCount(): Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert( + playlistEntity: PlaylistEntity, + mediaEntityList: List, + playlistMediaEntityList: List + ) + + @Transaction + @Query( + value = """ + SELECT * FROM playlistentity + WHERE playlistId = :playlistId + """ + ) + fun getPopulated(playlistId: String): Flow + + @Transaction + @Query( + value = """ + SELECT * FROM playlistentity + """ + ) + fun getAllPopulated(): Flow> +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadStateMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/MediaDownloadEntityStatusMapper.kt similarity index 56% rename from media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadStateMapper.kt rename to media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/MediaDownloadEntityStatusMapper.kt index 0fcc000095..f312c615f0 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadStateMapper.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/MediaDownloadEntityStatusMapper.kt @@ -14,17 +14,19 @@ * limitations under the License. */ -package com.google.android.horologist.mediasample.data.mapper +package com.google.android.horologist.mediasample.data.database.mapper import androidx.media3.exoplayer.offline.Download -import com.google.android.horologist.mediasample.data.database.model.PlaylistDownloadState +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntityStatus -object PlaylistDownloadStateMapper { +object MediaDownloadEntityStatusMapper { - fun map(@Download.State state: Int): PlaylistDownloadState = when (state) { - Download.STATE_QUEUED, Download.STATE_DOWNLOADING, Download.STATE_RESTARTING -> PlaylistDownloadState.Downloading - Download.STATE_COMPLETED -> PlaylistDownloadState.Downloaded - Download.STATE_FAILED -> PlaylistDownloadState.Failed - else -> PlaylistDownloadState.NotDownloaded + fun map(@Download.State state: Int): MediaDownloadEntityStatus = when (state) { + Download.STATE_QUEUED, + Download.STATE_DOWNLOADING, + Download.STATE_RESTARTING -> MediaDownloadEntityStatus.Downloading + Download.STATE_COMPLETED -> MediaDownloadEntityStatus.Downloaded + Download.STATE_FAILED -> MediaDownloadEntityStatus.Failed + else -> MediaDownloadEntityStatus.NotDownloaded } } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/DownloadDatabase.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/MediaEntityMapper.kt similarity index 60% rename from media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/DownloadDatabase.kt rename to media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/MediaEntityMapper.kt index 02d2e7a8e8..fab42d1cf4 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/DownloadDatabase.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/MediaEntityMapper.kt @@ -14,19 +14,18 @@ * limitations under the License. */ -package com.google.android.horologist.mediasample.data.database +package com.google.android.horologist.mediasample.data.database.mapper -import androidx.room.Database -import androidx.room.RoomDatabase -import com.google.android.horologist.mediasample.data.database.dao.PlaylistDownloadDao -import com.google.android.horologist.mediasample.data.database.model.PlaylistDownloadEntity +import com.google.android.horologist.media.model.Media +import com.google.android.horologist.mediasample.data.database.model.MediaEntity -@Database( - entities = [PlaylistDownloadEntity::class], - version = 1, - exportSchema = false -) -abstract class DownloadDatabase : RoomDatabase() { +object MediaEntityMapper { - abstract fun playlistDownloadDao(): PlaylistDownloadDao + fun map(media: Media): MediaEntity = MediaEntity( + mediaId = media.id, + mediaUrl = media.uri, + artworkUrl = media.artworkUri ?: "", + title = media.title, + artist = media.artist, + ) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadStatusMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/PlaylistEntityMapper.kt similarity index 56% rename from media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadStatusMapper.kt rename to media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/PlaylistEntityMapper.kt index 94d50c75f2..16e4b43dc0 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadStatusMapper.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/PlaylistEntityMapper.kt @@ -14,17 +14,16 @@ * limitations under the License. */ -package com.google.android.horologist.mediasample.data.mapper +package com.google.android.horologist.mediasample.data.database.mapper -import com.google.android.horologist.mediasample.data.database.model.PlaylistDownloadState -import com.google.android.horologist.mediasample.domain.model.PlaylistDownload +import com.google.android.horologist.mediasample.data.database.model.PlaylistEntity +import com.google.android.horologist.mediasample.domain.model.Playlist -object PlaylistDownloadStatusMapper { +object PlaylistEntityMapper { - fun map(playlistDownloadState: PlaylistDownloadState): PlaylistDownload.Status = when (playlistDownloadState) { - PlaylistDownloadState.NotDownloaded -> PlaylistDownload.Status.Idle - PlaylistDownloadState.Downloading -> PlaylistDownload.Status.InProgress - PlaylistDownloadState.Downloaded -> PlaylistDownload.Status.Completed - PlaylistDownloadState.Failed -> PlaylistDownload.Status.Idle - } + fun map(playlist: Playlist): PlaylistEntity = PlaylistEntity( + playlistId = playlist.id, + name = playlist.name, + artworkUri = playlist.artworkUri, + ) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/PlaylistMediaEntityMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/PlaylistMediaEntityMapper.kt new file mode 100644 index 0000000000..c4ab158f43 --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/mapper/PlaylistMediaEntityMapper.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.database.mapper + +import com.google.android.horologist.media.model.Media +import com.google.android.horologist.mediasample.data.database.model.PlaylistMediaEntity +import com.google.android.horologist.mediasample.domain.model.Playlist + +object PlaylistMediaEntityMapper { + + fun map(playlist: Playlist, media: Media) = PlaylistMediaEntity( + playlistId = playlist.id, + mediaId = media.id + ) +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/MediaDownloadEntity.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/MediaDownloadEntity.kt new file mode 100644 index 0000000000..5f8f4bdb38 --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/MediaDownloadEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.database.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class MediaDownloadEntity( + @PrimaryKey val mediaId: String, + val status: MediaDownloadEntityStatus, +) + +enum class MediaDownloadEntityStatus { + NotDownloaded, Downloading, Downloaded, Failed +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/MediaEntity.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/MediaEntity.kt new file mode 100644 index 0000000000..b0f4908aa4 --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/MediaEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.database.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class MediaEntity( + @PrimaryKey val mediaId: String, + @ColumnInfo val mediaUrl: String, + @ColumnInfo val artworkUrl: String, + @ColumnInfo val title: String?, + @ColumnInfo val artist: String?, +) diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistEntity.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistEntity.kt new file mode 100644 index 0000000000..f760dbb8ee --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistEntity.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.database.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class PlaylistEntity( + @PrimaryKey val playlistId: String, + val name: String, + val artworkUri: String?, +) diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistDownloadEntity.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistMediaEntity.kt similarity index 72% rename from media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistDownloadEntity.kt rename to media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistMediaEntity.kt index 33e5e38c3f..d0b313c8ca 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistDownloadEntity.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PlaylistMediaEntity.kt @@ -17,20 +17,15 @@ package com.google.android.horologist.mediasample.data.database.model import androidx.room.Entity +import androidx.room.Index @Entity( - tableName = "playlist_download", - primaryKeys = ["playlistId", "mediaItemId"] + primaryKeys = ["playlistId", "mediaId"], + indices = [ + Index(value = ["playlistId"]), + ] ) -data class PlaylistDownloadEntity( +data class PlaylistMediaEntity( val playlistId: String, - val mediaItemId: String, - val status: PlaylistDownloadState, + val mediaId: String, ) - -enum class PlaylistDownloadState { - NotDownloaded, - Downloading, - Downloaded, - Failed -} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PopulatedPlaylist.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PopulatedPlaylist.kt new file mode 100644 index 0000000000..03f027fc12 --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/database/model/PopulatedPlaylist.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.database.model + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation + +/** + * [PlaylistEntity] populated with a list of [MediaEntity]. + */ +data class PopulatedPlaylist( + @Embedded val playlist: PlaylistEntity, + @Relation( + parentColumn = "playlistId", + entityColumn = "mediaId", + associateBy = Junction( + PlaylistMediaEntity::class, + parentColumn = "playlistId", + entityColumn = "mediaId", + ) + ) + val mediaList: List +) diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/MediaDownloadLocalDataSource.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/MediaDownloadLocalDataSource.kt new file mode 100644 index 0000000000..0feb458908 --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/MediaDownloadLocalDataSource.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.datasource + +import com.google.android.horologist.mediasample.data.database.dao.MediaDownloadDao +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntity +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntityStatus +import kotlinx.coroutines.flow.Flow + +class MediaDownloadLocalDataSource( + private val mediaDownloadDao: MediaDownloadDao, +) { + + fun get(mediaIds: List): Flow> = + mediaDownloadDao.getList(mediaIds) + + suspend fun add(mediaId: String) { + mediaDownloadDao.insert( + MediaDownloadEntity( + mediaId = mediaId, + status = MediaDownloadEntityStatus.NotDownloaded + ) + ) + } + + suspend fun delete(mediaId: String) { + mediaDownloadDao.delete(mediaId) + } + + suspend fun updateStatus(mediaId: String, status: MediaDownloadEntityStatus) { + mediaDownloadDao.updateStatus(mediaId = mediaId, status = status) + } +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistDownloadLocalDataSource.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistLocalDataSource.kt similarity index 52% rename from media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistDownloadLocalDataSource.kt rename to media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistLocalDataSource.kt index 80a0e1baeb..a3e1931fa8 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistDownloadLocalDataSource.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistLocalDataSource.kt @@ -16,30 +16,31 @@ package com.google.android.horologist.mediasample.data.datasource -import com.google.android.horologist.mediasample.data.database.dao.PlaylistDownloadDao -import com.google.android.horologist.mediasample.data.database.model.PlaylistDownloadEntity -import com.google.android.horologist.mediasample.data.database.model.PlaylistDownloadState -import com.google.android.horologist.mediasample.data.mapper.PlaylistDownloadMapper +import com.google.android.horologist.mediasample.data.database.dao.PlaylistDao +import com.google.android.horologist.mediasample.data.database.mapper.MediaEntityMapper +import com.google.android.horologist.mediasample.data.database.mapper.PlaylistEntityMapper +import com.google.android.horologist.mediasample.data.database.mapper.PlaylistMediaEntityMapper +import com.google.android.horologist.mediasample.data.database.model.PopulatedPlaylist import com.google.android.horologist.mediasample.domain.model.Playlist -import com.google.android.horologist.mediasample.domain.model.PlaylistDownload import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -class PlaylistDownloadLocalDataSource( - private val playlistDownloadDao: PlaylistDownloadDao, +class PlaylistLocalDataSource( + private val playlistDao: PlaylistDao, ) { + suspend fun isEmpty(): Boolean = playlistDao.getCount() == 0 - fun get(playlist: Playlist): Flow = - playlistDownloadDao.getStream(playlistId = playlist.id) - .map { PlaylistDownloadMapper.map(playlist, it) } - - suspend fun add(playlistId: String, mediaItemId: String) { - playlistDownloadDao.insert( - PlaylistDownloadEntity( - playlistId = playlistId, - mediaItemId = mediaItemId, - status = PlaylistDownloadState.NotDownloaded + suspend fun insert(playlists: List) { + playlists.forEach { playlist -> + playlistDao.insert( + PlaylistEntityMapper.map(playlist), + playlist.mediaList.map(MediaEntityMapper::map), + playlist.mediaList.map { PlaylistMediaEntityMapper.map(playlist, it) } ) - ) + } } + + fun getPopulated(playlistId: String): Flow = + playlistDao.getPopulated(playlistId) + + fun getAllPopulated(): Flow> = playlistDao.getAllPopulated() } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistRemoteDataSource.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistRemoteDataSource.kt index 14ffdd2e8e..630224c092 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistRemoteDataSource.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/datasource/PlaylistRemoteDataSource.kt @@ -16,30 +16,19 @@ package com.google.android.horologist.mediasample.data.datasource -import com.google.android.horologist.media3.logging.ErrorReporter -import com.google.android.horologist.mediasample.R import com.google.android.horologist.mediasample.data.api.UampService -import com.google.android.horologist.mediasample.data.mapper.PlaylistMapper -import com.google.android.horologist.mediasample.domain.model.Playlist +import com.google.android.horologist.mediasample.data.api.model.CatalogApiModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import java.io.IOException class PlaylistRemoteDataSource( private val ioDispatcher: CoroutineDispatcher, private val uampService: UampService, - private val errorReporter: ErrorReporter ) { - fun getPlaylists(): Flow>> = flow { - val result = try { - Result.success(PlaylistMapper.map(uampService.catalog())) - } catch (ioe: IOException) { - errorReporter.showMessage(R.string.horologist_sample_network_error) - Result.failure(ioe) - } - emit(result) + fun getPlaylists(): Flow = flow { + emit(uampService.catalog()) }.flowOn(ioDispatcher) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaDownloadMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaDownloadMapper.kt new file mode 100644 index 0000000000..57ccad2278 --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaDownloadMapper.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.mapper + +import com.google.android.horologist.media.model.Media +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntity +import com.google.android.horologist.mediasample.domain.model.MediaDownload +import com.google.android.horologist.mediasample.domain.model.Playlist + +object MediaDownloadMapper { + + fun map(media: Media, mediaDownloadEntity: MediaDownloadEntity): MediaDownload = MediaDownload( + media = media, + status = MediaDownloadStatusMapper.map(mediaDownloadEntity.status) + ) + + fun map( + playlist: Playlist, + mediaDownloadEntityList: List + ): List = + playlist.mediaList.map { media -> + mediaDownloadEntityList.find { it.mediaId == media.id }?.let { mediaDownloadEntity -> + map(media = media, mediaDownloadEntity = mediaDownloadEntity) + } ?: MediaDownload( + media = media, + status = MediaDownload.Status.Idle + ) + } +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaDownloadStatusMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaDownloadStatusMapper.kt new file mode 100644 index 0000000000..0033f80c61 --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaDownloadStatusMapper.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.data.mapper + +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntityStatus +import com.google.android.horologist.mediasample.domain.model.MediaDownload + +object MediaDownloadStatusMapper { + + fun map(mediaDownloadEntityStatus: MediaDownloadEntityStatus): MediaDownload.Status = when (mediaDownloadEntityStatus) { + MediaDownloadEntityStatus.NotDownloaded -> MediaDownload.Status.Idle + MediaDownloadEntityStatus.Downloading -> MediaDownload.Status.InProgress + MediaDownloadEntityStatus.Downloaded -> MediaDownload.Status.Completed + MediaDownloadEntityStatus.Failed -> MediaDownload.Status.Idle + } +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaMapper.kt index e0f886f3c8..86baf9a0b5 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaMapper.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/MediaMapper.kt @@ -18,9 +18,10 @@ package com.google.android.horologist.mediasample.data.mapper import com.google.android.horologist.media.model.Media import com.google.android.horologist.mediasample.data.api.model.MusicApiModel +import com.google.android.horologist.mediasample.data.database.model.MediaEntity /** - * Maps a [MusicApiModel] into [Media]. + * Mappers for [Media]. */ object MediaMapper { @@ -32,5 +33,11 @@ object MediaMapper { artworkUri = musicApiModel.image ) - fun map(musicApiModels: List): List = musicApiModels.map(::map) + fun map(media: MediaEntity): Media = Media( + id = media.mediaId, + uri = media.mediaUrl, + title = media.title ?: "", + artist = media.artist ?: "", + artworkUri = media.artworkUrl + ) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadMapper.kt index 014ceeaacd..e81397525a 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadMapper.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistDownloadMapper.kt @@ -16,26 +16,20 @@ package com.google.android.horologist.mediasample.data.mapper -import com.google.android.horologist.mediasample.data.database.model.PlaylistDownloadEntity -import com.google.android.horologist.mediasample.domain.model.Playlist +import com.google.android.horologist.mediasample.data.database.model.MediaDownloadEntity +import com.google.android.horologist.mediasample.data.database.model.PopulatedPlaylist import com.google.android.horologist.mediasample.domain.model.PlaylistDownload object PlaylistDownloadMapper { fun map( - playlist: Playlist, - playlistDownloadEntityList: List - ): PlaylistDownload = PlaylistDownload( - playlist = playlist, - buildList { - playlist.mediaList.forEach { media -> - val status = - playlistDownloadEntityList.firstOrNull { it.mediaItemId == media.id } - ?.let { PlaylistDownloadStatusMapper.map(it.status) } - ?: PlaylistDownload.Status.Idle - - add(Pair(media, status)) - } - } - ) + populatedPlaylist: PopulatedPlaylist, + mediaDownloadEntity: List + ): PlaylistDownload { + val playlist = PlaylistMapper.map(populatedPlaylist) + return PlaylistDownload( + playlist = playlist, + mediaList = MediaDownloadMapper.map(playlist, mediaDownloadEntity) + ) + } } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistMapper.kt index 8a52578bac..bd4c2a09fd 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistMapper.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/mapper/PlaylistMapper.kt @@ -16,7 +16,10 @@ package com.google.android.horologist.mediasample.data.mapper +import com.google.android.horologist.media.model.Media import com.google.android.horologist.mediasample.data.api.model.CatalogApiModel +import com.google.android.horologist.mediasample.data.database.model.PlaylistEntity +import com.google.android.horologist.mediasample.data.database.model.PopulatedPlaylist import com.google.android.horologist.mediasample.domain.model.Playlist /** @@ -31,11 +34,26 @@ object PlaylistMapper { Playlist( id = sanitize(entry.key), name = entry.key, - mediaList = MediaMapper.map(entry.value), - artworkUri = entry.value.firstOrNull()?.image + artworkUri = entry.value.firstOrNull()?.image, + mediaList = entry.value.map(MediaMapper::map) ) } + fun map(populatedPlaylist: PopulatedPlaylist): Playlist = + Playlist( + id = populatedPlaylist.playlist.playlistId, + name = populatedPlaylist.playlist.name, + artworkUri = populatedPlaylist.playlist.artworkUri, + mediaList = populatedPlaylist.mediaList.map(MediaMapper::map) + ) + + fun map(playlistEntity: PlaylistEntity, mediaList: List): Playlist = Playlist( + id = playlistEntity.playlistId, + name = playlistEntity.name, + artworkUri = playlistEntity.artworkUri, + mediaList = mediaList + ) + private fun sanitize(it: String): String { return it.replace("[^A-Za-z]".toRegex(), "") } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/repository/PlaylistDownloadRepositoryImpl.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/repository/PlaylistDownloadRepositoryImpl.kt index 42b0a1f70f..58b308b93c 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/repository/PlaylistDownloadRepositoryImpl.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/repository/PlaylistDownloadRepositoryImpl.kt @@ -18,32 +18,47 @@ package com.google.android.horologist.mediasample.data.repository import androidx.core.net.toUri import com.google.android.horologist.mediasample.data.datasource.Media3DownloadDataSource -import com.google.android.horologist.mediasample.data.datasource.PlaylistDownloadLocalDataSource +import com.google.android.horologist.mediasample.data.datasource.MediaDownloadLocalDataSource +import com.google.android.horologist.mediasample.data.datasource.PlaylistLocalDataSource +import com.google.android.horologist.mediasample.data.mapper.PlaylistDownloadMapper import com.google.android.horologist.mediasample.domain.PlaylistDownloadRepository import com.google.android.horologist.mediasample.domain.model.Playlist import com.google.android.horologist.mediasample.domain.model.PlaylistDownload import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch class PlaylistDownloadRepositoryImpl( private val coroutineScope: CoroutineScope, - private val playlistDownloadLocalDataSource: PlaylistDownloadLocalDataSource, + private val playlistLocalDataSource: PlaylistLocalDataSource, + private val mediaDownloadLocalDataSource: MediaDownloadLocalDataSource, private val media3DownloadDataSource: Media3DownloadDataSource, ) : PlaylistDownloadRepository { - override fun get(playlist: Playlist): Flow = - playlistDownloadLocalDataSource.get(playlist) + @OptIn(FlowPreview::class) + override fun get(playlistId: String): Flow = + playlistLocalDataSource.getPopulated(playlistId).flatMapMerge { populatedPlaylist -> + combine( + flowOf(populatedPlaylist), + mediaDownloadLocalDataSource.get( + populatedPlaylist.mediaList + .map { it.mediaId }.toList() + ) + ) { _, mediaDownloadList -> + PlaylistDownloadMapper.map(populatedPlaylist, mediaDownloadList) + } + } override fun download(playlist: Playlist) { coroutineScope.launch { - playlist.mediaList.forEach { - playlistDownloadLocalDataSource.add( - playlistId = playlist.id, - mediaItemId = it.id, - ) + playlist.mediaList.forEach { media -> + mediaDownloadLocalDataSource.add(mediaId = media.id) - media3DownloadDataSource.download(it.id, it.uri.toUri()) + media3DownloadDataSource.download(media.id, media.uri.toUri()) } } } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/repository/PlaylistRepositoryImpl.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/repository/PlaylistRepositoryImpl.kt index 41f9b583c3..558b55bce4 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/repository/PlaylistRepositoryImpl.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/repository/PlaylistRepositoryImpl.kt @@ -16,29 +16,34 @@ package com.google.android.horologist.mediasample.data.repository +import com.google.android.horologist.mediasample.data.datasource.PlaylistLocalDataSource import com.google.android.horologist.mediasample.data.datasource.PlaylistRemoteDataSource +import com.google.android.horologist.mediasample.data.mapper.PlaylistMapper import com.google.android.horologist.mediasample.domain.PlaylistRepository import com.google.android.horologist.mediasample.domain.model.Playlist -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach class PlaylistRepositoryImpl( - playlistRemoteDataSource: PlaylistRemoteDataSource, - coroutineScope: CoroutineScope + private val playlistLocalDataSource: PlaylistLocalDataSource, + private val playlistRemoteDataSource: PlaylistRemoteDataSource, ) : PlaylistRepository { - // temporary implementation of cache - private val playlistCache = playlistRemoteDataSource.getPlaylists().shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(5000), - replay = 1 - ) - - override suspend fun getPlaylist(id: String): Playlist? = - playlistCache.first().getOrNull()?.firstOrNull { it.id == id } - - override fun getPlaylists(): Flow>> = playlistCache + override fun getAllPopulated(): Flow> = flow { + emitAll( + if (playlistLocalDataSource.isEmpty()) { + playlistRemoteDataSource.getPlaylists() + .map(PlaylistMapper::map) + .onEach(playlistLocalDataSource::insert) + } else { + playlistLocalDataSource.getAllPopulated() + .map { list -> + buildList { list.forEach { add(PlaylistMapper.map(it)) } } + } + } + ) + } } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/service/download/DownloadManagerListener.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/service/download/DownloadManagerListener.kt index 52646394fd..74c7b4fee7 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/data/service/download/DownloadManagerListener.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/data/service/download/DownloadManagerListener.kt @@ -18,15 +18,15 @@ package com.google.android.horologist.mediasample.data.service.download import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.DownloadManager -import com.google.android.horologist.mediasample.data.database.dao.PlaylistDownloadDao -import com.google.android.horologist.mediasample.data.mapper.PlaylistDownloadStateMapper +import com.google.android.horologist.mediasample.data.database.mapper.MediaDownloadEntityStatusMapper +import com.google.android.horologist.mediasample.data.datasource.MediaDownloadLocalDataSource import com.google.android.horologist.mediasample.di.annotation.DownloadFeature import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class DownloadManagerListener( @DownloadFeature private val coroutineScope: CoroutineScope, - private val playlistDownloadDao: PlaylistDownloadDao, + private val mediaDownloadLocalDataSource: MediaDownloadLocalDataSource, ) : DownloadManager.Listener { override fun onDownloadChanged( @@ -35,16 +35,16 @@ class DownloadManagerListener( finalException: Exception? ) { coroutineScope.launch { - val mediaItemId = download.request.id - val status = PlaylistDownloadStateMapper.map(download.state) - playlistDownloadDao.updateStatusByMediaItemId(mediaItemId, status) + val mediaId = download.request.id + val status = MediaDownloadEntityStatusMapper.map(download.state) + mediaDownloadLocalDataSource.updateStatus(mediaId, status) } } override fun onDownloadRemoved(downloadManager: DownloadManager, download: Download) { coroutineScope.launch { val mediaItemId = download.request.id - playlistDownloadDao.deleteByMediaItemId(mediaItemId) + mediaDownloadLocalDataSource.delete(mediaItemId) } } } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DataModule.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DataModule.kt new file mode 100644 index 0000000000..661ba7a3f9 --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DataModule.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.media3.exoplayer.offline.DownloadIndex +import com.google.android.horologist.mediasample.data.api.UampService +import com.google.android.horologist.mediasample.data.database.dao.MediaDownloadDao +import com.google.android.horologist.mediasample.data.database.dao.PlaylistDao +import com.google.android.horologist.mediasample.data.datasource.Media3DownloadDataSource +import com.google.android.horologist.mediasample.data.datasource.MediaDownloadLocalDataSource +import com.google.android.horologist.mediasample.data.datasource.PlaylistLocalDataSource +import com.google.android.horologist.mediasample.data.datasource.PlaylistRemoteDataSource +import com.google.android.horologist.mediasample.data.repository.PlaylistDownloadRepositoryImpl +import com.google.android.horologist.mediasample.data.repository.PlaylistRepositoryImpl +import com.google.android.horologist.mediasample.data.service.download.MediaDownloadService +import com.google.android.horologist.mediasample.domain.PlaylistDownloadRepository +import com.google.android.horologist.mediasample.domain.PlaylistRepository +import com.google.android.horologist.mediasample.domain.SettingsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DataModule { + + @Singleton + @Provides + fun playlistDownloadRepository( + @ForApplicationScope coroutineScope: CoroutineScope, + playlistLocalDataSource: PlaylistLocalDataSource, + mediaDownloadLocalDataSource: MediaDownloadLocalDataSource, + media3DownloadDataSource: Media3DownloadDataSource + ): PlaylistDownloadRepository = + PlaylistDownloadRepositoryImpl( + coroutineScope, + playlistLocalDataSource, + mediaDownloadLocalDataSource, + media3DownloadDataSource + ) + + @Singleton + @Provides + fun playlistRepository( + playlistDownloadLocalDataSource: PlaylistLocalDataSource, + playlistRemoteDataSource: PlaylistRemoteDataSource, + ): PlaylistRepository = + PlaylistRepositoryImpl(playlistDownloadLocalDataSource, playlistRemoteDataSource) + + @Singleton + @Provides + fun settingsRepository( + prefsDataStore: DataStore + ) = + SettingsRepository(prefsDataStore) + + @Singleton + @Provides + fun media3DownloadDataSource( + @ApplicationContext applicationContext: Context, + downloadIndex: DownloadIndex + ) = Media3DownloadDataSource( + applicationContext, + MediaDownloadService::class.java, + downloadIndex + ) + + @Provides + @Singleton + fun mediaDownloadLocalDataSource( + mediaDownloadDao: MediaDownloadDao, + ): MediaDownloadLocalDataSource = MediaDownloadLocalDataSource(mediaDownloadDao) + + @Singleton + @Provides + fun playlistLocalDataSource(playlistDao: PlaylistDao): PlaylistLocalDataSource = + PlaylistLocalDataSource(playlistDao) + + @Singleton + @Provides + fun playlistRemoteDataSource( + uampService: UampService, + ): PlaylistRemoteDataSource = + PlaylistRemoteDataSource( + ioDispatcher = Dispatchers.IO, + uampService = uampService, + ) +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DatabaseModule.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DatabaseModule.kt new file mode 100644 index 0000000000..d321f323fe --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DatabaseModule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.di + +import android.content.Context +import androidx.room.Room +import com.google.android.horologist.mediasample.data.database.MediaDatabase +import com.google.android.horologist.mediasample.data.database.dao.MediaDownloadDao +import com.google.android.horologist.mediasample.data.database.dao.PlaylistDao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + private const val MEDIA_DATABASE_NAME = "media-database" + + @Provides + @Singleton + fun mediaDatabase( + @ApplicationContext context: Context, + ): MediaDatabase { + return Room.databaseBuilder( + context, + MediaDatabase::class.java, + MEDIA_DATABASE_NAME + ).build() + } + + @Provides + @Singleton + fun mediaDownloadDao( + database: MediaDatabase, + ): MediaDownloadDao = database.mediaDownloadDao() + + @Provides + @Singleton + fun playlistDao( + database: MediaDatabase, + ): PlaylistDao = database.playlistDao() +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DownloadModule.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DownloadModule.kt index 80f5397962..91dad4ce4d 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DownloadModule.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/di/DownloadModule.kt @@ -22,17 +22,12 @@ import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource import androidx.media3.datasource.cache.Cache import androidx.media3.datasource.okhttp.OkHttpDataSource -import androidx.media3.exoplayer.offline.DownloadIndex import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadNotificationHelper import androidx.media3.exoplayer.workmanager.WorkManagerScheduler -import androidx.room.Room import com.google.android.horologist.media3.logging.ErrorReporter import com.google.android.horologist.media3.logging.TransferListener -import com.google.android.horologist.mediasample.data.database.DownloadDatabase -import com.google.android.horologist.mediasample.data.database.dao.PlaylistDownloadDao -import com.google.android.horologist.mediasample.data.datasource.Media3DownloadDataSource -import com.google.android.horologist.mediasample.data.datasource.PlaylistDownloadLocalDataSource +import com.google.android.horologist.mediasample.data.datasource.MediaDownloadLocalDataSource import com.google.android.horologist.mediasample.data.service.download.DownloadManagerListener import com.google.android.horologist.mediasample.data.service.download.MediaDownloadService import com.google.android.horologist.mediasample.di.annotation.DownloadFeature @@ -56,7 +51,6 @@ import javax.inject.Singleton object DownloadModule { private const val DOWNLOAD_WORK_MANAGER_SCHEDULER_WORK_NAME = "mediasample_download" - private const val DOWNLOAD_DATABASE_NAME = "download-database" @DownloadFeature @Singleton @@ -126,41 +120,6 @@ object DownloadModule { @ApplicationContext applicationContext: Context, ) = WorkManagerScheduler(applicationContext, DOWNLOAD_WORK_MANAGER_SCHEDULER_WORK_NAME) - @Singleton - @Provides - fun downloadDataSource( - @ApplicationContext applicationContext: Context, - downloadIndex: DownloadIndex - ) = Media3DownloadDataSource( - applicationContext, - MediaDownloadService::class.java, - downloadIndex - ) - - @Provides - @Singleton - fun downloadDatabase( - @ApplicationContext context: Context, - ): DownloadDatabase { - return Room.databaseBuilder( - context, - DownloadDatabase::class.java, - DOWNLOAD_DATABASE_NAME - ).build() - } - - @Provides - @Singleton - fun playlistDownloadLocalDataSource( - playlistDownloadDao: PlaylistDownloadDao, - ): PlaylistDownloadLocalDataSource = PlaylistDownloadLocalDataSource(playlistDownloadDao) - - @Provides - @Singleton - fun playlistDownloadDao( - database: DownloadDatabase, - ): PlaylistDownloadDao = database.playlistDownloadDao() - @DownloadFeature @Provides @Singleton @@ -170,6 +129,9 @@ object DownloadModule { @Singleton fun downloadManagerListener( @DownloadFeature coroutineScope: CoroutineScope, - playlistDownloadDao: PlaylistDownloadDao, - ): DownloadManagerListener = DownloadManagerListener(coroutineScope, playlistDownloadDao) + mediaDownloadLocalDataSource: MediaDownloadLocalDataSource + ): DownloadManagerListener = DownloadManagerListener( + coroutineScope, + mediaDownloadLocalDataSource + ) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/di/MediaApplicationModule.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/di/MediaApplicationModule.kt index 1178dc0fd9..5734b6dbb7 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/di/MediaApplicationModule.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/di/MediaApplicationModule.kt @@ -40,14 +40,6 @@ import com.google.android.horologist.media3.rules.PlaybackRules import com.google.android.horologist.mediasample.AppConfig import com.google.android.horologist.mediasample.complication.DataUpdates import com.google.android.horologist.mediasample.complication.MediaStatusComplicationService -import com.google.android.horologist.mediasample.data.api.UampService -import com.google.android.horologist.mediasample.data.datasource.Media3DownloadDataSource -import com.google.android.horologist.mediasample.data.datasource.PlaylistDownloadLocalDataSource -import com.google.android.horologist.mediasample.data.datasource.PlaylistRemoteDataSource -import com.google.android.horologist.mediasample.data.repository.PlaylistDownloadRepositoryImpl -import com.google.android.horologist.mediasample.data.repository.PlaylistRepositoryImpl -import com.google.android.horologist.mediasample.domain.PlaylistDownloadRepository -import com.google.android.horologist.mediasample.domain.PlaylistRepository import com.google.android.horologist.mediasample.domain.SettingsRepository import com.google.android.horologist.mediasample.system.Logging import com.google.android.horologist.mediasample.util.ResourceProvider @@ -110,13 +102,6 @@ object MediaApplicationModule { application.preferencesDataStoreFile("prefs") } - @Singleton - @Provides - fun settingsRepository( - prefsDataStore: DataStore - ) = - SettingsRepository(prefsDataStore) - @Singleton @Provides @ForApplicationScope @@ -223,37 +208,4 @@ object MediaApplicationModule { ) return DataUpdates(updater) } - - @Singleton - @Provides - fun playlistRepository( - playlistRemoteDataSource: PlaylistRemoteDataSource, - @ForApplicationScope coroutineScope: CoroutineScope - ): PlaylistRepository = - PlaylistRepositoryImpl(playlistRemoteDataSource, coroutineScope) - - @Singleton - @Provides - fun playlistDownloadRepository( - @ForApplicationScope coroutineScope: CoroutineScope, - playlistDownloadLocalDataSource: PlaylistDownloadLocalDataSource, - media3DownloadDataSource: Media3DownloadDataSource - ): PlaylistDownloadRepository = - PlaylistDownloadRepositoryImpl( - coroutineScope, - playlistDownloadLocalDataSource, - media3DownloadDataSource - ) - - @Singleton - @Provides - fun playlistRemoteDataSource( - uampService: UampService, - errorReporter: ErrorReporter - ): PlaylistRemoteDataSource = - PlaylistRemoteDataSource( - ioDispatcher = Dispatchers.IO, - uampService = uampService, - errorReporter = errorReporter - ) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/PlaylistDownloadRepository.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/PlaylistDownloadRepository.kt index cfd52020cd..20a0c65bf9 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/PlaylistDownloadRepository.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/PlaylistDownloadRepository.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.Flow */ interface PlaylistDownloadRepository { - fun get(playlist: Playlist): Flow + fun get(playlistId: String): Flow fun download(playlist: Playlist) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/PlaylistRepository.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/PlaylistRepository.kt index ebb512ea63..af9fea80d7 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/PlaylistRepository.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/PlaylistRepository.kt @@ -24,7 +24,5 @@ import kotlinx.coroutines.flow.Flow */ interface PlaylistRepository { - suspend fun getPlaylist(id: String): Playlist? - - fun getPlaylists(): Flow>> + fun getAllPopulated(): Flow> } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/MediaDownload.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/MediaDownload.kt new file mode 100644 index 0000000000..026100107f --- /dev/null +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/MediaDownload.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.horologist.mediasample.domain.model + +import com.google.android.horologist.media.model.Media + +data class MediaDownload( + val media: Media, + val status: Status +) { + + enum class Status { + Idle, + InProgress, + Completed + } +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/Playlist.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/Playlist.kt index 6fe03d00de..c79ad7767e 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/Playlist.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/Playlist.kt @@ -21,6 +21,6 @@ import com.google.android.horologist.media.model.Media data class Playlist( val id: String, val name: String, - val mediaList: List, val artworkUri: String? = null, + val mediaList: List, ) diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/PlaylistDownload.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/PlaylistDownload.kt index 479c66cdce..8a2bc5acf5 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/PlaylistDownload.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/domain/model/PlaylistDownload.kt @@ -16,16 +16,7 @@ package com.google.android.horologist.mediasample.domain.model -import com.google.android.horologist.media.model.Media - data class PlaylistDownload( val playlist: Playlist, - val mediaList: List> -) { - - enum class Status { - Idle, - InProgress, - Completed - } -} + val mediaList: List +) diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/entity/DownloadMediaUiModelMapper.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/entity/DownloadMediaUiModelMapper.kt index cfd560fdf4..343c572308 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/entity/DownloadMediaUiModelMapper.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/entity/DownloadMediaUiModelMapper.kt @@ -19,23 +19,27 @@ package com.google.android.horologist.mediasample.ui.entity import com.google.android.horologist.media.model.Media import com.google.android.horologist.media.ui.state.mapper.MediaUiModelMapper import com.google.android.horologist.media.ui.state.model.DownloadMediaUiModel -import com.google.android.horologist.mediasample.domain.model.PlaylistDownload +import com.google.android.horologist.mediasample.domain.model.MediaDownload object DownloadMediaUiModelMapper { fun map( - media: Media, - status: PlaylistDownload.Status - ): DownloadMediaUiModel = when (status) { - PlaylistDownload.Status.Idle, - PlaylistDownload.Status.InProgress -> { - DownloadMediaUiModel.Unavailable(MediaUiModelMapper.map(media)) + mediaDownload: MediaDownload, + ): DownloadMediaUiModel = when (mediaDownload.status) { + MediaDownload.Status.Idle, + MediaDownload.Status.InProgress -> { + DownloadMediaUiModel.Unavailable(MediaUiModelMapper.map(mediaDownload.media)) } - PlaylistDownload.Status.Completed -> { - DownloadMediaUiModel.Available(MediaUiModelMapper.map(media)) + MediaDownload.Status.Completed -> { + DownloadMediaUiModel.Available(MediaUiModelMapper.map(mediaDownload.media)) } } - fun map(list: List>): List = - list.map { map(it.first, it.second) } + fun mapList(mediaDownloadList: List): List = + mediaDownloadList.map(::map) + + fun map(media: Media): DownloadMediaUiModel = + DownloadMediaUiModel.Unavailable(MediaUiModelMapper.map(media)) + + fun map(mediaList: List): List = mediaList.map(::map) } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/entity/UampEntityScreenViewModel.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/entity/UampEntityScreenViewModel.kt index 810d7173b7..c4f8bff353 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/entity/UampEntityScreenViewModel.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/entity/UampEntityScreenViewModel.kt @@ -23,16 +23,12 @@ import com.google.android.horologist.media.repository.PlayerRepository import com.google.android.horologist.media.ui.navigation.NavigationScreens import com.google.android.horologist.media.ui.screens.entity.EntityScreenState import com.google.android.horologist.mediasample.domain.PlaylistDownloadRepository -import com.google.android.horologist.mediasample.domain.PlaylistRepository -import com.google.android.horologist.mediasample.domain.model.Playlist +import com.google.android.horologist.mediasample.domain.model.MediaDownload import com.google.android.horologist.mediasample.domain.model.PlaylistDownload import com.google.android.horologist.mediasample.ui.mapper.PlaylistUiModelMapper import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flatMapMerge -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -40,30 +36,25 @@ import javax.inject.Inject @HiltViewModel class UampEntityScreenViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val playlistRepository: PlaylistRepository, private val playlistDownloadRepository: PlaylistDownloadRepository, private val playerRepository: PlayerRepository, ) : ViewModel() { private val playlistId: String = savedStateHandle[NavigationScreens.Collection.id]!! private val playlistName: String = savedStateHandle[NavigationScreens.Collection.name]!! - private val playlist: StateFlow = flow { - emit(playlistRepository.getPlaylist(playlistId)) - }.stateIn(viewModelScope, started = SharingStarted.Eagerly, initialValue = null) + private val playlistDownload: StateFlow = + playlistDownloadRepository.get(playlistId) + .stateIn(viewModelScope, started = SharingStarted.Eagerly, initialValue = null) - @OptIn(FlowPreview::class) - val uiState: StateFlow = playlist.flatMapMerge { playlist -> - if (playlist != null) { - playlistDownloadRepository.get(playlist) - .map { - EntityScreenState.Loaded( - playlistUiModel = PlaylistUiModelMapper.map(it.playlist), - downloadList = DownloadMediaUiModelMapper.map(it.mediaList), - downloading = it.mediaList.any { (_, status) -> status == PlaylistDownload.Status.InProgress }, - ) - } + val uiState: StateFlow = playlistDownload.map { playlistDownload -> + if (playlistDownload != null) { + EntityScreenState.Loaded( + playlistUiModel = PlaylistUiModelMapper.map(playlistDownload.playlist), + downloadList = playlistDownload.mediaList.map(DownloadMediaUiModelMapper::map), + downloading = playlistDownload.mediaList.any { (_, status) -> status == MediaDownload.Status.InProgress }, + ) } else { - flow { emit(EntityScreenState.Loading(playlistName)) } + EntityScreenState.Loading(playlistName) } }.stateIn( viewModelScope, @@ -80,14 +71,14 @@ class UampEntityScreenViewModel @Inject constructor( } private fun play(shuffled: Boolean) { - playlist.value?.let { + playlistDownload.value?.let { playlistDownload -> playerRepository.setShuffleModeEnabled(shuffled) - playerRepository.setMediaList(it.mediaList) + playerRepository.setMediaList(playlistDownload.playlist.mediaList) playerRepository.prepare() playerRepository.play() } } - fun download() = playlist.value?.let(playlistDownloadRepository::download) + fun download() = playlistDownload.value?.let { playlistDownloadRepository.download(it.playlist) } } diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/playlists/UampPlaylistsScreenViewModel.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/playlists/UampPlaylistsScreenViewModel.kt index 5becf39711..a1d99d0bb9 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/playlists/UampPlaylistsScreenViewModel.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/playlists/UampPlaylistsScreenViewModel.kt @@ -24,13 +24,17 @@ import com.google.android.horologist.media.ui.snackbar.UiMessage import com.google.android.horologist.mediasample.R import com.google.android.horologist.mediasample.domain.PlaylistRepository import com.google.android.horologist.mediasample.domain.SettingsRepository +import com.google.android.horologist.mediasample.domain.model.Playlist +import com.google.android.horologist.mediasample.domain.model.Settings import com.google.android.horologist.mediasample.ui.mapper.PlaylistUiModelMapper import com.google.android.horologist.mediasample.util.ResourceProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import java.io.IOException import javax.inject.Inject @HiltViewModel @@ -41,29 +45,35 @@ class UampPlaylistsScreenViewModel @Inject constructor( private val resourceProvider: ResourceProvider ) : ViewModel() { - val uiState = combine( - playlistRepository.getPlaylists(), - settingsRepository.settingsFlow - ) { playlistsResult, settings -> - when { - playlistsResult.isSuccess -> { - PlaylistScreenState.Loaded( - playlistsResult.getOrThrow().map { - PlaylistUiModelMapper.map( - playlist = it, - shouldMapArtworkUri = settings.showArtworkOnChip + val uiState: StateFlow = + combine, Settings, PlaylistScreenState>( + playlistRepository.getAllPopulated(), + settingsRepository.settingsFlow + ) { playlistsResult, settings -> + PlaylistScreenState.Loaded( + playlistsResult.map { + PlaylistUiModelMapper.map( + playlist = it, + shouldMapArtworkUri = settings.showArtworkOnChip + ) + } + ) + }.catch { throwable -> + when (throwable) { + is IOException -> { + snackbarManager.showMessage( + UiMessage( + message = resourceProvider.getString(R.string.horologist_sample_network_error), + error = true ) - } - ) + ) + emit(PlaylistScreenState.Failed(R.string.horologist_sample_network_error)) + } + else -> throw throwable } - else -> { - snackbarManager.showMessage(UiMessage(message = resourceProvider.getString(R.string.horologist_sample_network_error), error = true)) - PlaylistScreenState.Failed(R.string.horologist_sample_network_error) - } - } - }.stateIn( - viewModelScope, - started = SharingStarted.Eagerly, - initialValue = PlaylistScreenState.Loading - ) + }.stateIn( + viewModelScope, + started = SharingStarted.Eagerly, + initialValue = PlaylistScreenState.Loading + ) } diff --git a/media-sample/src/test/java/com/google/android/horologist/mediasample/data/datasource/PlaylistRemoteDataSourceTest.kt b/media-sample/src/test/java/com/google/android/horologist/mediasample/data/datasource/PlaylistRemoteDataSourceTest.kt index 71d09349fd..ec999ab4fe 100644 --- a/media-sample/src/test/java/com/google/android/horologist/mediasample/data/datasource/PlaylistRemoteDataSourceTest.kt +++ b/media-sample/src/test/java/com/google/android/horologist/mediasample/data/datasource/PlaylistRemoteDataSourceTest.kt @@ -16,16 +16,13 @@ package com.google.android.horologist.mediasample.data.datasource -import com.google.android.horologist.mediasample.data.mapper.PlaylistMapper import com.google.android.horologist.test.toolbox.testdoubles.FakeUampService -import com.google.android.horologist.test.toolbox.testdoubles.InMemoryErrorReporter import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import java.io.IOException class PlaylistRemoteDataSourceTest { @@ -33,8 +30,6 @@ class PlaylistRemoteDataSourceTest { private val uampService = FakeUampService() - private val errorLogger = InMemoryErrorReporter() - private lateinit var sut: PlaylistRemoteDataSource @Before @@ -42,32 +37,18 @@ class PlaylistRemoteDataSourceTest { sut = PlaylistRemoteDataSource( ioDispatcher = testDispatcher, uampService = uampService, - errorReporter = errorLogger ) } @Test fun getPlaylists_backedByUampServiceCatalog() = runTest(testDispatcher) { // given - val uampServiceCatalogResult = PlaylistMapper.map(uampService.catalog()) - - // when - val result = sut.getPlaylists().first() - - // then - assertThat(result.getOrNull()).isEqualTo(uampServiceCatalogResult) - } - - @Test - fun handlesFailures() = runTest(testDispatcher) { - // given - uampService.failing = IOException() + val uampServiceCatalogResult = uampService.catalog() // when val result = sut.getPlaylists().first() // then - assertThat(result.exceptionOrNull()).isInstanceOf(IOException::class.java) - assertThat(errorLogger.messages).isNotEmpty() + assertThat(result).isEqualTo(uampServiceCatalogResult) } } diff --git a/media-sample/src/test/java/com/google/android/horologist/mediasample/data/mapper/MediaMapperTest.kt b/media-sample/src/test/java/com/google/android/horologist/mediasample/data/mapper/MediaMapperTest.kt index d021f386a6..abaca67491 100644 --- a/media-sample/src/test/java/com/google/android/horologist/mediasample/data/mapper/MediaMapperTest.kt +++ b/media-sample/src/test/java/com/google/android/horologist/mediasample/data/mapper/MediaMapperTest.kt @@ -56,58 +56,4 @@ class MediaMapperTest { assertThat(result.artworkUri).isEqualTo(artworkUri) assertThat(result.extras).isEmpty() } - - @Test - fun givenListOfMusicApiModel_thenMapsCorrectly() { - // given - val musicApiModel1 = MusicApiModel( - album = "album1", - artist = "artist1", - duration = 1, - genre = "genre1", - id = "id1", - image = "artworkUri1", - site = "site1", - source = "source1", - title = "title1", - totalTrackCount = 1, - trackNumber = 1, - ) - - val musicApiModel2 = MusicApiModel( - album = "album2", - artist = "artist2", - duration = 2, - genre = "genre2", - id = "id2", - image = "artworkUri2", - site = "site2", - source = "source2", - title = "title2", - totalTrackCount = 2, - trackNumber = 2, - ) - - val list = listOf(musicApiModel1, musicApiModel2) - - // when - val result = MediaMapper.map(list) - - // then - assertThat(result).hasSize(2) - assertThat(result[0].id).isEqualTo(musicApiModel1.id) - assertThat(result[0].uri).isEqualTo(musicApiModel1.source) - assertThat(result[0].title).isEqualTo(musicApiModel1.title) - assertThat(result[0].artist).isEqualTo(musicApiModel1.artist) - assertThat(result[0].artworkUri).isEqualTo(musicApiModel1.image) - assertThat(result[0].extras).isEmpty() - - assertThat(result).hasSize(2) - assertThat(result[1].id).isEqualTo(musicApiModel2.id) - assertThat(result[1].uri).isEqualTo(musicApiModel2.source) - assertThat(result[1].title).isEqualTo(musicApiModel2.title) - assertThat(result[1].artist).isEqualTo(musicApiModel2.artist) - assertThat(result[1].artworkUri).isEqualTo(musicApiModel2.image) - assertThat(result[1].extras).isEmpty() - } }