Skip to content

Commit e021069

Browse files
authored
Visual design: Privacy Shield (#6000)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1210067401123377 ### Description - This PR updates the privacy shield and trackers animation for the new visual design. - To enable it, go to Settings / Experimental UI / Enable all O-A changes and restart the app ### Steps to test this PR _Updates states_ - [ ] Install app and enable visual design updates - [ ] Visit a site with trackers - [ ] Verify new privacy shield and trackers animation appears - [ ] Disable protection for the site - [ ] Verify new unprotected shield appears and no tracker animation is played
1 parent 95bfd33 commit e021069

19 files changed

+228
-36
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/omnibar/animations/LottiePrivacyShieldAnimationHelperTest.kt

Lines changed: 132 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import com.duckduckgo.app.browser.senseofprotection.SenseOfProtectionExperiment
2323
import com.duckduckgo.app.global.model.PrivacyShield.MALICIOUS
2424
import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED
2525
import com.duckduckgo.app.global.model.PrivacyShield.UNPROTECTED
26+
import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore
27+
import com.duckduckgo.common.ui.experiments.visual.store.VisualDesignExperimentDataStore.FeatureState
2628
import com.duckduckgo.common.ui.store.AppTheme
29+
import kotlinx.coroutines.flow.MutableStateFlow
30+
import org.junit.Before
2731
import org.junit.Test
2832
import org.mockito.kotlin.mock
2933
import org.mockito.kotlin.verify
@@ -32,13 +36,25 @@ import org.mockito.kotlin.whenever
3236
class LottiePrivacyShieldAnimationHelperTest {
3337

3438
private val senseOfProtectionExperiment: SenseOfProtectionExperiment = mock()
39+
private val visualDesignExperimentDataStore: VisualDesignExperimentDataStore = mock()
40+
private val enabledVisualExperimentStateFlow = MutableStateFlow(FeatureState(isAvailable = true, isEnabled = true))
41+
private val disabledVisualExperimentStateFlow = MutableStateFlow(FeatureState(isAvailable = false, isEnabled = false))
42+
43+
@Before
44+
fun setup() {
45+
whenever(visualDesignExperimentDataStore.experimentState).thenReturn(
46+
disabledVisualExperimentStateFlow,
47+
)
48+
}
3549

3650
@Test
3751
fun whenLightModeAndPrivacyShieldProtectedThenSetLightShieldAnimation() {
52+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
53+
3854
val holder: LottieAnimationView = mock()
3955
val appTheme: AppTheme = mock()
4056
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
41-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
57+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
4258

4359
testee.setAnimationView(holder, PROTECTED)
4460

@@ -47,10 +63,12 @@ class LottiePrivacyShieldAnimationHelperTest {
4763

4864
@Test
4965
fun whenDarkModeAndPrivacyShieldProtectedThenSetDarkShieldAnimation() {
66+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
67+
5068
val holder: LottieAnimationView = mock()
5169
val appTheme: AppTheme = mock()
5270
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
53-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
71+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
5472

5573
testee.setAnimationView(holder, PROTECTED)
5674

@@ -59,10 +77,12 @@ class LottiePrivacyShieldAnimationHelperTest {
5977

6078
@Test
6179
fun whenLightModeAndPrivacyShieldUnProtectedThenUseLightAnimation() {
80+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
81+
6282
val holder: LottieAnimationView = mock()
6383
val appTheme: AppTheme = mock()
6484
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
65-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
85+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
6686

6787
testee.setAnimationView(holder, UNPROTECTED)
6888

@@ -72,10 +92,12 @@ class LottiePrivacyShieldAnimationHelperTest {
7292

7393
@Test
7494
fun whenDarkModeAndPrivacyShieldUnProtectedThenUseDarkAnimation() {
95+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
96+
7597
val holder: LottieAnimationView = mock()
7698
val appTheme: AppTheme = mock()
7799
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
78-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
100+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
79101

80102
testee.setAnimationView(holder, UNPROTECTED)
81103

@@ -85,10 +107,12 @@ class LottiePrivacyShieldAnimationHelperTest {
85107

86108
@Test
87109
fun whenLightModeAndPrivacyShieldMaliciousThenUseLightAnimation() {
110+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
111+
88112
val holder: LottieAnimationView = mock()
89113
val appTheme: AppTheme = mock()
90114
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
91-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
115+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
92116

93117
testee.setAnimationView(holder, MALICIOUS)
94118

@@ -98,10 +122,12 @@ class LottiePrivacyShieldAnimationHelperTest {
98122

99123
@Test
100124
fun whenDarkModeAndPrivacyShieldMaliciousThenUseDarkAnimation() {
125+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
126+
101127
val holder: LottieAnimationView = mock()
102128
val appTheme: AppTheme = mock()
103129
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
104-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
130+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
105131

106132
testee.setAnimationView(holder, MALICIOUS)
107133

@@ -118,7 +144,7 @@ class LottiePrivacyShieldAnimationHelperTest {
118144
val appTheme: AppTheme = mock()
119145
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
120146

121-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
147+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
122148

123149
testee.setAnimationView(holder, PROTECTED)
124150

@@ -134,7 +160,7 @@ class LottiePrivacyShieldAnimationHelperTest {
134160
val appTheme: AppTheme = mock()
135161
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
136162

137-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
163+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
138164

139165
testee.setAnimationView(holder, UNPROTECTED)
140166

@@ -150,7 +176,7 @@ class LottiePrivacyShieldAnimationHelperTest {
150176
val appTheme: AppTheme = mock()
151177
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
152178

153-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
179+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
154180

155181
testee.setAnimationView(holder, PROTECTED)
156182

@@ -166,7 +192,7 @@ class LottiePrivacyShieldAnimationHelperTest {
166192
val appTheme: AppTheme = mock()
167193
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
168194

169-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
195+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
170196

171197
testee.setAnimationView(holder, UNPROTECTED)
172198

@@ -182,7 +208,102 @@ class LottiePrivacyShieldAnimationHelperTest {
182208
val appTheme: AppTheme = mock()
183209
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
184210

185-
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment)
211+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
212+
213+
testee.setAnimationView(holder, PROTECTED)
214+
215+
verify(holder).setAnimation(R.raw.protected_shield)
216+
}
217+
218+
@SuppressLint("DenyListedApi")
219+
@Test
220+
fun whenLightModeAndProtectedAndSelfEnabledAndShouldShowNewVisualDesignShieldThenUseExperimentAssets() {
221+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
222+
whenever(visualDesignExperimentDataStore.experimentState).thenReturn(
223+
enabledVisualExperimentStateFlow,
224+
)
225+
226+
val holder: LottieAnimationView = mock()
227+
val appTheme: AppTheme = mock()
228+
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
229+
230+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
231+
232+
testee.setAnimationView(holder, PROTECTED)
233+
234+
verify(holder).setAnimation(R.raw.protected_shield_visual_updates)
235+
}
236+
237+
@SuppressLint("DenyListedApi")
238+
@Test
239+
fun whenLightModeAndUnprotectedAndSelfEnabledAndShouldShowNewVisualDesignShieldThenUseExperimentAssets() {
240+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
241+
whenever(visualDesignExperimentDataStore.experimentState).thenReturn(
242+
enabledVisualExperimentStateFlow,
243+
)
244+
245+
val holder: LottieAnimationView = mock()
246+
val appTheme: AppTheme = mock()
247+
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
248+
249+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
250+
251+
testee.setAnimationView(holder, UNPROTECTED)
252+
253+
verify(holder).setAnimation(R.raw.unprotected_shield_visual_updates)
254+
}
255+
256+
@SuppressLint("DenyListedApi")
257+
@Test
258+
fun whenDarkModeAndProtectedAndSelfEnabledAndShouldShowNewVisualDesignShieldThenUseExperimentAssets() {
259+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
260+
whenever(visualDesignExperimentDataStore.experimentState).thenReturn(
261+
enabledVisualExperimentStateFlow,
262+
)
263+
264+
val holder: LottieAnimationView = mock()
265+
val appTheme: AppTheme = mock()
266+
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
267+
268+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
269+
270+
testee.setAnimationView(holder, PROTECTED)
271+
272+
verify(holder).setAnimation(R.raw.dark_protected_shield_visual_updates)
273+
}
274+
275+
@SuppressLint("DenyListedApi")
276+
@Test
277+
fun whenDarkModeAndUnprotectedAndSelfEnabledAndShouldShowNewVisualDesignShieldThenUseExperimentAssets() {
278+
whenever(senseOfProtectionExperiment.shouldShowNewPrivacyShield()).thenReturn(false)
279+
whenever(visualDesignExperimentDataStore.experimentState).thenReturn(
280+
enabledVisualExperimentStateFlow,
281+
)
282+
283+
val holder: LottieAnimationView = mock()
284+
val appTheme: AppTheme = mock()
285+
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
286+
287+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
288+
289+
testee.setAnimationView(holder, UNPROTECTED)
290+
291+
verify(holder).setAnimation(R.raw.dark_unprotected_shield_visual_updates)
292+
}
293+
294+
@SuppressLint("DenyListedApi")
295+
@Test
296+
fun whenLightModeAndProtectedAndSelfEnabledAndShouldShowNotNewVisualDesignShieldThenUseNonExperimentAssets() {
297+
whenever(senseOfProtectionExperiment.isUserEnrolledInAVariantAndExperimentEnabled()).thenReturn(false)
298+
whenever(visualDesignExperimentDataStore.experimentState).thenReturn(
299+
disabledVisualExperimentStateFlow,
300+
)
301+
302+
val holder: LottieAnimationView = mock()
303+
val appTheme: AppTheme = mock()
304+
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
305+
306+
val testee = LottiePrivacyShieldAnimationHelper(appTheme, senseOfProtectionExperiment, visualDesignExperimentDataStore)
186307

187308
testee.setAnimationView(holder, PROTECTED)
188309

app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.Command.StartCo
7272
import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.Command.StartExperimentVariant1Animation
7373
import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.Command.StartExperimentVariant2OrVariant3Animation
7474
import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.Command.StartTrackersAnimation
75+
import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.Command.StartVisualDesignTrackersAnimation
7576
import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.LeadingIconState.PRIVACY_SHIELD
7677
import com.duckduckgo.app.browser.omnibar.OmnibarLayoutViewModel.ViewState
7778
import com.duckduckgo.app.browser.omnibar.animations.BrowserTrackersAnimatorHelper
@@ -496,7 +497,7 @@ open class OmnibarLayout @JvmOverloads constructor(
496497
}
497498

498499
if (viewState.leadingIconState == PRIVACY_SHIELD) {
499-
renderPrivacyShield(viewState.privacyShield, viewState.viewMode)
500+
renderPrivacyShield(viewState.privacyShield, viewState.viewMode, viewState.isVisualDesignExperimentEnabled)
500501
} else {
501502
lastSeenPrivacyShield = null
502503
}
@@ -526,6 +527,10 @@ open class OmnibarLayout @JvmOverloads constructor(
526527
moveCaretToFront()
527528
}
528529

530+
is StartVisualDesignTrackersAnimation -> {
531+
startVisualDesignTrackersAnimation(command.entities)
532+
}
533+
529534
is StartExperimentVariant1Animation -> {
530535
startExperimentVariant1Animation()
531536
}
@@ -549,8 +554,8 @@ open class OmnibarLayout @JvmOverloads constructor(
549554
}
550555
}
551556

552-
private fun renderLeadingIconState(iconState: OmnibarLayoutViewModel.LeadingIconState) {
553-
when (iconState) {
557+
private fun renderLeadingIconState(viewState: ViewState) {
558+
when (viewState.leadingIconState) {
554559
OmnibarLayoutViewModel.LeadingIconState.SEARCH -> {
555560
searchIcon.show()
556561
shieldIcon.gone()
@@ -561,7 +566,7 @@ open class OmnibarLayout @JvmOverloads constructor(
561566
}
562567

563568
OmnibarLayoutViewModel.LeadingIconState.PRIVACY_SHIELD -> {
564-
if (senseOfProtectionExperiment.shouldShowNewPrivacyShield()) {
569+
if (shouldShowUpdatedPrivacyShield(viewState.isVisualDesignExperimentEnabled)) {
565570
shieldIcon.gone()
566571
shieldIconExperiment.show()
567572
} else {
@@ -603,6 +608,10 @@ open class OmnibarLayout @JvmOverloads constructor(
603608
}
604609
}
605610

611+
private fun shouldShowUpdatedPrivacyShield(navigationBarEnabled: Boolean): Boolean {
612+
return senseOfProtectionExperiment.shouldShowNewPrivacyShield() || navigationBarEnabled
613+
}
614+
606615
open fun renderButtons(viewState: ViewState) {
607616
val newTransitionState = TransitionState(
608617
showClearButton = viewState.showClearButton,
@@ -674,7 +683,7 @@ open class OmnibarLayout @JvmOverloads constructor(
674683
renderTabIcon(viewState)
675684
renderPulseAnimation(viewState)
676685

677-
renderLeadingIconState(viewState.leadingIconState)
686+
renderLeadingIconState(viewState)
678687

679688
renderHint(viewState)
680689
}
@@ -832,6 +841,17 @@ open class OmnibarLayout @JvmOverloads constructor(
832841
)
833842
}
834843

844+
private fun startVisualDesignTrackersAnimation(events: List<Entity>?) {
845+
animatorHelper.startTrackersAnimation(
846+
context = context,
847+
shieldAnimationView = shieldIconExperiment,
848+
trackersAnimationView = trackersAnimation,
849+
omnibarViews = omnibarViews(),
850+
entities = events,
851+
visualDesignExperimentEnabled = true,
852+
)
853+
}
854+
835855
private fun startExperimentVariant1Animation() {
836856
if (this::animatorHelper.isInitialized) {
837857
animatorHelper.startExperimentVariant1Animation(
@@ -862,11 +882,12 @@ open class OmnibarLayout @JvmOverloads constructor(
862882
private fun renderPrivacyShield(
863883
privacyShield: PrivacyShield,
864884
viewMode: ViewMode,
885+
navigationBarEnabled: Boolean,
865886
) {
866887
renderIfChanged(privacyShield, lastSeenPrivacyShield) {
867888
lastSeenPrivacyShield = privacyShield
868889
val shieldIconView = if (viewMode is ViewMode.Browser) {
869-
val isExperimentEnabled = senseOfProtectionExperiment.shouldShowNewPrivacyShield()
890+
val isExperimentEnabled = shouldShowUpdatedPrivacyShield(navigationBarEnabled)
870891
if (isExperimentEnabled) shieldIconExperiment else shieldIcon
871892
} else {
872893
customTabToolbarContainer.customTabShieldIcon

0 commit comments

Comments
 (0)