Skip to content

Add option to clear recent Duck.ai chats with Fire Button #6495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.duckduckgo.app.browser.UriString.Companion.sameOrSubdomain
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository
import com.duckduckgo.app.global.file.FileDeleter
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
Expand All @@ -47,6 +48,7 @@ class DuckDuckGoIndexedDBManager @Inject constructor(
private val fileDeleter: FileDeleter,
private val moshi: Moshi,
private val dispatcherProvider: DispatcherProvider,
private val settingsDataStore: SettingsDataStore,
) : IndexedDBManager {

private val jsonAdapter: JsonAdapter<IndexedDBSettings> by lazy {
Expand Down Expand Up @@ -81,13 +83,24 @@ class DuckDuckGoIndexedDBManager @Inject constructor(
private fun getExcludedFolders(
rootFolder: File,
allowedDomains: List<String>,
clearDuckAiData: Boolean = settingsDataStore.clearDuckAiData,
): List<String> {
return (rootFolder.listFiles() ?: emptyArray())
.filter {
// IndexedDB folders have this format: <scheme>_<host>_<port>.indexeddb.leveldb
val host = it.name.split("_").getOrNull(1) ?: return@filter false
allowedDomains.any { domain -> sameOrSubdomain(host, domain) }
val isAllowed = allowedDomains.any { domain -> sameOrSubdomain(host, domain) }

if (clearDuckAiData && sameOrSubdomain(host, DUCKDUCKGO_DOMAIN)) {
false
} else {
isAllowed
}
}
.map { it.name }
}

companion object {
const val DUCKDUCKGO_DOMAIN = "duckduckgo.com"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.duckduckgo.app.browser.weblocalstorage
import android.content.Context
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
Expand Down Expand Up @@ -48,9 +49,11 @@ class DuckDuckGoWebLocalStorageManager @Inject constructor(
private val webLocalStorageSettingsJsonParser: WebLocalStorageSettingsJsonParser,
private val fireproofWebsiteRepository: FireproofWebsiteRepository,
private val dispatcherProvider: DispatcherProvider,
private val settingsDataStore: SettingsDataStore,
) : WebLocalStorageManager {

private var domains = emptyList<String>()
private var allowedKeys = emptyList<String>()
private var matchingRegex = emptyList<String>()

override fun clearWebLocalStorage() = runBlocking {
Expand All @@ -66,9 +69,11 @@ class DuckDuckGoWebLocalStorageManager @Inject constructor(
}

domains = webLocalStorageSettings.domains.list + fireproofedDomains
allowedKeys = webLocalStorageSettings.allowedKeys.list
matchingRegex = webLocalStorageSettings.matchingRegex.list

logcat { "WebLocalStorageManager: Allowed domains: $domains" }
logcat { "WebLocalStorageManager: Allowed keys: $allowedKeys" }
logcat { "WebLocalStorageManager: Matching regex: $matchingRegex" }

val db = databaseProvider.get()
Expand All @@ -79,22 +84,35 @@ class DuckDuckGoWebLocalStorageManager @Inject constructor(
val entry = iterator.next()
val key = String(entry.key, StandardCharsets.UTF_8)

if (!isAllowedKey(key)) {
val domainForMatchingAllowedKey = getDomainForMatchingAllowedKey(key)
if (domainForMatchingAllowedKey == null) {
db.delete(entry.key)
logcat { "WebLocalStorageManager: Deleted key: $key" }
} else if (settingsDataStore.clearDuckAiData && domainForMatchingAllowedKey == DUCKDUCKGO_DOMAIN) {
if (allowedKeys.none { key.endsWith(it) }) {
db.delete(entry.key)
logcat { "WebLocalStorageManager: Deleted key: $key" }
}
}
}
}
}

private fun isAllowedKey(key: String): Boolean {
val regexPatterns = domains.flatMap { domain ->
private fun getDomainForMatchingAllowedKey(key: String): String? {
for (domain in domains) {
val escapedDomain = Regex.escape(domain)
matchingRegex.map { pattern ->
val regexPatterns = matchingRegex.map { pattern ->
pattern.replace("{domain}", escapedDomain)
}
if (regexPatterns.any { pattern -> Regex(pattern).matches(key) }) {
return domain
}
}
return regexPatterns.any { pattern -> Regex(pattern).matches(key) }
return null
}

companion object {
const val DUCKDUCKGO_DOMAIN = "duckduckgo.com"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ import javax.inject.Inject
import kotlinx.coroutines.withContext

data class Domains(val list: List<String> = emptyList())
data class AllowedKeys(val list: List<String> = emptyList())
data class MatchingRegex(val list: List<String> = emptyList())

data class WebLocalStorageSettings(
val domains: Domains = Domains(),
val allowedKeys: AllowedKeys = AllowedKeys(),
val matchingRegex: MatchingRegex = MatchingRegex(),
)

Expand All @@ -50,26 +52,32 @@ class WebLocalStorageSettingsJsonParserImpl @Inject constructor(
}

override suspend fun parseJson(json: String?): WebLocalStorageSettings = withContext(dispatcherProvider.io()) {
if (json == null) return@withContext WebLocalStorageSettings(Domains(), MatchingRegex())
if (json == null) return@withContext WebLocalStorageSettings(Domains(), AllowedKeys(), MatchingRegex())

kotlin.runCatching {
val parsed = jsonAdapter.fromJson(json)
val domains = parsed?.asDomains() ?: Domains()
val allowedKeys = parsed?.asAllowedKeys() ?: AllowedKeys()
val matchingRegex = parsed?.asMatchingRegex() ?: MatchingRegex()
WebLocalStorageSettings(domains, matchingRegex)
}.getOrDefault(WebLocalStorageSettings(Domains(), MatchingRegex()))
WebLocalStorageSettings(domains, allowedKeys, matchingRegex)
}.getOrDefault(WebLocalStorageSettings(Domains(), AllowedKeys(), MatchingRegex()))
}

private fun SettingsJson.asDomains(): Domains {
return Domains(domains.map { it })
return Domains(domains ?: emptyList())
}

private fun SettingsJson.asAllowedKeys(): AllowedKeys {
return AllowedKeys(allowedKeys ?: emptyList())
}

private fun SettingsJson.asMatchingRegex(): MatchingRegex {
return MatchingRegex(matchingRegex.map { it })
return MatchingRegex(matchingRegex ?: emptyList())
}

private data class SettingsJson(
val domains: List<String>,
val matchingRegex: List<String>,
val domains: List<String>?,
val allowedKeys: List<String>?,
val matchingRegex: List<String>?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
Expand Down Expand Up @@ -84,6 +85,7 @@ class FireButtonActivity : DuckDuckGoActivity() {
automaticallyClearWhatSetting.setClickListener { viewModel.onAutomaticallyClearWhatClicked() }
automaticallyClearWhenSetting.setClickListener { viewModel.onAutomaticallyClearWhenClicked() }
selectedFireAnimationSetting.setClickListener { viewModel.userRequestedToChangeFireAnimation() }
clearDuckAiDataSetting.setOnCheckedChangeListener { _, isChecked -> viewModel.onClearDuckAiDataToggled(isChecked) }
}
}

Expand All @@ -94,6 +96,7 @@ class FireButtonActivity : DuckDuckGoActivity() {
viewState.let {
updateAutomaticClearDataOptions(it.automaticallyClearData)
updateSelectedFireAnimation(it.selectedFireAnimation)
updateClearDuckAiDataSetting(it.clearDuckAiData, it.showClearDuckAiDataSetting)
}
}.launchIn(lifecycleScope)

Expand All @@ -119,6 +122,11 @@ class FireButtonActivity : DuckDuckGoActivity() {
binding.selectedFireAnimationSetting.setSecondaryText(subtitle)
}

private fun updateClearDuckAiDataSetting(enabled: Boolean, isVisible: Boolean) {
binding.clearDuckAiDataSetting.setIsChecked(enabled)
binding.clearDuckAiDataSetting.visibility = if (isVisible) View.VISIBLE else View.GONE
}

private fun processCommand(it: Command) {
when (it) {
is Command.LaunchFireproofWebsites -> launchFireproofWebsites()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.duckduckgo.app.settings.clear.getPixelValue
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.api.DuckChat
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
Expand All @@ -44,6 +45,7 @@ class FireButtonViewModel @Inject constructor(
private val settingsDataStore: SettingsDataStore,
private val fireAnimationLoader: FireAnimationLoader,
private val pixel: Pixel,
private val duckChat: DuckChat,
) : ViewModel() {

data class ViewState(
Expand All @@ -52,6 +54,8 @@ class FireButtonViewModel @Inject constructor(
ClearWhenOption.APP_EXIT_ONLY,
),
val selectedFireAnimation: FireAnimation = FireAnimation.HeroFire,
val clearDuckAiData: Boolean = false,
val showClearDuckAiDataSetting: Boolean = false,
)

data class AutomaticallyClearData(
Expand Down Expand Up @@ -84,6 +88,8 @@ class FireButtonViewModel @Inject constructor(
automaticallyClearWhenEnabled,
),
selectedFireAnimation = settingsDataStore.selectedFireAnimation,
clearDuckAiData = settingsDataStore.clearDuckAiData,
showClearDuckAiDataSetting = duckChat.wasOpenedBefore(),
),
)
}
Expand Down Expand Up @@ -176,6 +182,24 @@ class FireButtonViewModel @Inject constructor(
pixel.fire(pixelName)
}

fun onClearDuckAiDataToggled(enabled: Boolean) {
if (settingsDataStore.clearDuckAiData == enabled) {
logcat(VERBOSE) { "User selected same thing they already have set: clearDuckAiData=$enabled; no need to do anything else" }
return
}

settingsDataStore.clearDuckAiData = enabled
viewModelScope.launch {
viewState.emit(currentViewState().copy(clearDuckAiData = enabled))
}

if (enabled) {
pixel.fire(AppPixelName.SETTINGS_CLEAR_DUCK_AI_DATA_TOGGLED_ON)
} else {
pixel.fire(AppPixelName.SETTINGS_CLEAR_DUCK_AI_DATA_TOGGLED_OFF)
}
}

private fun ClearWhatOption.pixelEvent(): Pixel.PixelName {
return when (this) {
ClearWhatOption.CLEAR_NONE -> AppPixelName.AUTOMATIC_CLEAR_DATA_WHAT_OPTION_NONE
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
SETTINGS_PRIVATE_SEARCH_MORE_SEARCH_SETTINGS_PRESSED("ms_private_search_more_search_settings_pressed"),
SETTINGS_COOKIE_POPUP_PROTECTION_PRESSED("ms_cookie_popup_protection_setting_pressed"),
SETTINGS_FIRE_BUTTON_PRESSED("ms_fire_button_setting_pressed"),
SETTINGS_CLEAR_DUCK_AI_DATA_TOGGLED_ON("ms_clear_duck_ai_data_toggled_on"),
SETTINGS_CLEAR_DUCK_AI_DATA_TOGGLED_OFF("ms_clear_duck_ai_data_toggled_off"),
SETTINGS_GENERAL_APP_LAUNCH_PRESSED("m_settings_general_app_launch_pressed"),

SURVEY_CTA_SHOWN(pixelName = "mus_cs"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ interface SettingsDataStore {
var notifyMeInDownloadsDismissed: Boolean
var experimentalWebsiteDarkMode: Boolean
var isFullUrlEnabled: Boolean
var clearDuckAiData: Boolean

fun isCurrentlySelected(clearWhatOption: ClearWhatOption): Boolean
fun isCurrentlySelected(clearWhenOption: ClearWhenOption): Boolean
Expand Down Expand Up @@ -217,6 +218,10 @@ class SettingsSharedPreferences @Inject constructor(
get() = preferences.getBoolean(KEY_IS_FULL_URL_ENABLED, true)
set(enabled) = preferences.edit { putBoolean(KEY_IS_FULL_URL_ENABLED, enabled) }

override var clearDuckAiData: Boolean
get() = preferences.getBoolean(KEY_CLEAR_DUCK_AI_DATA, false)
set(enabled) = preferences.edit { putBoolean(KEY_CLEAR_DUCK_AI_DATA, enabled) }

override fun hasBackgroundTimestampRecorded(): Boolean = preferences.contains(KEY_APP_BACKGROUNDED_TIMESTAMP)
override fun clearAppBackgroundTimestamp() = preferences.edit { remove(KEY_APP_BACKGROUNDED_TIMESTAMP) }

Expand Down Expand Up @@ -293,6 +298,7 @@ class SettingsSharedPreferences @Inject constructor(
const val KEY_EXPERIMENTAL_SITE_DARK_MODE = "KEY_EXPERIMENTAL_SITE_DARK_MODE"
const val KEY_OMNIBAR_POSITION = "KEY_OMNIBAR_POSITION"
const val KEY_IS_FULL_URL_ENABLED = "KEY_IS_FULL_URL_ENABLED"
const val KEY_CLEAR_DUCK_AI_DATA = "KEY_CLEAR_DUCK_AI_DATA"
}

private class FireAnimationPrefsMapper {
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/layout/activity_data_clearing.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
app:primaryText="@string/settingsAutomaticallyClearWhen"
tools:secondaryText="test test test" />

<com.duckduckgo.common.ui.view.listitem.OneLineListItem
android:id="@+id/clearDuckAiDataSetting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:primaryText="@string/settingsClearAiData"
app:showSwitch="true" />

</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
<string name="settingsAutomaticallyClearWhenAppExit15Minutes">App exit, inactive for 15 minutes</string>
<string name="settingsAutomaticallyClearWhenAppExit30Minutes">App exit, inactive for 30 minutes</string>
<string name="settingsAutomaticallyClearWhenAppExit60Minutes">App exit, inactive for 1 hour</string>
<string name="settingsClearAiData">Clear recent Duck.ai chats</string>

<!-- About Activity -->
<string name="aboutActivityTitle">About DuckDuckGo</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/test/java/com/duckduckgo/app/Fakes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ class FakeSettingsDataStore : SettingsDataStore, AutoCompleteSettings {
get() = store["isFullUrlEnabled"] as Boolean? ?: false
set(value) { store["isFullUrlEnabled"] = value }

override var clearDuckAiData: Boolean
get() = store["clearDuckAiData"] as Boolean? ?: false
set(value) { store["clearDuckAiData"] = value }

override fun isCurrentlySelected(clearWhatOption: ClearWhatOption): Boolean {
val currentlySelected = store["automaticallyClearWhatOption"] as ClearWhatOption?
return currentlySelected == clearWhatOption
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.duckduckgo.app.browser.weblocalstorage.WebLocalStorageSettingsJsonPar
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.feature.toggles.api.Toggle
import dagger.Lazy
Expand Down Expand Up @@ -52,13 +53,15 @@ class DuckDuckGoWebLocalStorageManagerTest {
private val mockWebLocalStorageToggle: Toggle = mock()
private val mockFireproofedWebLocalStorageToggle: Toggle = mock()
private val mockFireproofWebsiteRepository: FireproofWebsiteRepository = mock()
private val mockSettingsDataStore: SettingsDataStore = mock()

private val testee = DuckDuckGoWebLocalStorageManager(
mockDatabaseProvider,
mockAndroidBrowserConfigFeature,
mockWebLocalStorageSettingsJsonParser,
mockFireproofWebsiteRepository,
coroutineRule.testDispatcherProvider,
mockSettingsDataStore,
)

@Before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository
import com.duckduckgo.app.global.file.FileDeleter
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.feature.toggles.api.Toggle
import com.squareup.moshi.Moshi
Expand Down Expand Up @@ -50,6 +51,7 @@ class IndexedDBManagerTest {
private val mockFireproofToggle: Toggle = mock()
private val mockFireproofRepository: FireproofWebsiteRepository = mock()
private val mockFileDeleter: FileDeleter = mock()
private val mockSettingsDataStore: SettingsDataStore = mock()
private val moshi: Moshi = Moshi.Builder().build()
private val dispatcherProvider = coroutineRule.testDispatcherProvider

Expand All @@ -61,6 +63,7 @@ class IndexedDBManagerTest {
mockFileDeleter,
moshi,
dispatcherProvider,
mockSettingsDataStore,
)
}

Expand Down
Loading
Loading