Skip to content

Commit d424c3f

Browse files
Rename MSP to Scam Blocker (#6077)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1163321984198618/task/1210238891527021?focus=true ### Description Added newThreatProtectionSettings RC flag (default enabled) in case we want to revert the changes * If on, this feature will add a threat protection entry to the settings list, and remove "Safe Site Warnings" section from "General settings". Threat protection contains smarter encryption and scam blocker * Some changes are not controlled by this flag: * General settings. Rename "Site Safety Warnings" to "Scam Blocker" and "Warn me on sites flagged for phishing or malware" to "Warn on sites flagged for scams, phishing and malware". These changes will only be visible if newThreatProtectionSettings is disabled, as otherwise this section won't show * VPN settings. Rename "Block risky domains" to "Scam Blocker" ### Steps to test this PR _Feature 1_ - [ ] Open settings - [ ] Verify there's a new top-level setting for Threat Protection - [ ] Tap on it - [ ] Toggle Scam Blocker on and off and check the setting persists. Check when the toggle is off, a new line appears at the bottom - [ ] Go back, tap on general settings - [ ] Check there's no "Site Safety Warnings" _Feature 2_ - [ ] Disable androidBrowserConfig -> newThreatProtectionSettings - [ ] Open settings - [ ] Check there's no "Threat Protection" section - [ ] Open "General" settings - [ ] Check there's a "Site Safety Warnings" section ### UI changes Note: The Threat Protection icon in top-level settings is temporary, waiting for the final asset ![before](https://github.com/user-attachments/assets/4c458a8c-cb24-4af7-9452-64dd4fb1b2ea) ![after_newsettings_off_msp_on](https://github.com/user-attachments/assets/c230d3b7-35f8-452e-9262-ada9a46f4202) ![after_newsettings_msp_on](https://github.com/user-attachments/assets/e97216ad-9d9c-47fb-8e82-edec760201ba) ![after_newsettings_on_msp_off](https://github.com/user-attachments/assets/c181ab85-6cb0-4b35-930c-0950a1a08ecc) ![after_newsettings_msp_on_msp_user_off](https://github.com/user-attachments/assets/45d82383-e1a8-44f3-9422-53d97ee46f5c) --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210271436898553 --------- Co-authored-by: Dax The Translator <[email protected]>
1 parent 5cab069 commit d424c3f

File tree

77 files changed

+1527
-181
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1527
-181
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,12 @@
448448
android:exported="false"
449449
android:label="@string/downloadsActivityTitle"
450450
android:parentActivityName=".BrowserActivity" />
451+
<activity
452+
android:name=".threatprotection.ThreatProtectionSettingsActivity"
453+
android:exported="false"
454+
android:label="Threat protection"
455+
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity"
456+
/>
451457

452458
<service
453459
android:name="com.duckduckgo.widget.FavoritesWidgetService"

app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Acti
224224
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.LeaveSite
225225
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.ReportError
226226
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.VisitSite
227+
import com.duckduckgo.app.browser.webview.SCAM_PROTECTION_LEARN_MORE_URL
227228
import com.duckduckgo.app.browser.webview.SslWarningLayout.Action
228229
import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta
229230
import com.duckduckgo.app.cta.ui.Cta
@@ -387,8 +388,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
387388
import org.json.JSONArray
388389
import org.json.JSONObject
389390

390-
private const val MALICIOUS_SITE_LEARN_MORE_URL = "https://duckduckgo.com/duckduckgo-help-pages/privacy/phishing-and-malware-protection/"
391-
private const val MALICIOUS_SITE_REPORT_ERROR_URL = "https://duckduckgo.com/malicious-site-protection/report-error?url="
391+
private const val SCAM_PROTECTION_REPORT_ERROR_URL = "https://duckduckgo.com/malicious-site-protection/report-error?url="
392392

393393
@ContributesViewModel(FragmentScope::class)
394394
class BrowserTabViewModel @Inject constructor(
@@ -2033,8 +2033,8 @@ class BrowserTabViewModel @Inject constructor(
20332033
)
20342034
}
20352035

2036-
LearnMore -> command.postValue(OpenBrokenSiteLearnMore(MALICIOUS_SITE_LEARN_MORE_URL))
2037-
ReportError -> command.postValue(ReportBrokenSiteError("$MALICIOUS_SITE_REPORT_ERROR_URL$siteUrl"))
2036+
LearnMore -> command.postValue(OpenBrokenSiteLearnMore(SCAM_PROTECTION_LEARN_MORE_URL))
2037+
ReportError -> command.postValue(ReportBrokenSiteError("$SCAM_PROTECTION_REPORT_ERROR_URL$siteUrl"))
20382038
}
20392039
}
20402040

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.threatprotection
18+
19+
import android.content.Context
20+
import android.view.View
21+
import com.duckduckgo.anvil.annotations.PriorityKey
22+
import com.duckduckgo.app.browser.R
23+
import com.duckduckgo.common.ui.view.listitem.SettingsListItem
24+
import com.duckduckgo.di.scopes.ActivityScope
25+
import com.duckduckgo.navigation.api.GlobalActivityStarter
26+
import com.duckduckgo.settings.api.ThreatProtectionSettingsPlugin
27+
import com.squareup.anvil.annotations.ContributesMultibinding
28+
import javax.inject.Inject
29+
30+
@ContributesMultibinding(ActivityScope::class)
31+
@PriorityKey(100)
32+
class ThreatProtectionSettingsTitle @Inject constructor(
33+
private val globalActivityStarter: GlobalActivityStarter,
34+
) : ThreatProtectionSettingsPlugin {
35+
override fun getView(context: Context): View {
36+
return SettingsListItem(context).apply {
37+
setLeadingIconResource(R.drawable.radar_color_24)
38+
setPrimaryText(context.getString(R.string.threatProtectionTitle))
39+
setOnClickListener {
40+
globalActivityStarter.start(this.context, ThreatProtectionSettingsNoParams, null)
41+
}
42+
setStatus(true)
43+
}
44+
}
45+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.threatprotection
18+
19+
import android.os.Bundle
20+
import android.text.SpannableStringBuilder
21+
import android.text.method.LinkMovementMethod
22+
import android.text.style.URLSpan
23+
import android.view.View
24+
import android.view.ViewGroup
25+
import android.widget.CompoundButton
26+
import androidx.annotation.StringRes
27+
import androidx.core.view.isVisible
28+
import androidx.lifecycle.Lifecycle
29+
import androidx.lifecycle.flowWithLifecycle
30+
import androidx.lifecycle.lifecycleScope
31+
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
32+
import com.duckduckgo.anvil.annotations.InjectWith
33+
import com.duckduckgo.app.browser.R
34+
import com.duckduckgo.app.browser.databinding.ActivityThreatProtectionSettingsBinding
35+
import com.duckduckgo.app.browser.threatprotection.ThreatProtectionSettingsViewModel.Command
36+
import com.duckduckgo.app.browser.threatprotection.ThreatProtectionSettingsViewModel.Command.OpenScamProtectionLearnMore
37+
import com.duckduckgo.app.browser.threatprotection.ThreatProtectionSettingsViewModel.Command.OpenSmarterEncryptionLearnMore
38+
import com.duckduckgo.app.browser.threatprotection.ThreatProtectionSettingsViewModel.Command.OpenThreatProtectionLearnMore
39+
import com.duckduckgo.app.browser.webview.SCAM_PROTECTION_LEARN_MORE_URL
40+
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
41+
import com.duckduckgo.common.ui.DuckDuckGoActivity
42+
import com.duckduckgo.common.ui.spans.DuckDuckGoClickableSpan
43+
import com.duckduckgo.common.ui.view.addClickableSpan
44+
import com.duckduckgo.common.ui.view.text.DaxTextView
45+
import com.duckduckgo.common.ui.viewbinding.viewBinding
46+
import com.duckduckgo.common.utils.extensions.html
47+
import com.duckduckgo.di.scopes.ActivityScope
48+
import com.duckduckgo.mobile.android.R as CommonR
49+
import com.duckduckgo.navigation.api.GlobalActivityStarter
50+
import javax.inject.Inject
51+
import kotlinx.coroutines.flow.launchIn
52+
import kotlinx.coroutines.flow.onEach
53+
54+
@InjectWith(ActivityScope::class)
55+
@ContributeToActivityStarter(ThreatProtectionSettingsNoParams::class)
56+
class ThreatProtectionSettingsActivity : DuckDuckGoActivity() {
57+
58+
private val viewModel: ThreatProtectionSettingsViewModel by bindViewModel()
59+
private val binding: ActivityThreatProtectionSettingsBinding by viewBinding()
60+
61+
@Inject
62+
lateinit var globalActivityStarter: GlobalActivityStarter
63+
64+
private val scamProtectionToggleListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
65+
viewModel.onScamProtectionSettingChanged(isChecked)
66+
}
67+
68+
override fun onCreate(savedInstanceState: Bundle?) {
69+
super.onCreate(savedInstanceState)
70+
71+
setContentView(binding.root)
72+
73+
/*
74+
* TwoLineListItem doesn't support links, and has an intrinsic bottom padding of keyline_2
75+
* that can't be modified by consumers. Since we want the Learn More to be placed wight below
76+
* secondary text in the toggle, with no spacing, we need to set a negative top margin to the
77+
* Learn More text view. See figma.com/design/uP27mEGEaHCI7ZYXAs1815?node-id=3232-39585&m=dev#1266019353
78+
*/
79+
(binding.scamProtectionLearnMore.layoutParams as ViewGroup.MarginLayoutParams).let {
80+
it.topMargin = -resources.getDimensionPixelSize(CommonR.dimen.keyline_2)
81+
binding.scamProtectionLearnMore.layoutParams = it
82+
}
83+
84+
setupToolbar(binding.includeToolbar.toolbar)
85+
86+
configureUiEventHandlers()
87+
observeViewModel()
88+
}
89+
90+
private fun configureUiEventHandlers() {
91+
with(binding) {
92+
scamBlockerToggle.setOnCheckedChangeListener(scamProtectionToggleListener)
93+
smarterEncryptionSettingInfo.setSpannable(R.string.smarterEncryptionDescription) { viewModel.smarterEncryptionLearnMoreClicked() }
94+
95+
binding.scamProtectionLearnMore.addClickableSpan(
96+
textSequence = getText(R.string.maliciousSiteSettingLearnMore),
97+
spans = listOf(
98+
"learn_more_link" to object : DuckDuckGoClickableSpan() {
99+
override fun onClick(widget: View) {
100+
viewModel.scamProtectionLearnMoreClicked()
101+
}
102+
},
103+
),
104+
)
105+
106+
binding.threatProtectionLearnMore.addClickableSpan(
107+
textSequence = getText(R.string.maliciousSiteSettingLearnMore),
108+
spans = listOf(
109+
"learn_more_link" to object : DuckDuckGoClickableSpan() {
110+
override fun onClick(widget: View) {
111+
viewModel.threatProtectionLearnMoreClicked()
112+
}
113+
},
114+
),
115+
)
116+
}
117+
}
118+
119+
private fun observeViewModel() {
120+
viewModel.viewState
121+
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
122+
.onEach { viewState ->
123+
viewState?.let {
124+
binding.scamBlockerDisabledMessage.isVisible = !it.scamProtectionUserEnabled && it.scamProtectionRCEnabled
125+
binding.scamProtectionLearnMore.isVisible = it.scamProtectionRCEnabled
126+
binding.scamBlockerToggle.quietlySetIsChecked(
127+
newCheckedState = it.scamProtectionUserEnabled,
128+
changeListener = scamProtectionToggleListener,
129+
)
130+
binding.scamBlockerToggle.isVisible = it.scamProtectionRCEnabled
131+
}
132+
}.launchIn(lifecycleScope)
133+
134+
viewModel.commands
135+
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
136+
.onEach { processCommand(it) }
137+
.launchIn(lifecycleScope)
138+
}
139+
140+
private fun processCommand(command: Command) {
141+
when (command) {
142+
OpenThreatProtectionLearnMore -> {
143+
globalActivityStarter.start(
144+
this,
145+
WebViewActivityWithParams(
146+
url = THREAT_PROTECTION_LEARN_MORE,
147+
screenTitle = getString(R.string.maliciousSiteLearnMoreTitle),
148+
),
149+
)
150+
}
151+
OpenScamProtectionLearnMore -> {
152+
globalActivityStarter.start(
153+
this,
154+
WebViewActivityWithParams(
155+
url = SCAM_PROTECTION_LEARN_MORE_URL,
156+
screenTitle = getString(R.string.maliciousSiteLearnMoreTitle),
157+
),
158+
)
159+
}
160+
OpenSmarterEncryptionLearnMore -> {
161+
globalActivityStarter.start(
162+
this,
163+
WebViewActivityWithParams(
164+
url = SMARTER_ENCRYPTION_LEARN_MORE,
165+
screenTitle = getString(R.string.threatProtectionLearnMoreTitle),
166+
),
167+
)
168+
}
169+
}
170+
}
171+
172+
private fun DaxTextView.setSpannable(
173+
@StringRes errorResource: Int,
174+
actionHandler: () -> Unit,
175+
) {
176+
val clickableSpan = object : DuckDuckGoClickableSpan() {
177+
override fun onClick(widget: View) {
178+
actionHandler()
179+
}
180+
}
181+
val htmlContent = context.getString(errorResource).html(context)
182+
val spannableString = SpannableStringBuilder(htmlContent)
183+
val urlSpans = htmlContent.getSpans(0, htmlContent.length, URLSpan::class.java)
184+
urlSpans?.forEach {
185+
spannableString.apply {
186+
setSpan(
187+
clickableSpan,
188+
spannableString.getSpanStart(it),
189+
spannableString.getSpanEnd(it),
190+
spannableString.getSpanFlags(it),
191+
)
192+
removeSpan(it)
193+
trim()
194+
}
195+
}
196+
text = spannableString
197+
movementMethod = LinkMovementMethod.getInstance()
198+
}
199+
200+
companion object {
201+
private const val SMARTER_ENCRYPTION_LEARN_MORE = "https://duckduckgo.com/duckduckgo-help-pages/privacy/smarter-encryption"
202+
private const val THREAT_PROTECTION_LEARN_MORE = "https://duckduckgo.com/duckduckgo-help-pages/threat-protection"
203+
}
204+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) 2023 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.threatprotection
18+
19+
import com.duckduckgo.navigation.api.GlobalActivityStarter
20+
21+
/**
22+
* Use this model to launch the Threat Protection Settings screen
23+
*/
24+
object ThreatProtectionSettingsNoParams : GlobalActivityStarter.ActivityParams

0 commit comments

Comments
 (0)