diff --git a/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt b/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt index 08bfa18b4..046189037 100644 --- a/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt +++ b/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt @@ -39,4 +39,7 @@ class OfflineFirstUserDataRepository @Inject constructor( override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) = niaPreferencesDataSource.toggleFollowedAuthorId(followedAuthorId, followed) + + override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) = + niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked) } diff --git a/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt b/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt index f07b4efbe..deea011cc 100644 --- a/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt +++ b/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt @@ -45,4 +45,9 @@ interface UserDataRepository { * Toggles the user's newly followed/unfollowed author */ suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) + + /** + * Updates the bookmarked status for a news resource + */ + suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) } diff --git a/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt b/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt index 2e214333f..5feb5eab3 100644 --- a/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt +++ b/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeUserDataRepository.kt @@ -49,4 +49,8 @@ class FakeUserDataRepository @Inject constructor( override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) { niaPreferencesDataSource.toggleFollowedAuthorId(followedAuthorId, followed) } + + override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) { + niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked) + } } diff --git a/core-data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt b/core-data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt index 08e5294c6..d65820ea0 100644 --- a/core-data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt +++ b/core-data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt @@ -47,7 +47,7 @@ class OfflineFirstUserDataRepositoryTest { } @Test - fun offlineFirstTopicsRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() = + fun offlineFirstUserDataRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() = runTest { subject.toggleFollowedTopicId(followedTopicId = "0", followed = true) @@ -68,7 +68,8 @@ class OfflineFirstUserDataRepositoryTest { ) assertEquals( - niaPreferencesDataSource.followedTopicIds + niaPreferencesDataSource.userDataStream + .map { it.followedTopics } .first(), subject.userDataStream .map { it.followedTopics } @@ -77,7 +78,7 @@ class OfflineFirstUserDataRepositoryTest { } @Test - fun offlineFirstTopicsRepository_set_followed_topics_logic_delegates_to_nia_preferences() = + fun offlineFirstUserDataRepository_set_followed_topics_logic_delegates_to_nia_preferences() = runTest { subject.setFollowedTopicIds(followedTopicIds = setOf("1", "2")) @@ -89,11 +90,43 @@ class OfflineFirstUserDataRepositoryTest { ) assertEquals( - niaPreferencesDataSource.followedTopicIds + niaPreferencesDataSource.userDataStream + .map { it.followedTopics } .first(), subject.userDataStream .map { it.followedTopics } .first() ) } + + @Test + fun offlineFirstUserDataRepository_bookmark_news_resource_logic_delegates_to_nia_preferences() = + runTest { + subject.updateNewsResourceBookmark(newsResourceId = "0", bookmarked = true) + + assertEquals( + setOf("0"), + subject.userDataStream + .map { it.bookmarkedNewsResources } + .first() + ) + + subject.updateNewsResourceBookmark(newsResourceId = "1", bookmarked = true) + + assertEquals( + setOf("0", "1"), + subject.userDataStream + .map { it.bookmarkedNewsResources } + .first() + ) + + assertEquals( + niaPreferencesDataSource.userDataStream + .map { it.bookmarkedNewsResources } + .first(), + subject.userDataStream + .map { it.bookmarkedNewsResources } + .first() + ) + } } diff --git a/core-datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core-datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt index 8ce1fb3ee..6d7028f0c 100644 --- a/core-datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt +++ b/core-datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt @@ -18,55 +18,69 @@ package com.google.samples.apps.nowinandroid.core.datastore import android.util.Log import androidx.datastore.core.DataStore +import com.google.protobuf.kotlin.DslList +import com.google.protobuf.kotlin.DslProxy import com.google.samples.apps.nowinandroid.core.model.data.UserData import java.io.IOException import javax.inject.Inject -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.retry class NiaPreferencesDataSource @Inject constructor( private val userPreferences: DataStore ) { - suspend fun setFollowedTopicIds(followedTopicIds: Set) { - try { - userPreferences.updateData { - it.copy { - this.followedTopicIds.clear() - this.followedTopicIds.addAll(followedTopicIds) - } - } - } catch (ioException: IOException) { - Log.e("NiaPreferences", "Failed to update user preferences", ioException) - } - } - suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) { - try { - userPreferences.updateData { - it.copy { - val current = - if (followed) { - followedTopicIds + followedTopicId - } else { - followedTopicIds - followedTopicId - } - this.followedTopicIds.clear() - this.followedTopicIds.addAll(current) - } - } - } catch (ioException: IOException) { - Log.e("NiaPreferences", "Failed to update user preferences", ioException) + val userDataStream = userPreferences.data + .map { + UserData( + bookmarkedNewsResources = it.bookmarkedNewsResourceIdsList.toSet(), + followedTopics = it.followedTopicIdsList.toSet(), + followedAuthors = it.followedAuthorIdsList.toSet(), + ) } - } - val followedTopicIds: Flow> = userPreferences.data - .retry { - Log.e("NiaPreferences", "Failed to read user preferences", it) - true - } - .map { it.followedTopicIdsList.toSet() } + suspend fun setFollowedTopicIds(followedTopicIds: Set) = + userPreferences.setList( + listGetter = { it.followedTopicIds }, + listModifier = { followedTopicIds.toList() }, + clear = { it.clear() }, + addAll = { dslList, editedList -> dslList.addAll(editedList) } + ) + + suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) = + userPreferences.editList( + add = followed, + value = followedTopicId, + listGetter = { it.followedTopicIds }, + clear = { it.clear() }, + addAll = { dslList, editedList -> dslList.addAll(editedList) } + ) + + suspend fun setFollowedAuthorIds(followedAuthorIds: Set) = + userPreferences.setList( + listGetter = { it.followedAuthorIds }, + listModifier = { followedAuthorIds.toList() }, + clear = { it.clear() }, + addAll = { dslList, editedList -> dslList.addAll(editedList) } + ) + + suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) = + userPreferences.editList( + add = followed, + value = followedAuthorId, + listGetter = { it.followedAuthorIds }, + clear = { it.clear() }, + addAll = { dslList, editedList -> dslList.addAll(editedList) } + ) + + suspend fun toggleNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) = + userPreferences.editList( + add = bookmarked, + value = newsResourceId, + listGetter = { it.bookmarkedNewsResourceIds }, + clear = { it.clear() }, + addAll = { dslList, editedList -> dslList.addAll(editedList) } + ) suspend fun getChangeListVersions() = userPreferences.data .map { @@ -106,51 +120,47 @@ class NiaPreferencesDataSource @Inject constructor( } } - suspend fun setFollowedAuthorIds(followedAuthorIds: Set) { - try { - userPreferences.updateData { - it.copy { - this.followedAuthorIds.clear() - this.followedAuthorIds.addAll(followedAuthorIds) - } - } - } catch (ioException: IOException) { - Log.e("NiaPreferences", "Failed to update user preferences", ioException) - } + /** + * Adds or removes [value] from the [DslList] provided by [listGetter] + */ + private suspend fun DataStore.editList( + add: Boolean, + value: String, + listGetter: (UserPreferencesKt.Dsl) -> DslList, + clear: UserPreferencesKt.Dsl.(DslList) -> Unit, + addAll: UserPreferencesKt.Dsl.(DslList, Iterable) -> Unit + ) { + setList( + listGetter = listGetter, + listModifier = { currentList -> + if (add) currentList + value + else currentList - value + }, + clear = clear, + addAll = addAll + ) } - suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) { + /** + * Sets the value provided by [listModifier] into the [DslList] read by [listGetter] + */ + private suspend fun DataStore.setList( + listGetter: (UserPreferencesKt.Dsl) -> DslList, + listModifier: (DslList) -> List, + clear: UserPreferencesKt.Dsl.(DslList) -> Unit, + addAll: UserPreferencesKt.Dsl.(DslList, List) -> Unit + ) { try { - userPreferences.updateData { + updateData { it.copy { - val current = - if (followed) { - followedAuthorIds + followedAuthorId - } else { - followedAuthorIds - followedAuthorId - } - this.followedAuthorIds.clear() - this.followedAuthorIds.addAll(current) + val dslList = listGetter(this) + val newList = listModifier(dslList) + clear(dslList) + addAll(dslList, newList) } } } catch (ioException: IOException) { Log.e("NiaPreferences", "Failed to update user preferences", ioException) } } - - val followedAuthorIds: Flow> = userPreferences.data - .retry { - Log.e("NiaPreferences", "Failed to read user preferences", it) - true - } - .map { it.followedAuthorIdsList.toSet() } - - val userDataStream = userPreferences.data - .map { - UserData( - bookmarkedNewsResources = emptySet(), - followedTopics = it.followedTopicIdsList.toSet(), - followedAuthors = it.followedAuthorIdsList.toSet(), - ) - } } diff --git a/core-datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto b/core-datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto index 466565e9e..407031215 100644 --- a/core-datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto +++ b/core-datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto @@ -30,4 +30,5 @@ message UserPreferences { bool has_done_int_to_string_id_migration = 8; repeated string followed_topic_ids = 9; repeated string followed_author_ids = 10; + repeated string bookmarked_news_resource_ids = 11; } diff --git a/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt index 1cffbb369..4b0dad48d 100644 --- a/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt +++ b/core-testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt @@ -65,6 +65,15 @@ class TestUserDataRepository : UserDataRepository { } } + override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) { + currentUserData.let { current -> + val bookmarkedNews = if (bookmarked) current.bookmarkedNewsResources + newsResourceId + else current.bookmarkedNewsResources - newsResourceId + + _userData.tryEmit(current.copy(bookmarkedNewsResources = bookmarkedNews)) + } + } + /** * A test-only API to allow querying the current followed topics. */