Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bf4e7c0
Add internal toggle for always enabled address bar feature
mikescamell Oct 3, 2025
80afd1a
Add trackers blocked count and text views to omnibar
mikescamell Sep 24, 2025
ff26217
Add back classes for address bar trackers animation
mikescamell Sep 24, 2025
a57d8b4
Rename visual design trackers animation method for clarity
mikescamell Sep 24, 2025
75b7b15
Refactor address bar trackers animation methods for clarity and consi…
mikescamell Sep 24, 2025
280add0
Add new shield pop animation for the trackers animation
mikescamell Oct 2, 2025
952e9d9
Add Lottie animation for address bar trackers blocked state
mikescamell Oct 2, 2025
79b65aa
Remove commented out old experiment code
mikescamell Oct 2, 2025
48ace2a
Enhance address bar trackers animation with new scenes and improved l…
mikescamell Oct 2, 2025
cd9bac1
Use new animation if flag is on
mikescamell Oct 3, 2025
9136200
Add enqueuing if auto consent popup handled
mikescamell Oct 3, 2025
a46c092
Refactor address bar trackers animation code into separate classes
mikescamell Oct 3, 2025
c2356d8
Refactor address bar animations to new package structure and update i…
mikescamell Oct 3, 2025
7e0b92c
Delete unused interface
mikescamell Oct 3, 2025
2da6789
Use DispatcherProvider for coroutine scope in BrowserLottieTrackersAn…
mikescamell Oct 6, 2025
bdb9423
Add detailed documentation for tracker count animation methods
mikescamell Oct 6, 2025
d273a1a
Rename cookieDummyView to animatedIconBackgroundView and update relat…
mikescamell Oct 6, 2025
ab911dc
Implement address bar trackers animation cancellation and management
mikescamell Oct 7, 2025
39bac46
Rename cancelTrackersAnimation to cancelAddressBarAnimations for clarity
mikescamell Oct 7, 2025
3afc734
Fix animation background going under the duck.ai icon
mikescamell Oct 7, 2025
eddc379
Refactor animation duration reference in address bar tracker animation
mikescamell Oct 7, 2025
9955254
Fix and add tests
mikescamell Oct 7, 2025
e685cfc
Tweak icon background fade in/out to look more seamless
mikescamell Oct 7, 2025
08ff7ea
Fix tests
mikescamell Oct 8, 2025
bf9717b
Add clarifying comments
mikescamell Oct 8, 2025
cb9c0c3
Remove unused code
mikescamell Oct 9, 2025
f787696
Fix shield highlighting for prod shield and new shield
mikescamell Oct 9, 2025
f40f1b4
Remove unused import for AddressBarTrackersAnimationFeatureToggle
mikescamell Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ import com.duckduckgo.app.browser.WebViewErrorResponse.BAD_URL
import com.duckduckgo.app.browser.WebViewErrorResponse.LOADING
import com.duckduckgo.app.browser.WebViewErrorResponse.OMITTED
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
import com.duckduckgo.app.browser.animations.AddressBarTrackersAnimationFeatureToggle
import com.duckduckgo.app.browser.applinks.AppLinksHandler
import com.duckduckgo.app.browser.camera.CameraHardwareChecker
import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository
import com.duckduckgo.app.browser.certificates.remoteconfig.SSLCertificatesFeature
import com.duckduckgo.app.browser.commands.Command
import com.duckduckgo.app.browser.commands.Command.CloseCustomTab
import com.duckduckgo.app.browser.commands.Command.EnqueueCookiesAnimation
import com.duckduckgo.app.browser.commands.Command.EscapeMaliciousSite
import com.duckduckgo.app.browser.commands.Command.HideBrokenSitePromptCta
import com.duckduckgo.app.browser.commands.Command.HideOnboardingDaxBubbleCta
Expand Down Expand Up @@ -596,6 +598,7 @@ class BrowserTabViewModelTest {

private val mockOnboardingDesignExperimentManager: OnboardingDesignExperimentManager = mock()
private val mockSerpEasterEggLogoToggles: SerpEasterEggLogosToggles = mock()
private val mockAddressBarTrackersAnimationFeatureToggle: AddressBarTrackersAnimationFeatureToggle = mock()

private val nonHttpAppLinkChecker: NonHttpAppLinkChecker = mock()

Expand Down Expand Up @@ -689,6 +692,7 @@ class BrowserTabViewModelTest {
whenever(mockSerpEasterEggLogoToggles.feature()).thenReturn(mockDisabledToggle)
whenever(nonHttpAppLinkChecker.isPermitted(anyOrNull())).thenReturn(true)
remoteMessagingModel = givenRemoteMessagingModel(mockRemoteMessagingRepository, mockPixel, coroutineRule.testDispatcherProvider)
whenever(mockAddressBarTrackersAnimationFeatureToggle.feature()).thenReturn(mockDisabledToggle)

ctaViewModel =
CtaViewModel(
Expand Down Expand Up @@ -842,6 +846,7 @@ class BrowserTabViewModelTest {
addDocumentStartJavascriptPlugins = fakeAddDocumentStartJavaScriptPlugins,
webMessagingPlugins = fakeMessagingPlugins,
postMessageWrapperPlugins = fakePostMessageWrapperPlugins,
addressBarTrackersAnimationFeatureToggle = mockAddressBarTrackersAnimationFeatureToggle,
)

testee.loadData("abc", null, false, false)
Expand Down Expand Up @@ -6435,6 +6440,64 @@ class BrowserTabViewModelTest {
assertCommandIssued<ShowAutoconsentAnimation>()
}

@Test
fun whenAutoConsentPopupHandledWithFeatureToggleEnabledAndTrackersBlockedThenEnqueueCookiesAnimation() {
whenever(mockAddressBarTrackersAnimationFeatureToggle.feature()).thenReturn(mockEnabledToggle)

testee.browserViewState.value =
testee.browserViewState.value?.copy(
browserShowing = true,
maliciousSiteBlocked = false,
maliciousSiteStatus = null,
)

val site = givenCurrentSite("https://example.com")
whenever(site.trackerCount).thenReturn(5)
testee.siteLiveData.value = site

testee.onAutoConsentPopUpHandled(true)

assertCommandIssued<EnqueueCookiesAnimation> {
assertTrue(isCosmetic)
}
}

@Test
fun whenAutoConsentPopupHandledWithFeatureToggleDisabledThenShowAutoconsentAnimation() {
whenever(mockAddressBarTrackersAnimationFeatureToggle.feature()).thenReturn(mockDisabledToggle)

testee.browserViewState.value =
testee.browserViewState.value?.copy(
browserShowing = true,
maliciousSiteBlocked = false,
maliciousSiteStatus = null,
)

testee.onAutoConsentPopUpHandled(true)

assertCommandIssued<ShowAutoconsentAnimation>()
}

@Test
fun whenAutoConsentPopupHandledWithFeatureToggleEnabledButNoTrackersThenShowAutoconsentAnimation() {
whenever(mockAddressBarTrackersAnimationFeatureToggle.feature()).thenReturn(mockEnabledToggle)

testee.browserViewState.value =
testee.browserViewState.value?.copy(
browserShowing = true,
maliciousSiteBlocked = false,
maliciousSiteStatus = null,
)

val site = givenCurrentSite("https://example.com")
whenever(site.trackerCount).thenReturn(0)
testee.siteLiveData.value = site

testee.onAutoConsentPopUpHandled(true)

assertCommandIssued<ShowAutoconsentAnimation>()
}

@Test
fun whenVisitSiteThenUpdateLoadingViewStateAndOmnibarViewState() {
testee.browserViewState.value =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ package com.duckduckgo.app.browser.omnibar.animations
import android.annotation.SuppressLint
import com.airbnb.lottie.LottieAnimationView
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.animations.AddressBarTrackersAnimationFeatureToggle
import com.duckduckgo.app.browser.omnibar.Omnibar
import com.duckduckgo.app.browser.omnibar.animations.addressbar.LottiePrivacyShieldAnimationHelper
import com.duckduckgo.app.global.model.PrivacyShield.MALICIOUS
import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.UNPROTECTED
import com.duckduckgo.common.ui.store.AppTheme
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
import com.duckduckgo.feature.toggles.api.Toggle
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.mockito.kotlin.mock
Expand All @@ -34,13 +38,14 @@ class LottiePrivacyShieldAnimationHelperTest {

private val browserViewMode = Omnibar.ViewMode.Browser("cnn.com")
private val customTabViewMode = Omnibar.ViewMode.CustomTab(0, "cnn.com", "cnn.com")
private val fakeAddressBarTrackersAnimationFeatureToggle = FakeFeatureToggleFactory.create(AddressBarTrackersAnimationFeatureToggle::class.java)

@Test
fun whenLightModeAndPrivacyShieldProtectedThenSetLightShieldAnimation() = runTest {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, PROTECTED, browserViewMode)

Expand All @@ -52,7 +57,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, PROTECTED, browserViewMode)

Expand All @@ -64,7 +69,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, UNPROTECTED, browserViewMode)

Expand All @@ -77,7 +82,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, UNPROTECTED, browserViewMode)

Expand All @@ -90,7 +95,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, MALICIOUS, browserViewMode)

Expand All @@ -103,7 +108,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, MALICIOUS, browserViewMode)

Expand All @@ -118,7 +123,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(true)

val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, PROTECTED, customTabViewMode)

Expand All @@ -132,7 +137,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(false)

val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, PROTECTED, customTabViewMode)

Expand All @@ -146,7 +151,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(true)

val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, UNPROTECTED, customTabViewMode)

Expand All @@ -160,7 +165,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(false)

val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, UNPROTECTED, customTabViewMode)

Expand All @@ -174,7 +179,7 @@ class LottiePrivacyShieldAnimationHelperTest {
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(true)

val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, PROTECTED, customTabViewMode)

Expand All @@ -188,10 +193,40 @@ class LottiePrivacyShieldAnimationHelperTest {
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(false)

val testee = LottiePrivacyShieldAnimationHelper(appTheme)
val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, PROTECTED, customTabViewMode)

verify(holder).setAnimation(R.raw.dark_protected_shield_custom_tab)
}

@SuppressLint("DenyListedApi")
@Test
fun whenLightModeAndAddressBarTrackersAnimationFeatureToggleIsOnThenUseAddressBarTrackersAnimationShieldVariant() = runTest {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
fakeAddressBarTrackersAnimationFeatureToggle.feature().setRawStoredState(Toggle.State(true))
whenever(appTheme.isLightModeEnabled()).thenReturn(true)

val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, PROTECTED, browserViewMode)

verify(holder).setAnimation(R.raw.address_bar_trackers_animation_shield)
}

@SuppressLint("DenyListedApi")
@Test
fun whenDarkModeAndAddressBarTrackersAnimationFeatureToggleIsOnThenUseAddressBarTrackersAnimationShieldVariant() = runTest {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
fakeAddressBarTrackersAnimationFeatureToggle.feature().setRawStoredState(Toggle.State(true))
whenever(appTheme.isLightModeEnabled()).thenReturn(false)

val testee = LottiePrivacyShieldAnimationHelper(appTheme, fakeAddressBarTrackersAnimationFeatureToggle)

testee.setAnimationView(holder, PROTECTED, browserViewMode)

verify(holder).setAnimation(R.raw.address_bar_trackers_animation_shield)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser.omnibar.animations

import androidx.test.platform.app.InstrumentationRegistry
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.omnibar.animations.addressbar.TrackersRenderer
import org.junit.Assert.assertEquals
import org.junit.Test

Expand Down
28 changes: 13 additions & 15 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -742,21 +742,7 @@ class BrowserTabFragment :
override fun onFirstPopUpHandled() {}

override fun onPopUpHandled(isCosmetic: Boolean) {
launch {
context?.let {
/* TODO uncomment when sense of protection experiment shield is enabled
if (senseOfProtectionExperiment.isUserEnrolledInAVariantAndExperimentEnabled() &&
viewModel.trackersCount().isNotEmpty()
) {
if (isCosmetic) {
delay(COOKIES_ANIMATION_DELAY)
}
omnibar.enqueueCookiesAnimation(isCosmetic)
} else {*/
viewModel.onAutoConsentPopUpHandled(isCosmetic)
// }
}
}
viewModel.onAutoConsentPopUpHandled(isCosmetic)
}

override fun onResultReceived(
Expand Down Expand Up @@ -2296,6 +2282,7 @@ class BrowserTabFragment :
}

is Command.SubmitChat -> duckChat.openDuckChatWithAutoPrompt(it.query)
is Command.EnqueueCookiesAnimation -> enqueueCookiesAnimation(it.isCosmetic)
}
}

Expand Down Expand Up @@ -2323,6 +2310,17 @@ class BrowserTabFragment :
}
}

private fun enqueueCookiesAnimation(isCosmetic: Boolean) {
launch {
if (isCosmetic) {
delay(COOKIES_ANIMATION_DELAY)
}
context?.let {
omnibar.enqueueCookiesAnimation(isCosmetic)
}
}
}

private fun setBrowserBackgroundRes(backgroundRes: Int) {
newBrowserTab.browserBackground.setImageResource(backgroundRes)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.ShouldLaunchPrivacy
import com.duckduckgo.app.browser.WebViewErrorResponse.LOADING
import com.duckduckgo.app.browser.WebViewErrorResponse.OMITTED
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
import com.duckduckgo.app.browser.animations.AddressBarTrackersAnimationFeatureToggle
import com.duckduckgo.app.browser.applinks.AppLinksHandler
import com.duckduckgo.app.browser.camera.CameraHardwareChecker
import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository
Expand Down Expand Up @@ -492,6 +493,7 @@ class BrowserTabViewModel @Inject constructor(
private val addDocumentStartJavascriptPlugins: PluginPoint<AddDocumentStartJavaScriptPlugin>,
private val webMessagingPlugins: PluginPoint<WebMessagingPlugin>,
private val postMessageWrapperPlugins: PluginPoint<PostMessageWrapperPlugin>,
private val addressBarTrackersAnimationFeatureToggle: AddressBarTrackersAnimationFeatureToggle,
) : ViewModel(),
WebViewClientListener,
EditSavedSiteListener,
Expand Down Expand Up @@ -4479,7 +4481,11 @@ class BrowserTabViewModel @Inject constructor(

fun onAutoConsentPopUpHandled(isCosmetic: Boolean) {
if (!currentBrowserViewState().maliciousSiteBlocked) {
command.postValue(ShowAutoconsentAnimation(isCosmetic))
if (addressBarTrackersAnimationFeatureToggle.feature().isEnabled() && trackersCount().isNotEmpty()) {
command.postValue(Command.EnqueueCookiesAnimation(isCosmetic))
} else {
command.postValue(ShowAutoconsentAnimation(isCosmetic))
}
}
}

Expand All @@ -4500,8 +4506,6 @@ class BrowserTabViewModel @Inject constructor(
command.value = LaunchBookmarksActivity
}

fun trackersCount(): String = site?.trackerCount?.takeIf { it > 0 }?.toString() ?: ""

fun resetTrackersCount() {
site?.resetTrackingEvents()
}
Expand Down Expand Up @@ -4538,6 +4542,9 @@ class BrowserTabViewModel @Inject constructor(
command.value = Command.ShowSerpEasterEggLogo(url)
}

private fun trackersCount(): String =
siteLiveData.value?.trackerCount?.takeIf { it > 0 }?.toString() ?: ""

companion object {
private const val FIXED_PROGRESS = 50

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface AddressBarTrackersAnimationFeatureToggle {
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
fun self(): Toggle

@Toggle.InternalAlwaysEnabled
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
fun feature(): Toggle
}
Original file line number Diff line number Diff line change
Expand Up @@ -498,4 +498,8 @@ sealed class Command {
data class ShowSerpEasterEggLogo(
val logoUrl: String,
) : Command()

data class EnqueueCookiesAnimation(
val isCosmetic: Boolean,
) : Command()
}
Loading
Loading