Skip to content

Commit

Permalink
Add user setting to enable/disable malicious site protection (#5579)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1207943168535187/f

### Description
Add general setting to allow user to toggle malicious site protection
on/off

### Steps to test this PR

_Feature 1_
- [x] Ensure setting is toggled to ON
- [x] Go to
https://privacy-test-pages.site/security/badware/phishing.html
- [x] Check error page is shown
- [x] Go back to setting and toggle OFF
- [x] Reload page and check it's no longer blocked and error page is not
shown
- [x] Open a new tab and go to
https://privacy-test-pages.site/security/badware/phishing.html
- [x] Check that error page is not shown

### UI changes
| Before  | After-Enabled |
| ------ | ----- |

![before](https://github.com/user-attachments/assets/1cc2787f-328f-4a9a-8238-037bdaf69656)|![after_enabled](https://github.com/user-attachments/assets/36266230-5fd4-4b70-84d2-eb56f82ae504)|
| Before | After-Disabled

![before](https://github.com/user-attachments/assets/e7c0c397-8965-43ed-9be4-2ce6585b9dc6)|![after_disabled](https://github.com/user-attachments/assets/4b57d289-026f-485f-9eee-6402d3b84287)|
  • Loading branch information
laghee authored Feb 5, 2025
1 parent 44481cb commit 990c68f
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegra
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 @@ -91,6 +92,7 @@ class RealExemptedUrlsHolder @Inject constructor() : ExemptedUrlsHolder {
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 @@ -101,6 +103,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 Down Expand Up @@ -130,7 +134,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
documentUri: Uri?,
confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit,
): IsMaliciousViewData {
if (!isFeatureEnabled) {
if (!isEnabled()) {
return IsMaliciousViewData.Safe
}
val url = request.url.let {
Expand Down Expand Up @@ -185,7 +189,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit,
): IsMaliciousViewData {
return runBlocking {
if (!isFeatureEnabled) {
if (!isEnabled()) {
return@runBlocking IsMaliciousViewData.Safe
}
val decodedUrl = URLDecoder.decode(url.toString(), "UTF-8").lowercase()
Expand Down Expand Up @@ -249,6 +253,10 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
request.url.path?.contains("/iframe/") == true ||
request.requestHeaders["Accept"]?.contains("text/html") == true

private fun isEnabled(): Boolean {
return isFeatureEnabled && isSettingEnabled
}

override fun onPageLoadStarted() {
processedUrls.clear()
}
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,17 @@ 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.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.navigation.api.GlobalActivityStarter
Expand All @@ -61,6 +66,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 +84,17 @@ 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()
}
},
),
)

configureUiEventHandlers()
observeViewModel()
}
Expand All @@ -84,6 +104,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 +126,11 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
} else {
binding.autocompleteRecentlyVisitedSitesToggle.isVisible = false
}
binding.maliciousDisabledMessage.isVisible = !it.maliciousSiteProtectionEnabled
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 +161,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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ object PixelInterceptorPixelsRequiringDataCleaning : PixelParamRemovalPlugin {
SITE_NOT_WORKING_SHOWN.pixelName to PixelParameter.removeAtb(),
SITE_NOT_WORKING_WEBSITE_BROKEN.pixelName to PixelParameter.removeAtb(),
AppPixelName.APP_VERSION_AT_SEARCH_TIME.pixelName to PixelParameter.removeAll(),
AppPixelName.MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED.pixelName to PixelParameter.removeAtb(),
)
}
}
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 @@ -363,6 +363,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
42 changes: 42 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,48 @@
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" />

<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"
android:textColor="?daxColorDestructive"
app:typography="body2" />

</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

0 comments on commit 990c68f

Please sign in to comment.