Skip to content

Commit

Permalink
Add FF for enabling behavioral toast report
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisBarreiro committed Oct 29, 2024
1 parent 54fffec commit 54fb184
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 2 deletions.
2 changes: 2 additions & 0 deletions broken-site/broken-site-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ dependencies {
implementation Square.retrofit2.converter.moshi
implementation Square.okHttp3.okHttp

implementation AndroidX.dataStore.preferences

// Testing dependencies
testImplementation "org.mockito.kotlin:mockito-kotlin:_"
testImplementation Testing.junit4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* 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
*
* http://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.duckduckgo.brokensite.impl

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.brokensite.impl.SharedPreferencesDuckPlayerDataStore.Keys.COOL_DOWN_DAYS
import com.duckduckgo.brokensite.impl.SharedPreferencesDuckPlayerDataStore.Keys.DISMISS_STREAK_RESET_DAYS
import com.duckduckgo.brokensite.impl.SharedPreferencesDuckPlayerDataStore.Keys.MAX_DISMISS_STREAK
import com.duckduckgo.brokensite.impl.di.BrokenSitePrompt
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

interface BrokenSitePomptDataStore {
suspend fun setMaxDismissStreak(maxDismissStreak: Int)
fun getMaxDismissStreak(): Int

suspend fun setDismissStreakResetDays(days: Int)
fun getDismissStreakResetDays(): Int

suspend fun setCoolDownDays(days: Int)
fun getCoolDownDays(): Int
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
class SharedPreferencesDuckPlayerDataStore @Inject constructor(
@BrokenSitePrompt private val store: DataStore<Preferences>,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : BrokenSitePomptDataStore {

private object Keys {
val MAX_DISMISS_STREAK = intPreferencesKey(name = "MAX_DISMISS_STREAK")
val DISMISS_STREAK_RESET_DAYS = intPreferencesKey(name = "DISMISS_STREAK_RESET_DAYS")
val COOL_DOWN_DAYS = intPreferencesKey(name = "COOL_DOWN_DAYS")
}

private val maxDismissStreak: StateFlow<Int> = store.data
.map { prefs ->
prefs[MAX_DISMISS_STREAK] ?: 3
}
.distinctUntilChanged()
.stateIn(appCoroutineScope, SharingStarted.Eagerly, 3)

private val dismissStreakResetDays: StateFlow<Int> = store.data
.map { prefs ->
prefs[DISMISS_STREAK_RESET_DAYS] ?: 30
}
.distinctUntilChanged()
.stateIn(appCoroutineScope, SharingStarted.Eagerly, 30)

private val coolDownDays: StateFlow<Int> = store.data
.map { prefs ->
prefs[COOL_DOWN_DAYS] ?: 7
}
.distinctUntilChanged()
.stateIn(appCoroutineScope, SharingStarted.Eagerly, 7)

override suspend fun setMaxDismissStreak(maxDismissStreak: Int) {
store.edit { prefs -> prefs[MAX_DISMISS_STREAK] = maxDismissStreak }
}

override fun getMaxDismissStreak(): Int = maxDismissStreak.value

override suspend fun setDismissStreakResetDays(days: Int) {
store.edit { prefs -> prefs[DISMISS_STREAK_RESET_DAYS] = days }
}

override fun getDismissStreakResetDays(): Int = dismissStreakResetDays.value

override suspend fun setCoolDownDays(days: Int) {
store.edit { prefs -> prefs[COOL_DOWN_DAYS] = days }
}

override fun getCoolDownDays(): Int = coolDownDays.value
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,61 @@

package com.duckduckgo.brokensite.impl

import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.feature.toggles.api.FeatureSettings
import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.feature.toggles.api.Toggle.DefaultValue
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@ContributesRemoteFeature(
scope = AppScope::class,
featureName = "brokenSitePrompt",
settingsStore = BrokenSitePromptRCFeatureStore::class,
)
interface BrokenSitePromptRCFeature {
// TODO (cbarreiro) Implement broken site prompt
@DefaultValue(false)
fun self(): Toggle
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
@RemoteFeatureStoreNamed(BrokenSitePromptRCFeature::class)
class BrokenSitePromptRCFeatureStore @Inject constructor(
private val repository: BrokenSiteReportRepository,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
) : FeatureSettings.Store {

private val jsonAdapter by lazy { buildJsonAdapter() }

override fun store(jsonString: String) {
appCoroutineScope.launch(dispatcherProvider.io()) {
jsonAdapter.fromJson(jsonString)?.let {
repository.setBrokenSitePromptRCSettings(it.maxDismissStreak, it.dismissStreakResetDays, it.coolDownDays)
}
}
}

private fun buildJsonAdapter(): JsonAdapter<BrokenSitePromptSettings> {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
return moshi.adapter(BrokenSitePromptSettings::class.java)
}

data class BrokenSitePromptSettings(
val maxDismissStreak: Int,
val dismissStreakResetDays: Int,
val coolDownDays: Int,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,24 @@ interface BrokenSiteReportRepository {
fun setLastSentDay(hostname: String)

fun cleanupOldEntries()

suspend fun setMaxDismissStreak(maxDismissStreak: Int)
fun getMaxDismissStreak(): Int

suspend fun setDismissStreakResetDays(days: Int)
fun getDismissStreakResetDays(): Int

suspend fun setCoolDownDays(days: Int)
fun getCoolDownDays(): Int

suspend fun setBrokenSitePromptRCSettings(maxDismissStreak: Int, dismissStreakResetDays: Int, coolDownDays: Int)
}

class RealBrokenSiteReportRepository constructor(
class RealBrokenSiteReportRepository(
private val database: BrokenSiteDatabase,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
private val brokenSitePromptDataStore: BrokenSitePomptDataStore,
) : BrokenSiteReportRepository {

override suspend fun getLastSentDay(hostname: String): String? {
Expand Down Expand Up @@ -74,6 +86,37 @@ class RealBrokenSiteReportRepository constructor(
}
}

override suspend fun setMaxDismissStreak(maxDismissStreak: Int) {
brokenSitePromptDataStore.setMaxDismissStreak(maxDismissStreak)
}

override fun getMaxDismissStreak(): Int =
brokenSitePromptDataStore.getMaxDismissStreak()

override suspend fun setDismissStreakResetDays(days: Int) {
brokenSitePromptDataStore.setDismissStreakResetDays(days)
}

override fun getDismissStreakResetDays(): Int =
brokenSitePromptDataStore.getDismissStreakResetDays()

override suspend fun setCoolDownDays(days: Int) {
brokenSitePromptDataStore.setCoolDownDays(days)
}

override fun getCoolDownDays(): Int =
brokenSitePromptDataStore.getCoolDownDays()

override suspend fun setBrokenSitePromptRCSettings(
maxDismissStreak: Int,
dismissStreakResetDays: Int,
coolDownDays: Int,
) {
setMaxDismissStreak(maxDismissStreak)
setDismissStreakResetDays(dismissStreakResetDays)
setCoolDownDays(coolDownDays)
}

private fun convertToShortDate(dateString: String): String {
val inputFormatter = DateTimeFormatter.ISO_INSTANT
val outputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.duckduckgo.brokensite.impl.di
import android.content.Context
import androidx.room.Room
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.brokensite.impl.BrokenSitePomptDataStore
import com.duckduckgo.brokensite.impl.BrokenSiteReportRepository
import com.duckduckgo.brokensite.impl.RealBrokenSiteReportRepository
import com.duckduckgo.brokensite.store.ALL_MIGRATIONS
Expand All @@ -41,8 +42,9 @@ class BrokenSiteModule {
database: BrokenSiteDatabase,
@AppCoroutineScope coroutineScope: CoroutineScope,
dispatcherProvider: DispatcherProvider,
brokenSitePomptDataStore: BrokenSitePomptDataStore,
): BrokenSiteReportRepository {
return RealBrokenSiteReportRepository(database, coroutineScope, dispatcherProvider)
return RealBrokenSiteReportRepository(database, coroutineScope, dispatcherProvider, brokenSitePomptDataStore)
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* 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
*
* http://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.duckduckgo.brokensite.impl.di

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import javax.inject.Qualifier

@ContributesTo(AppScope::class)
@Module
object DuckPlayerDataStoreModule {

private val Context.brokenSitePromptDataStore: DataStore<Preferences> by preferencesDataStore(
name = "broken_site_prompt",
)

@Provides
@BrokenSitePrompt
fun provideBrokenSitePromptDataStore(context: Context): DataStore<Preferences> = context.brokenSitePromptDataStore
}

@Qualifier
internal annotation class BrokenSitePrompt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class RealBrokenSiteReportRepositoryTest {

private val mockDatabase: BrokenSiteDatabase = mock()
private val mockBrokenSiteDao: BrokenSiteDao = mock()
private val mockDataStore: BrokenSitePomptDataStore = mock()
lateinit var testee: RealBrokenSiteReportRepository

@Before
Expand All @@ -48,6 +49,7 @@ class RealBrokenSiteReportRepositoryTest {
database = mockDatabase,
coroutineScope = coroutineRule.testScope,
dispatcherProvider = coroutineRule.testDispatcherProvider,
brokenSitePromptDataStore = mockDataStore,
)
}

Expand Down Expand Up @@ -109,4 +111,31 @@ class RealBrokenSiteReportRepositoryTest {

verify(mockDatabase.brokenSiteDao()).deleteAllExpiredReports(any())
}

@Test
fun whenSetMaxDismissStreakCalledThenSetMaxDismissStreakIsCalled() = runTest {
val maxDismissStreak = 3

testee.setMaxDismissStreak(maxDismissStreak)

verify(mockDataStore).setMaxDismissStreak(maxDismissStreak)
}

@Test
fun whenDismissStreakResetDaysCalledThenDismissStreakResetDaysIsCalled() = runTest {
val days = 30

testee.setDismissStreakResetDays(days)

verify(mockDataStore).setDismissStreakResetDays(days)
}

@Test
fun whenCoolDownDaysCalledThenCoolDownDaysIsCalled() = runTest {
val days = 7

testee.setCoolDownDays(days)

verify(mockDataStore).setCoolDownDays(days)
}
}

0 comments on commit 54fb184

Please sign in to comment.