Skip to content

Commit 391ab0a

Browse files
landomendaxmobile
andauthored
[Web Tracking Protection] Add staggered grid of web protections (#6759)
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1211287284919898?focus=true ### Description Adds a staggered grid of protections that are always on to Web Tracking Protection screen. _**Note:** this is the main feature branch where two previous PRs were merged to (#6708 and #6723). All the code has already been reviewed and approved._ ### Steps to test this PR - [x] Go to Settings -> Web Tracking Protection - [x] Verify the description under the "Web Tracking Protection" title matches [copy spec](https://app.asana.com/1/137249556945/project/38424471409662/task/1210874321016131?focus=true) - [x] Verify you see the grid of new protection cards/items - [x] Verify that the design (margins, paddings, text styles, icons) matches [Figma](https://www.figma.com/design/ir88vAgIL2Mz87BBFiTAQB/O-E-Protections-List?node-id=245-5321&m=dev) - [x] Verify the copies matches [copy spec](https://app.asana.com/1/137249556945/project/38424471409662/task/1210874321016131?focus=true) - [x] Rotate device to landscape and verify that layout and paddings are still correct - [x] Go to Settings -> Appearance and change theme (light/dark) to opposite of what you had now - [x] Verify the designs match in both themes [Figma](https://www.figma.com/design/ir88vAgIL2Mz87BBFiTAQB/O-E-Protections-List?node-id=245-5321&m=dev) ### UI changes | Before | After (light) | After (dark) | | - | - | - | | <img src="https://github.com/user-attachments/assets/732ffe21-a203-434c-9b97-f43a9c17f42e" /> | <img src="https://github.com/user-attachments/assets/0cf46ead-ba45-445f-8a3c-d7d2406f6b16" /> | <img src="https://github.com/user-attachments/assets/7a128369-a016-47e0-9dc9-4b1c085ee297" /> | --------- Co-authored-by: Dax The Translator <[email protected]>
1 parent 3db2951 commit 391ab0a

Some content is hidden

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

44 files changed

+1261
-35
lines changed

app/src/main/java/com/duckduckgo/app/webtrackingprotection/WebTrackingProtectionActivity.kt

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package com.duckduckgo.app.webtrackingprotection
1818

19-
import android.R.attr.text
2019
import android.app.ActivityOptions
2120
import android.os.Bundle
2221
import android.text.SpannableStringBuilder
@@ -25,16 +24,21 @@ import android.text.method.LinkMovementMethod
2524
import android.text.style.ClickableSpan
2625
import android.text.style.URLSpan
2726
import android.view.View
27+
import androidx.annotation.StringRes
28+
import androidx.core.view.isVisible
2829
import androidx.lifecycle.Lifecycle
2930
import androidx.lifecycle.flowWithLifecycle
3031
import androidx.lifecycle.lifecycleScope
32+
import androidx.recyclerview.widget.StaggeredGridLayoutManager
3133
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
3234
import com.duckduckgo.anvil.annotations.InjectWith
3335
import com.duckduckgo.app.browser.R
3436
import com.duckduckgo.app.browser.databinding.ActivityWebTrackingProtectionBinding
3537
import com.duckduckgo.app.globalprivacycontrol.ui.GlobalPrivacyControlActivity
3638
import com.duckduckgo.app.privacy.ui.AllowListActivity
3739
import com.duckduckgo.app.webtrackingprotection.WebTrackingProtectionViewModel.Command
40+
import com.duckduckgo.app.webtrackingprotection.list.FeatureGridAdapter
41+
import com.duckduckgo.app.webtrackingprotection.list.FeatureGridItem
3842
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
3943
import com.duckduckgo.common.ui.DuckDuckGoActivity
4044
import com.duckduckgo.common.ui.view.getColorFromAttr
@@ -54,8 +58,12 @@ class WebTrackingProtectionActivity : DuckDuckGoActivity() {
5458
@Inject
5559
lateinit var globalActivityStarter: GlobalActivityStarter
5660

61+
@Inject
62+
lateinit var webTrackingProtectionsGridFeature: WebTrackingProtectionsGridFeature
63+
5764
private val viewModel: WebTrackingProtectionViewModel by bindViewModel()
5865
private val binding: ActivityWebTrackingProtectionBinding by viewBinding()
66+
private lateinit var gridAdapter: FeatureGridAdapter
5967

6068
// TODO eligible for extraction and use in AutoConsent as well when removing old settings
6169
private val clickableSpan = object : ClickableSpan() {
@@ -78,6 +86,7 @@ class WebTrackingProtectionActivity : DuckDuckGoActivity() {
7886

7987
configureUiEventHandlers()
8088
configureClickableLink()
89+
configureGridList()
8190
observeViewModel()
8291
}
8392

@@ -87,9 +96,13 @@ class WebTrackingProtectionActivity : DuckDuckGoActivity() {
8796
}
8897

8998
private fun configureClickableLink() {
90-
val htmlGPCText = getString(
91-
R.string.webTrackingProtectionDescriptionNew,
92-
).html(this)
99+
@StringRes val descriptionStringRes = if (webTrackingProtectionsGridFeature.self().isEnabled()) {
100+
R.string.webTrackingProtectionExplanationDescription
101+
} else {
102+
R.string.webTrackingProtectionDescriptionNew
103+
}
104+
105+
val htmlGPCText = getString(descriptionStringRes).html(this)
93106
val gpcSpannableString = SpannableStringBuilder(htmlGPCText)
94107
val urlSpans = htmlGPCText.getSpans(0, htmlGPCText.length, URLSpan::class.java)
95108
urlSpans?.forEach {
@@ -111,11 +124,28 @@ class WebTrackingProtectionActivity : DuckDuckGoActivity() {
111124
}
112125
}
113126

127+
private fun configureGridList() {
128+
if (!webTrackingProtectionsGridFeature.self().isEnabled()) {
129+
return
130+
}
131+
132+
binding.protectionsListDivider.isVisible = true
133+
binding.protectionsTitle.isVisible = true
134+
binding.protectionsList.isVisible = true
135+
136+
val columnCount = resources.getInteger(R.integer.web_tracking_protection_grid_column_count)
137+
val layoutManager = StaggeredGridLayoutManager(columnCount, StaggeredGridLayoutManager.VERTICAL)
138+
binding.protectionsList.layoutManager = layoutManager
139+
gridAdapter = FeatureGridAdapter()
140+
binding.protectionsList.adapter = gridAdapter
141+
}
142+
114143
private fun observeViewModel() {
115144
viewModel.viewState()
116145
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
117146
.onEach { viewState ->
118147
setGlobalPrivacyControlSetting(viewState.globalPrivacyControlEnabled)
148+
updateProtectionsGridList(viewState.protectionItems)
119149
}.launchIn(lifecycleScope)
120150

121151
viewModel.commands()
@@ -160,4 +190,12 @@ class WebTrackingProtectionActivity : DuckDuckGoActivity() {
160190
val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
161191
startActivity(GlobalPrivacyControlActivity.intent(this), options)
162192
}
193+
194+
private fun updateProtectionsGridList(protectionItems: List<FeatureGridItem>) {
195+
if (!webTrackingProtectionsGridFeature.self().isEnabled()) {
196+
return
197+
}
198+
199+
gridAdapter.submitList(protectionItems)
200+
}
163201
}

app/src/main/java/com/duckduckgo/app/webtrackingprotection/WebTrackingProtectionViewModel.kt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ package com.duckduckgo.app.webtrackingprotection
1919
import androidx.lifecycle.ViewModel
2020
import androidx.lifecycle.viewModelScope
2121
import com.duckduckgo.anvil.annotations.ContributesViewModel
22+
import com.duckduckgo.app.browser.R
2223
import com.duckduckgo.app.pixels.AppPixelName
2324
import com.duckduckgo.app.statistics.pixels.Pixel
25+
import com.duckduckgo.app.webtrackingprotection.list.FeatureGridItem
2426
import com.duckduckgo.di.scopes.ActivityScope
2527
import com.duckduckgo.feature.toggles.api.FeatureToggle
2628
import com.duckduckgo.privacy.config.api.Gpc
@@ -39,10 +41,12 @@ class WebTrackingProtectionViewModel @Inject constructor(
3941
private val gpc: Gpc,
4042
private val featureToggle: FeatureToggle,
4143
private val pixel: Pixel,
44+
private val webTrackingProtectionsGridFeature: WebTrackingProtectionsGridFeature,
4245
) : ViewModel() {
4346

4447
data class ViewState(
4548
val globalPrivacyControlEnabled: Boolean = false,
49+
val protectionItems: List<FeatureGridItem> = emptyList(),
4650
)
4751

4852
sealed class Command {
@@ -59,6 +63,7 @@ class WebTrackingProtectionViewModel @Inject constructor(
5963
viewState.emit(
6064
ViewState(
6165
globalPrivacyControlEnabled = gpc.isEnabled() && featureToggle.isFeatureEnabled(PrivacyFeatureName.GpcFeatureName.value),
66+
protectionItems = getProtectionItems(),
6267
),
6368
)
6469
}
@@ -82,6 +87,60 @@ class WebTrackingProtectionViewModel @Inject constructor(
8287
pixel.fire(AppPixelName.SETTINGS_MANAGE_ALLOWLIST)
8388
}
8489

90+
private fun getProtectionItems(): List<FeatureGridItem> {
91+
if (!webTrackingProtectionsGridFeature.self().isEnabled()) {
92+
return emptyList()
93+
}
94+
95+
return listOf(
96+
FeatureGridItem(
97+
iconRes = R.drawable.ic_shield_protection,
98+
titleRes = R.string.webTrackingProtectionThirdPartyTrackersTitle,
99+
descriptionRes = R.string.webTrackingProtectionThirdPartyTrackersDescription,
100+
),
101+
FeatureGridItem(
102+
iconRes = R.drawable.ic_ads_tracking_blocked,
103+
titleRes = R.string.webTrackingProtectionTargetedAdsTitle,
104+
descriptionRes = R.string.webTrackingProtectionTargetedAdsDescription,
105+
),
106+
FeatureGridItem(
107+
iconRes = R.drawable.ic_fingerprint,
108+
titleRes = R.string.webTrackingProtectionFingerprintTrackingTitle,
109+
descriptionRes = R.string.webTrackingProtectionFingerprintTrackingDescription,
110+
),
111+
FeatureGridItem(
112+
iconRes = R.drawable.ic_link_blocked,
113+
titleRes = R.string.webTrackingProtectionLinkTrackingTitle,
114+
descriptionRes = R.string.webTrackingProtectionLinkTrackingDescription,
115+
),
116+
FeatureGridItem(
117+
iconRes = R.drawable.ic_profile_secure,
118+
titleRes = R.string.webTrackingProtectionReferrerTrackingTitle,
119+
descriptionRes = R.string.webTrackingProtectionReferrerTrackingDescription,
120+
),
121+
FeatureGridItem(
122+
iconRes = R.drawable.ic_cookie_blocked,
123+
titleRes = R.string.webTrackingProtectionFirstPartyCookiesTitle,
124+
descriptionRes = R.string.webTrackingProtectionFirstPartyCookiesDescription,
125+
),
126+
FeatureGridItem(
127+
iconRes = R.drawable.ic_device_laptop_secure,
128+
titleRes = R.string.webTrackingProtectionDnsCnameCloakingTitle,
129+
descriptionRes = R.string.webTrackingProtectionDnsCnameCloakingDescription,
130+
),
131+
FeatureGridItem(
132+
iconRes = R.drawable.ic_eye_blocked,
133+
titleRes = R.string.webTrackingProtectionGoogleAmpTrackingTitle,
134+
descriptionRes = R.string.webTrackingProtectionGoogleAmpTrackingDescription,
135+
),
136+
FeatureGridItem(
137+
iconRes = R.drawable.ic_popup_blocked,
138+
titleRes = R.string.webTrackingProtectionGoogleSigninPopupsTitle,
139+
descriptionRes = R.string.webTrackingProtectionGoogleSigninPopupsDescription,
140+
),
141+
)
142+
}
143+
85144
companion object {
86145
const val LEARN_MORE_URL = "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/"
87146
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.webtrackingprotection
18+
19+
import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
20+
import com.duckduckgo.di.scopes.AppScope
21+
import com.duckduckgo.feature.toggles.api.Toggle
22+
import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
23+
24+
@ContributesRemoteFeature(
25+
scope = AppScope::class,
26+
featureName = "webTrackingProtectionsGrid",
27+
)
28+
interface WebTrackingProtectionsGridFeature {
29+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
30+
fun self(): Toggle
31+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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.webtrackingprotection.list
18+
19+
import android.view.LayoutInflater
20+
import android.view.ViewGroup
21+
import androidx.annotation.DrawableRes
22+
import androidx.annotation.StringRes
23+
import androidx.recyclerview.widget.DiffUtil
24+
import androidx.recyclerview.widget.ListAdapter
25+
import androidx.recyclerview.widget.RecyclerView
26+
import com.duckduckgo.app.browser.databinding.ItemFeatureGridBinding
27+
import com.duckduckgo.app.webtrackingprotection.list.FeatureGridAdapter.GridItemViewHolder
28+
29+
class FeatureGridAdapter : ListAdapter<FeatureGridItem, GridItemViewHolder>(FeatureGridItemDiffCallback()) {
30+
31+
override fun onCreateViewHolder(
32+
parent: ViewGroup,
33+
viewType: Int,
34+
): GridItemViewHolder {
35+
val inflater = LayoutInflater.from(parent.context)
36+
return GridItemViewHolder(ItemFeatureGridBinding.inflate(inflater, parent, false))
37+
}
38+
39+
override fun onBindViewHolder(
40+
holder: GridItemViewHolder,
41+
position: Int,
42+
) {
43+
holder.bind(getItem(position))
44+
}
45+
46+
class GridItemViewHolder(private val binding: ItemFeatureGridBinding) : RecyclerView.ViewHolder(binding.root) {
47+
48+
fun bind(item: FeatureGridItem) {
49+
binding.icon.setImageResource(item.iconRes)
50+
binding.title.setText(item.titleRes)
51+
binding.description.setText(item.descriptionRes)
52+
}
53+
}
54+
55+
private class FeatureGridItemDiffCallback : DiffUtil.ItemCallback<FeatureGridItem>() {
56+
57+
override fun areItemsTheSame(
58+
oldItem: FeatureGridItem,
59+
newItem: FeatureGridItem,
60+
): Boolean = oldItem == newItem
61+
62+
override fun areContentsTheSame(
63+
oldItem: FeatureGridItem,
64+
newItem: FeatureGridItem,
65+
): Boolean = oldItem == newItem
66+
}
67+
}
68+
69+
data class FeatureGridItem(
70+
@DrawableRes val iconRes: Int,
71+
@StringRes val titleRes: Int,
72+
@StringRes val descriptionRes: Int,
73+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
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+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:shape="oval">
19+
20+
<stroke
21+
android:width="@dimen/horizontalDividerHeight"
22+
android:color="?attr/daxColorContainer" />
23+
24+
<solid android:color="?attr/daxColorSurface" />
25+
</shape>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:width="16dp"
19+
android:height="16dp"
20+
android:viewportWidth="16"
21+
android:viewportHeight="16">
22+
<path
23+
android:pathData="M12.5,9a3.5,3.5 0,1 1,0 7,3.5 3.5,0 0,1 0,-7ZM12,1a4,4 0,0 1,4 4v2.697c0,0.565 -0.753,0.887 -1.25,0.619L14.75,5A2.75,2.75 0,0 0,12 2.25L4,2.25A2.75,2.75 0,0 0,1.25 5v6A2.75,2.75 0,0 0,4 13.75h3.917c0.156,0.576 -0.228,1.25 -0.824,1.25L4,15a4,4 0,0 1,-4 -4L0,5a4,4 0,0 1,4 -4h8ZM10.375,11.875a0.5,0.5 0,0 0,-0.5 0.5v0.25a0.5,0.5 0,0 0,0.5 0.5h4.25a0.5,0.5 0,0 0,0.5 -0.5v-0.25a0.5,0.5 0,0 0,-0.5 -0.5h-4.25ZM7.348,5.075a4.085,4.085 0,0 1,2.614 0.36c0.85,0.42 1.65,1.153 2.37,2.246l0.046,0.07a4.74,4.74 0,0 0,-1.359 0.235c-0.4,-0.535 -0.803,-0.919 -1.197,-1.187a1.997,1.997 0,0 1,-0.105 1.851c-0.307,0.223 -0.586,0.48 -0.832,0.769a2,2 0,0 1,-2.76 -2.49c-0.605,0.41 -1.12,0.96 -1.468,1.524a0.625,0.625 0,1 1,-1.064 -0.656c0.736,-1.193 2.115,-2.413 3.755,-2.723Z"
24+
android:fillColor="?attr/daxColorPrimaryIcon"/>
25+
</vector>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:width="16dp"
19+
android:height="16dp"
20+
android:viewportWidth="16"
21+
android:viewportHeight="16">
22+
<group>
23+
<clip-path
24+
android:pathData="M0,0h16v16H0z"/>
25+
<path
26+
android:pathData="M8.294,0.006c0.492,0.018 0.831,0.43 0.831,0.869a6,6 0,0 0,6 6c0.425,0 0.826,0.318 0.867,0.785 0.004,0.113 0.008,0.227 0.008,0.34 0,0.491 -0.604,0.704 -1.023,0.447a4.708,4.708 0,0 0,-0.235 -0.135c0.003,-0.066 0.006,-0.131 0.007,-0.197a7.291,7.291 0,0 1,-0.538 -0.048,4.725 4.725,0 0,0 -1.353,-0.303A7.255,7.255 0,0 1,7.885 1.25a6.75,6.75 0,1 0,0.427 13.492c0.042,0.08 0.088,0.158 0.135,0.234C8.704,15.397 8.491,16 8,16A8,8 0,1 1,8.294 0.006ZM12.5,9a3.5,3.5 0,1 1,0 7,3.5 3.5,0 0,1 0,-7ZM10.375,11.875a0.5,0.5 0,0 0,-0.5 0.5v0.25a0.5,0.5 0,0 0,0.5 0.5h4.25a0.5,0.5 0,0 0,0.5 -0.5v-0.25a0.5,0.5 0,0 0,-0.5 -0.5h-4.25ZM6.75,11a0.75,0.75 0,1 1,0 1.5,0.75 0.75,0 0,1 0,-1.5ZM3.75,8a0.75,0.75 0,1 1,0 1.5,0.75 0.75,0 0,1 0,-1.5ZM7.75,7a0.75,0.75 0,1 1,0 1.5,0.75 0.75,0 0,1 0,-1.5ZM5.75,4a0.75,0.75 0,1 1,0 1.5,0.75 0.75,0 0,1 0,-1.5Z"
27+
android:fillColor="?attr/daxColorPrimaryIcon"/>
28+
</group>
29+
</vector>

0 commit comments

Comments
 (0)