diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt index e640f43801..473856eca5 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import com.slack.circuit.codegen.annotations.CircuitInject @@ -25,7 +26,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL_DETAILS import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_SPOTLIGHT @@ -90,19 +91,25 @@ class ToolsPresenter @AssistedInject constructor( @Composable private fun rememberFilters(): ToolsScreen.Filters { + val scope = rememberCoroutineScope() + // selected category - var selectedCategory: String? by remember { mutableStateOf(null) } + val selectedCategory by remember { settings.getDashboardFilterCategoryFlow() }.collectAsState(null) // selected language - var selectedLocale: Locale? by remember { mutableStateOf(null) } + val selectedLocale by remember { settings.getDashboardFilterLocaleFlow() }.collectAsState(null) val selectedLanguage = rememberLanguage(selectedLocale) var languageQuery by remember { mutableStateOf("") } val filtersEventSink: (ToolsScreen.FiltersEvent) -> Unit = remember { { when (it) { - is ToolsScreen.FiltersEvent.SelectCategory -> selectedCategory = it.category - is ToolsScreen.FiltersEvent.SelectLanguage -> selectedLocale = it.locale + is ToolsScreen.FiltersEvent.SelectCategory -> scope.launch { + settings.updateDashboardFilterCategory(it.category) + } + is ToolsScreen.FiltersEvent.SelectLanguage -> scope.launch { + settings.updateDashboardFilterLocale(it.locale) + } is ToolsScreen.FiltersEvent.UpdateLanguageQuery -> languageQuery = it.query } } diff --git a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenterTest.kt b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenterTest.kt index 697172ae13..15f7fbcb43 100644 --- a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenterTest.kt +++ b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenterTest.kt @@ -5,6 +5,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.slack.circuit.test.FakeNavigator import com.slack.circuit.test.test +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -43,6 +44,8 @@ class ToolsPresenterTest { private val toolsFlow = MutableStateFlow(emptyList()) private val languagesFlow = MutableStateFlow(emptyList()) private val gospelLanguagesFlow = MutableStateFlow(emptyList()) + private val selectedCategory = MutableStateFlow(null) + private val selectedLocale = MutableStateFlow(null) private val languagesRepository: LanguagesRepository = mockk { every { findLanguageFlow(any()) } returns flowOf(null) @@ -54,6 +57,10 @@ class ToolsPresenterTest { every { appLanguage } returns this@ToolsPresenterTest.appLanguage.value every { appLanguageFlow } returns this@ToolsPresenterTest.appLanguage every { isFeatureDiscoveredFlow(Settings.FEATURE_TOOL_FAVORITE) } returns isFavoritesFeatureDiscovered + every { getDashboardFilterCategoryFlow() } returns selectedCategory + every { getDashboardFilterLocaleFlow() } returns selectedLocale + coEvery { updateDashboardFilterCategory(any()) } answers { selectedCategory.value = firstArg() } + coEvery { updateDashboardFilterLocale(any()) } answers { selectedLocale.value = firstArg() } } private val toolsRepository: ToolsRepository = mockk { every { getNormalToolsFlow() } returns toolsFlow diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c5f873de5..3ad06425e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ androidx-compose-compiler = "1.5.8" androidx-compose-material = "1.5.4" androidx-compose-ui = "1.5.4" androidx-core = "1.12.0" +androidx-datastore = "1.0.0" androidx-hilt = "1.1.0" androidx-lifecycle = "2.7.0" androidx-room = "2.6.1" @@ -60,7 +61,8 @@ androidx-constraintlayout-compose = "androidx.constraintlayout:constraintlayout- androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-databinding-compiler = { module = "androidx.databinding:databinding-compiler", version.ref = "android-gradle-plugin" } -androidx-datastore = "androidx.datastore:datastore:1.0.0" +androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidx-datastore" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.6.2" androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidx-hilt" } androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidx-hilt" } diff --git a/library/base/build.gradle.kts b/library/base/build.gradle.kts index cf17c47753..c44b111725 100644 --- a/library/base/build.gradle.kts +++ b/library/base/build.gradle.kts @@ -18,6 +18,7 @@ onesky { dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.core.ktx) + implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.gtoSupport.androidx.core) diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt b/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt index 86475cea67..cb1c4b6d6e 100644 --- a/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt +++ b/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt @@ -7,6 +7,8 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.edit import androidx.core.content.pm.PackageInfoCompat import androidx.core.os.LocaleListCompat +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore import androidx.lifecycle.LiveData import androidx.lifecycle.distinctUntilChanged import dagger.hilt.android.qualifiers.ApplicationContext @@ -18,6 +20,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import org.ccci.gto.android.common.androidx.lifecycle.getBooleanLiveData @@ -30,11 +33,14 @@ private const val PREF_LAUNCHES = "launches" private const val PREF_VERSION_FIRST_LAUNCH = "version.firstLaunch" private const val PREF_VERSION_LAST_LAUNCH = "version.lastLaunch" +private val Context.dataStorePreferences by preferencesDataStore("${PREFS_SETTINGS}Settings") + @Singleton class Settings internal constructor(private val context: Context, coroutineScope: CoroutineScope) { @Inject internal constructor(@ApplicationContext context: Context) : this(context, CoroutineScope(Dispatchers.Default)) + private val dataStorePreferences = context.dataStorePreferences private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_SETTINGS, Context.MODE_PRIVATE) companion object { @@ -53,6 +59,10 @@ class Settings internal constructor(private val context: Context, coroutineScope const val FEATURE_TUTORIAL_FEATURES = "tutorialTraining" const val FEATURE_TUTORIAL_LIVE_SHARE = "tutorialLiveShare." const val FEATURE_TUTORIAL_TIPS = "tutorialTips." + + // Dashboard Settings + private val KEY_DASHBOARD_FILTER_CATEGORY = stringPreferencesKey("dashboardFilterCategory") + private val KEY_DASHBOARD_FILTER_LOCALE = stringPreferencesKey("dashboardFilterLocale") } // region Language Settings @@ -114,6 +124,36 @@ class Settings internal constructor(private val context: Context, coroutineScope private fun isFeatureDiscoveredInt(feature: String) = prefs.getBoolean("$PREF_FEATURE_DISCOVERED$feature", false) // endregion Feature Discovery Tracking + // region Dashboard Settings + fun getDashboardFilterCategoryFlow() = dataStorePreferences.data + .map { it[KEY_DASHBOARD_FILTER_CATEGORY] } + .distinctUntilChanged() + suspend fun updateDashboardFilterCategory(category: String?) { + dataStorePreferences.updateData { + it.toMutablePreferences().apply { + when (category) { + null -> remove(KEY_DASHBOARD_FILTER_CATEGORY) + else -> set(KEY_DASHBOARD_FILTER_CATEGORY, category) + } + } + } + } + + fun getDashboardFilterLocaleFlow() = dataStorePreferences.data + .map { it[KEY_DASHBOARD_FILTER_LOCALE]?.let { Locale.forLanguageTag(it) } } + .distinctUntilChanged() + suspend fun updateDashboardFilterLocale(locale: Locale?) { + dataStorePreferences.updateData { + it.toMutablePreferences().apply { + when (locale) { + null -> remove(KEY_DASHBOARD_FILTER_LOCALE) + else -> set(KEY_DASHBOARD_FILTER_LOCALE, locale.toLanguageTag()) + } + } + } + } + // endregion Dashboard Settings + // region Campaign Tracking fun isAddedToCampaign(oktaId: String? = null, guid: String? = null) = when { oktaId == null && guid == null -> true diff --git a/library/base/src/test/kotlin/org/cru/godtools/base/SettingsTest.kt b/library/base/src/test/kotlin/org/cru/godtools/base/SettingsTest.kt index 724c655871..5e613c907d 100644 --- a/library/base/src/test/kotlin/org/cru/godtools/base/SettingsTest.kt +++ b/library/base/src/test/kotlin/org/cru/godtools/base/SettingsTest.kt @@ -2,8 +2,12 @@ package org.cru.godtools.base import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.test +import java.util.Locale import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertNull +import kotlinx.coroutines.test.runTest import org.cru.godtools.base.Settings.Companion.FEATURE_TUTORIAL_ONBOARDING import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -37,4 +41,32 @@ class SettingsTest { fun verifyFeatureDiscoveryTutorialOnboardingNewInstall() { assertFalse(settings.isFeatureDiscovered(FEATURE_TUTORIAL_ONBOARDING)) } + + // region Dashboard Settings + @Test + fun testDashboardFilterCategory() = runTest { + settings.getDashboardFilterCategoryFlow().test { + assertNull(awaitItem()) + + settings.updateDashboardFilterCategory("test") + assertEquals("test", awaitItem()) + + settings.updateDashboardFilterCategory(null) + assertNull(awaitItem()) + } + } + + @Test + fun testDashboardFilterLocale() = runTest { + settings.getDashboardFilterLocaleFlow().test { + assertNull(awaitItem()) + + settings.updateDashboardFilterLocale(Locale.ENGLISH) + assertEquals(Locale.ENGLISH, awaitItem()) + + settings.updateDashboardFilterLocale(null) + assertNull(awaitItem()) + } + } + // endregion Dashboard Settings }