Skip to content
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

Add user setting to enable/disable malicious site protection #5579

Merged
merged 10 commits into from
Feb 5, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.core.net.toUri
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.di.IsMainProcess
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.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection
Expand Down Expand Up @@ -69,6 +70,7 @@ class ExemptedUrlsHolder @Inject constructor() {
class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
private val maliciousSiteProtection: MaliciousSiteProtection,
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature,
private val settingsDataStore: SettingsDataStore,
private val dispatchers: DispatcherProvider,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val exemptedUrlsHolder: ExemptedUrlsHolder,
Expand All @@ -79,6 +81,8 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
val processedUrls = mutableListOf<String>()

private var isFeatureEnabled = false
private val isSettingEnabled: Boolean
get() = settingsDataStore.maliciousSiteProtectionEnabled
private var currentCheckId = AtomicInteger(0)

init {
Expand All @@ -102,7 +106,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
documentUri: Uri?,
confirmationCallback: (isMalicious: Boolean) -> Unit,
): WebResourceResponse? {
if (!isFeatureEnabled) {
if (!isFeatureEnabled || !isSettingEnabled) {
laghee marked this conversation as resolved.
Show resolved Hide resolved
return null
}
val url = request.url.let {
Expand Down Expand Up @@ -143,7 +147,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
confirmationCallback: (isMalicious: Boolean) -> Unit,
): Boolean {
return runBlocking {
if (!isFeatureEnabled) {
if (!isFeatureEnabled || !isSettingEnabled) {
return@runBlocking false
}
val decodedUrl = URLDecoder.decode(url.toString(), "UTF-8").lowercase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.duckduckgo.app.generalsettings

import android.os.Bundle
import android.view.View
import android.view.View.OnClickListener
import android.widget.CompoundButton
import androidx.core.view.isVisible
Expand All @@ -29,13 +30,18 @@ import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ActivityGeneralSettingsBinding
import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command
import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.LaunchShowOnAppLaunchScreen
import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.OpenMaliciousLearnMore
import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchScreenNoParams
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage
import com.duckduckgo.app.global.view.fadeTransitionConfig
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.spans.DuckDuckGoClickableSpan
import com.duckduckgo.common.ui.view.addClickableSpan
import com.duckduckgo.common.ui.view.getColorFromAttr
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.navigation.api.GlobalActivityStarter
Expand All @@ -61,6 +67,10 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
viewModel.onAutocompleteRecentlyVisitedSitesSettingChanged(isChecked)
}

private val maliciousSiteProtectionToggleListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
viewModel.onMaliciousSiteProtectionSettingChanged(isChecked)
}

private val voiceSearchChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
viewModel.onVoiceSearchChanged(isChecked)
}
Expand All @@ -75,6 +85,18 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
setContentView(binding.root)
setupToolbar(binding.includeToolbar.toolbar)

binding.maliciousLearnMore.addClickableSpan(
textSequence = getText(R.string.maliciousSiteSettingLearnMore),
spans = listOf(
"learn_more_link" to object : DuckDuckGoClickableSpan() {
override fun onClick(widget: View) {
viewModel.maliciousSiteLearnMoreClicked()
}
},
),
)
binding.maliciousDisabledMessage.setTextColor(getColorFromAttr(com.duckduckgo.mobile.android.R.attr.daxColorDestructive))
laghee marked this conversation as resolved.
Show resolved Hide resolved

configureUiEventHandlers()
observeViewModel()
}
Expand All @@ -84,6 +106,7 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
binding.autocompleteRecentlyVisitedSitesToggle.setOnCheckedChangeListener(autocompleteRecentlyVisitedSitesToggleListener)
binding.voiceSearchToggle.setOnCheckedChangeListener(voiceSearchChangeListener)
binding.showOnAppLaunchButton.setOnClickListener(showOnAppLaunchClickListener)
binding.maliciousToggle.setOnCheckedChangeListener(maliciousSiteProtectionToggleListener)
}

private fun observeViewModel() {
Expand All @@ -105,6 +128,11 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
} else {
binding.autocompleteRecentlyVisitedSitesToggle.isVisible = false
}
binding.maliciousDisabledMessage.visibility = if (it.maliciousSiteProtectionEnabled) View.GONE else View.VISIBLE
laghee marked this conversation as resolved.
Show resolved Hide resolved
binding.maliciousToggle.quietlySetIsChecked(
newCheckedState = it.maliciousSiteProtectionEnabled,
changeListener = maliciousSiteProtectionToggleListener,
)
if (it.showVoiceSearch) {
binding.voiceSearchToggle.isVisible = true
binding.voiceSearchToggle.quietlySetIsChecked(viewState.voiceSearchEnabled, voiceSearchChangeListener)
Expand Down Expand Up @@ -135,6 +163,19 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
LaunchShowOnAppLaunchScreen -> {
globalActivityStarter.start(this, ShowOnAppLaunchScreenNoParams, fadeTransitionConfig())
}
OpenMaliciousLearnMore -> {
globalActivityStarter.start(
this,
WebViewActivityWithParams(
url = MALICIOUS_SITE_LEARN_MORE_URL,
screenTitle = getString(R.string.maliciousSiteLearnMoreTitle),
),
)
}
}
}

companion object {
private const val MALICIOUS_SITE_LEARN_MORE_URL = "https://duckduckgo.com/duckduckgo-help-pages/privacy/phishing-and-malware-protection/"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ class GeneralSettingsViewModel @Inject constructor(
val voiceSearchEnabled: Boolean,
val isShowOnAppLaunchOptionVisible: Boolean,
val showOnAppLaunchSelectedOption: ShowOnAppLaunchOption,
val maliciousSiteProtectionEnabled: Boolean,
)

sealed class Command {
data object LaunchShowOnAppLaunchScreen : Command()
data object OpenMaliciousLearnMore : Command()
}

private val _viewState = MutableStateFlow<ViewState?>(null)
Expand All @@ -95,6 +97,7 @@ class GeneralSettingsViewModel @Inject constructor(
voiceSearchEnabled = voiceSearchAvailability.isVoiceSearchAvailable,
isShowOnAppLaunchOptionVisible = showOnAppLaunchFeature.self().isEnabled(),
showOnAppLaunchSelectedOption = showOnAppLaunchOptionDataStore.optionFlow.first(),
maliciousSiteProtectionEnabled = settingsDataStore.maliciousSiteProtectionEnabled,
)
}

Expand Down Expand Up @@ -151,6 +154,24 @@ class GeneralSettingsViewModel @Inject constructor(
pixel.fire(AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED)
}

fun onMaliciousSiteProtectionSettingChanged(enabled: Boolean) {
Timber.i("User changed malicious site setting, is now enabled: $enabled")
viewModelScope.launch(dispatcherProvider.io()) {
settingsDataStore.maliciousSiteProtectionEnabled = enabled
pixel.fire(
AppPixelName.MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED,
mapOf(NEW_STATE to enabled.toString()),
)
_viewState.value = _viewState.value?.copy(
maliciousSiteProtectionEnabled = enabled,
)
}
}

fun maliciousSiteLearnMoreClicked() {
sendCommand(Command.OpenMaliciousLearnMore)
}

private fun observeShowOnAppLaunchOption() {
showOnAppLaunchOptionDataStore.optionFlow
.onEach { showOnAppLaunchOption ->
Expand All @@ -163,4 +184,8 @@ class GeneralSettingsViewModel @Inject constructor(
_commands.send(newCommand)
}
}

companion object {
private const val NEW_STATE = "newState"
}
}
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 @@ -361,6 +361,8 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE("duckplayer_setting_never_overlay_youtube"),
DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER("duckplayer_setting_always_duck-player"),

MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED("m_malicious-site-protection_feature-toggled"),

ADD_BOOKMARK_CONFIRM_EDITED("m_add_bookmark_confirm_edit"),

REFERRAL_INSTALL_UTM_CAMPAIGN("m_android_install"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface SettingsDataStore {
@Deprecated(message = "hideTips variable is deprecated and no longer available in onboarding")
var hideTips: Boolean
var autoCompleteSuggestionsEnabled: Boolean
var maliciousSiteProtectionEnabled: Boolean
var appIcon: AppIcon
var selectedFireAnimation: FireAnimation
val fireAnimationEnabled: Boolean
Expand Down Expand Up @@ -117,6 +118,10 @@ class SettingsSharedPreferences @Inject constructor(
get() = preferences.getBoolean(KEY_AUTOCOMPLETE_ENABLED, true)
set(enabled) = preferences.edit { putBoolean(KEY_AUTOCOMPLETE_ENABLED, enabled) }

override var maliciousSiteProtectionEnabled: Boolean
get() = preferences.getBoolean(KEY_MALICIOUS_SITE_PROTECTION_ENABLED, true)
set(enabled) = preferences.edit { putBoolean(KEY_MALICIOUS_SITE_PROTECTION_ENABLED, enabled) }

override var appLoginDetection: Boolean
get() = preferences.getBoolean("KEY_LOGIN_DETECTION_ENABLED", true)
set(enabled) = preferences.edit { putBoolean("KEY_LOGIN_DETECTION_ENABLED", enabled) }
Expand Down Expand Up @@ -252,6 +257,7 @@ class SettingsSharedPreferences @Inject constructor(
const val FILENAME = "com.duckduckgo.app.settings_activity.settings"
const val KEY_BACKGROUND_JOB_ID = "BACKGROUND_JOB_ID"
const val KEY_AUTOCOMPLETE_ENABLED = "AUTOCOMPLETE_ENABLED"
const val KEY_MALICIOUS_SITE_PROTECTION_ENABLED = "MALICIOUS_SITE_PROTECTION_ENABLED"
const val KEY_AUTOMATIC_FIREPROOF_SETTING = "KEY_AUTOMATIC_FIREPROOF_SETTING"
const val KEY_AUTOMATICALLY_CLEAR_WHAT_OPTION = "AUTOMATICALLY_CLEAR_WHAT_OPTION"
const val KEY_AUTOMATICALLY_CLEAR_WHEN_OPTION = "AUTOMATICALLY_CLEAR_WHEN_OPTION"
Expand Down
43 changes: 43 additions & 0 deletions app/src/main/res/layout/activity_general_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,49 @@
app:primaryText="@string/showOnAppLaunchOptionTitle"
tools:secondaryText="Last Opened Tab" />

<com.duckduckgo.common.ui.view.divider.HorizontalDivider
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
android:id="@+id/maliciousSiteHeading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_4"
app:primaryText="@string/maliciousSiteSettingTitle" />

<com.duckduckgo.common.ui.view.listitem.OneLineListItem
android:id="@+id/maliciousToggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
app:primaryText="@string/maliciousSiteToggleHint"
app:primaryTextTruncated="false"
app:showSwitch="true" />

<com.duckduckgo.common.ui.view.text.DaxTextView
android:id="@+id/maliciousLearnMore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/keyline_4"
android:layout_marginEnd="@dimen/keyline_4"
android:text="@string/maliciousSiteSettingLearnMore"
app:typography="body2"
app:textType="secondary" />

<com.duckduckgo.common.ui.view.text.DaxTextView
android:id="@+id/maliciousDisabledMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_4"
android:layout_marginStart="@dimen/keyline_4"
android:layout_marginEnd="@dimen/keyline_4"
android:text="@string/maliciousSiteSettingDisabled"
android:visibility="visible"
app:typography="body2"
app:textType="secondary" />

</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
5 changes: 5 additions & 0 deletions app/src/main/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
<string name="maliciousSiteExpandedCTA"><![CDATA[<u>Accept Risk and Visit Site</u>]]></string>
<string name="maliciousSiteReportErrorTitle">Report a site incorrectly flagged as malicious</string>
<string name="maliciousSiteLearnMoreTitle">Learn more</string>
<string name="maliciousSiteSettingTitle">Site Safety Warnings</string>
<string name="maliciousSiteToggleHint">Warn me on sites flagged for phishing or malware</string>
<string name="maliciousSiteSettingLearnMore"><annotation type="learn_more_link">Learn More</annotation></string>
<string name="maliciousSiteSettingDisabled">Disabling this feature can put your information at risk.</string>


<!-- Broken Sites-->
<string name="brokenSitesLoginHint">What site are you signing in to? (required)</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 @@ -41,6 +41,10 @@ class FakeSettingsDataStore : SettingsDataStore {
get() = store["autoCompleteSuggestionsEnabled"] as Boolean? ?: true
set(value) { store["autoCompleteSuggestionsEnabled"] = value }

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

@Deprecated("Not used anymore after adding automatic fireproof", replaceWith = ReplaceWith("automaticFireproofSetting"))
override var appLoginDetection: Boolean
get() = store["appLoginDetection"] as Boolean? ?: true
Expand Down
Loading
Loading