From 9fa53661463e5fffe959c87d7c0522e61e649d08 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Fri, 24 Jan 2025 18:54:28 +0000 Subject: [PATCH 1/3] Added 2 pixels. --- .../app/browser/omnibar/OmnibarLayout.kt | 4 ++ .../browser/omnibar/OmnibarLayoutViewModel.kt | 22 ++++++++ .../com/duckduckgo/app/pixels/AppPixelName.kt | 3 ++ .../omnibar/OmnibarLayoutViewModelTest.kt | 53 +++++++++++++++++++ .../duckduckgo/app/statistics/pixels/Pixel.kt | 1 + 5 files changed, 83 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt index 82a2484aa64a..252c0bc75d07 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt @@ -684,6 +684,8 @@ class OmnibarLayout @JvmOverloads constructor( omnibarViews = omnibarViews(), entities = events, ) + + viewModel.onTrackersAnimationStarted() } private fun startExperimentTrackersAnimation(events: List?) { @@ -699,6 +701,8 @@ class OmnibarLayout @JvmOverloads constructor( omnibarViews = omnibarViews(), entities = events, ) + + viewModel.onExperimentTrackersAnimationStarted() } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt index 28e207998c55..f73b479ba845 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt @@ -42,8 +42,11 @@ import com.duckduckgo.app.browser.viewstate.HighlightableButton import com.duckduckgo.app.browser.viewstate.LoadingViewState import com.duckduckgo.app.browser.viewstate.OmnibarViewState import com.duckduckgo.app.global.model.PrivacyShield +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.FIRE_BUTTON_STATE import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Unique import com.duckduckgo.app.tabs.model.TabEntity @@ -81,6 +84,7 @@ class OmnibarLayoutViewModel @Inject constructor( private val userBrowserProperties: UserBrowserProperties, private val dispatcherProvider: DispatcherProvider, private val appPersonalityFeature: AppPersonalityFeature, + private val userStageStore: UserStageStore, ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) @@ -635,4 +639,22 @@ class OmnibarLayoutViewModel @Inject constructor( ) } } + + fun onTrackersAnimationStarted() { + viewModelScope.launch { + pixel.fire( + AppPixelName.TRACKERS_CIRCLES_ANIMATION_SHOWN, + mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "${userStageStore.getUserAppStage() != AppStage.ESTABLISHED}"), + ) + } + } + + fun onExperimentTrackersAnimationStarted() { + viewModelScope.launch { + pixel.fire( + AppPixelName.TRACKERS_BURST_ANIMATION_SHOWN, + mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "${userStageStore.getUserAppStage() != AppStage.ESTABLISHED}"), + ) + } + } } diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index cacc84640075..15106d14536c 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -383,4 +383,7 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { DEDICATED_WEBVIEW_URL_EXTRACTION_FAILED("m_dedicated_webview_url_extraction_failed"), BLOCKLIST_TDS_FAILURE("blocklist_experiment_tds_download_failure"), + + TRACKERS_CIRCLES_ANIMATION_SHOWN("m_trackers_circles_animation_shown"), + TRACKERS_BURST_ANIMATION_SHOWN("m_trackers_burst_animation_shown"), } diff --git a/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt index 2b2b7799e1b7..fc6d7431d6a5 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt @@ -17,9 +17,12 @@ import com.duckduckgo.app.browser.viewstate.OmnibarViewState import com.duckduckgo.app.global.model.PrivacyShield import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED import com.duckduckgo.app.global.model.PrivacyShield.UNPROTECTED +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.privacy.model.TestingEntity import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.FIRE_BUTTON_STATE import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Unique import com.duckduckgo.app.tabs.model.TabEntity @@ -60,6 +63,7 @@ class OmnibarLayoutViewModelTest { private val pixel: Pixel = mock() private val userBrowserProperties: UserBrowserProperties = mock() private val fakeAppPersonalityFeature = FakeFeatureToggleFactory.create(AppPersonalityFeature::class.java) + private val mockUserStageStore: UserStageStore = mock() private lateinit var testee: OmnibarLayoutViewModel @@ -81,6 +85,7 @@ class OmnibarLayoutViewModelTest { userBrowserProperties = userBrowserProperties, dispatcherProvider = coroutineTestRule.testDispatcherProvider, appPersonalityFeature = fakeAppPersonalityFeature, + userStageStore = mockUserStageStore, ) whenever(tabRepository.flowTabs).thenReturn(flowOf(emptyList())) @@ -931,6 +936,54 @@ class OmnibarLayoutViewModelTest { } } + @Test + fun whenOnTrackersAnimationStartedCalledAfterOnboardingThenPixelSentWithParamFalse() = runTest { + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) + + testee.onTrackersAnimationStarted() + + verify(pixel).fire( + AppPixelName.TRACKERS_CIRCLES_ANIMATION_SHOWN, + mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "false"), + ) + } + + @Test + fun whenOnTrackersAnimationStartedCalledDuringOnboardingThenPixelSentWithParamTrue() = runTest { + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.NEW) + + testee.onTrackersAnimationStarted() + + verify(pixel).fire( + AppPixelName.TRACKERS_CIRCLES_ANIMATION_SHOWN, + mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "true"), + ) + } + + @Test + fun whenOnExperimentTrackersAnimationStartedCalledAfterOnboardingThenPixelSentWithParamFalse() = runTest { + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) + + testee.onExperimentTrackersAnimationStarted() + + verify(pixel).fire( + AppPixelName.TRACKERS_BURST_ANIMATION_SHOWN, + mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "false"), + ) + } + + @Test + fun whenOnExperimentTrackersAnimationStartedCalledDuringOnboardingThenPixelSentWithParamTrue() = runTest { + whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.NEW) + + testee.onExperimentTrackersAnimationStarted() + + verify(pixel).fire( + AppPixelName.TRACKERS_BURST_ANIMATION_SHOWN, + mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "true"), + ) + } + private fun givenSiteLoaded(loadedUrl: String) { testee.onViewModeChanged(ViewMode.Browser(loadedUrl)) testee.onExternalStateChange( diff --git a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 38d710cfab8e..010951ea9065 100644 --- a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -66,6 +66,7 @@ interface Pixel { const val TAB_INACTIVE_1W = "tab_inactive_1w" const val TAB_INACTIVE_2W = "tab_inactive_2w" const val TAB_INACTIVE_3W = "tab_inactive_3w" + const val TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING = "duringOnboarding" } object PixelValues { From 60a4fcaa018f96f335a4994585787db94a455eb1 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Thu, 30 Jan 2025 10:26:09 +0000 Subject: [PATCH 2/3] Added the last pixel. --- .../app/browser/BrowserTabViewModelTest.kt | 4 ++ .../app/browser/BrowserTabViewModel.kt | 17 ++++++ ...RealPrivacyDashboardExternalPixelParams.kt | 57 +++++++++++++++++++ .../app/browser/omnibar/OmnibarLayout.kt | 2 - .../browser/omnibar/OmnibarLayoutViewModel.kt | 13 ++--- .../omnibar/OmnibarLayoutViewModelTest.kt | 27 +-------- .../PrivacyDashboardExternalPixelsPlugin.kt | 26 +++++++++ .../ui/PrivacyDashboardHybridViewModel.kt | 4 +- .../ui/PrivacyDashboardHybridViewModelTest.kt | 4 ++ .../duckduckgo/app/statistics/pixels/Pixel.kt | 2 + 10 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/animations/RealPrivacyDashboardExternalPixelParams.kt create mode 100644 privacy-dashboard/privacy-dashboard-api/src/main/java/com/duckduckgo/privacy/dashboard/api/PrivacyDashboardExternalPixelsPlugin.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 6a1b9b720ff3..c700a8c29349 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -226,6 +226,7 @@ import com.duckduckgo.privacy.config.api.ContentBlocking import com.duckduckgo.privacy.config.api.TrackingParameters import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc.Companion.GPC_HEADER import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc.Companion.GPC_HEADER_VALUE +import com.duckduckgo.privacy.dashboard.api.PrivacyDashboardExternalPixelParams import com.duckduckgo.privacy.dashboard.api.PrivacyProtectionTogglePlugin import com.duckduckgo.privacy.dashboard.api.PrivacyToggleOrigin import com.duckduckgo.privacy.dashboard.api.ui.ToggleReports @@ -504,6 +505,7 @@ class BrowserTabViewModelTest { private val mockTabStatsBucketing: TabStatsBucketing = mock() private val mockDuckChatJSHelper: DuckChatJSHelper = mock() private val fakeAppPersonalityFeature = FakeFeatureToggleFactory.create(AppPersonalityFeature::class.java) + private val mockPrivacyDashboardExternalPixelParams: PrivacyDashboardExternalPixelParams = mock() @Before fun before() = runTest { @@ -675,6 +677,8 @@ class BrowserTabViewModelTest { tabStatsBucketing = mockTabStatsBucketing, maliciousSiteBlockerWebViewIntegration = mock(), appPersonalityFeature = fakeAppPersonalityFeature, + userStageStore = mockUserStageStore, + privacyDashboardExternalPixelParams = mockPrivacyDashboardExternalPixelParams, ) testee.loadData("abc", null, false, false) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 7fdd9a3bdfd8..9e67ee30f1b1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -237,6 +237,8 @@ import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.domainMatchesUrl import com.duckduckgo.app.location.data.LocationPermissionType +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.HighlightsOnboardingExperimentManager import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_BANNER_DISMISSED @@ -258,6 +260,8 @@ import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.api.StatisticsUpdater import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.AFTER_BURST_ANIMATION +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Count import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Unique @@ -305,6 +309,7 @@ import com.duckduckgo.privacy.config.api.AmpLinkInfo import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.privacy.config.api.ContentBlocking import com.duckduckgo.privacy.config.api.TrackingParameters +import com.duckduckgo.privacy.dashboard.api.PrivacyDashboardExternalPixelParams import com.duckduckgo.privacy.dashboard.api.PrivacyProtectionTogglePlugin import com.duckduckgo.privacy.dashboard.api.PrivacyToggleOrigin import com.duckduckgo.privacy.dashboard.api.ui.DashboardOpener @@ -460,6 +465,8 @@ class BrowserTabViewModel @Inject constructor( private val tabStatsBucketing: TabStatsBucketing, private val maliciousSiteBlockerWebViewIntegration: MaliciousSiteBlockerWebViewIntegration, private val appPersonalityFeature: AppPersonalityFeature, + private val userStageStore: UserStageStore, + private val privacyDashboardExternalPixelParams: PrivacyDashboardExternalPixelParams, ) : WebViewClientListener, EditSavedSiteListener, DeleteBookmarkListener, @@ -1482,6 +1489,7 @@ class BrowserTabViewModel @Inject constructor( ) { Timber.v("Page changed: $url") cleanupBlobDownloadReplyProxyMaps() + privacyDashboardExternalPixelParams.clearPixelParams() hasCtaBeenShownForCurrentPage.set(false) buildSiteFactory(url, title, urlUnchangedForExternalLaunchPurposes(site?.url, url)) @@ -3794,6 +3802,15 @@ class BrowserTabViewModel @Inject constructor( fun onAnimationFinished(logos: List) { if (appPersonalityFeature.self().isEnabled() && appPersonalityFeature.trackersBlockedAnimation().isEnabled()) { command.value = Command.StartTrackersLogosAnimation(logos) + if (logos.size > 2) { + viewModelScope.launch { + pixel.fire( + AppPixelName.TRACKERS_BURST_ANIMATION_SHOWN, + mapOf(TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "${userStageStore.getUserAppStage() != AppStage.ESTABLISHED}"), + ) + privacyDashboardExternalPixelParams.setPixelParams(AFTER_BURST_ANIMATION, "true") + } + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/animations/RealPrivacyDashboardExternalPixelParams.kt b/app/src/main/java/com/duckduckgo/app/browser/animations/RealPrivacyDashboardExternalPixelParams.kt new file mode 100644 index 000000000000..8429e1f0d936 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/animations/RealPrivacyDashboardExternalPixelParams.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.animations + +import com.duckduckgo.app.browser.apppersonality.AppPersonalityFeature +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.privacy.dashboard.api.PrivacyDashboardExternalPixelParams +import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn +import javax.inject.Inject + +@SingleInstanceIn(AppScope::class) +@ContributesBinding(AppScope::class) +class RealPrivacyDashboardExternalPixelParams @Inject constructor( + private val appPersonalityFeature: AppPersonalityFeature, +) : PrivacyDashboardExternalPixelParams { + + private val pixelParams = mutableMapOf() + + @Synchronized + override fun getPixelParams(): Map { + if (pixelParams.isEmpty()) { + val key = if (appPersonalityFeature.self().isEnabled() && appPersonalityFeature.trackersBlockedAnimation().isEnabled()) { + PixelParameter.AFTER_BURST_ANIMATION + } else { + PixelParameter.AFTER_CIRCLES_ANIMATION + } + setPixelParams(key, "false") + } + return pixelParams.toMap() + } + + @Synchronized + override fun setPixelParams(key: String, value: String) { + pixelParams[key] = value + } + + @Synchronized + override fun clearPixelParams() { + pixelParams.clear() + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt index 252c0bc75d07..579ab39cc6b3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt @@ -701,8 +701,6 @@ class OmnibarLayout @JvmOverloads constructor( omnibarViews = omnibarViews(), entities = events, ) - - viewModel.onExperimentTrackersAnimationStarted() } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt index f73b479ba845..0e594f39f0df 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt @@ -47,6 +47,7 @@ import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.AFTER_CIRCLES_ANIMATION import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.FIRE_BUTTON_STATE import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Unique import com.duckduckgo.app.tabs.model.TabEntity @@ -56,6 +57,7 @@ import com.duckduckgo.browser.api.UserBrowserProperties import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.FragmentScope import com.duckduckgo.duckplayer.api.DuckPlayer +import com.duckduckgo.privacy.dashboard.api.PrivacyDashboardExternalPixelParams import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels import com.duckduckgo.voice.api.VoiceSearchAvailability import com.duckduckgo.voice.api.VoiceSearchAvailabilityPixelLogger @@ -85,6 +87,7 @@ class OmnibarLayoutViewModel @Inject constructor( private val dispatcherProvider: DispatcherProvider, private val appPersonalityFeature: AppPersonalityFeature, private val userStageStore: UserStageStore, + private val privacyDashboardExternalPixelParams: PrivacyDashboardExternalPixelParams, ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) @@ -646,15 +649,7 @@ class OmnibarLayoutViewModel @Inject constructor( AppPixelName.TRACKERS_CIRCLES_ANIMATION_SHOWN, mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "${userStageStore.getUserAppStage() != AppStage.ESTABLISHED}"), ) - } - } - - fun onExperimentTrackersAnimationStarted() { - viewModelScope.launch { - pixel.fire( - AppPixelName.TRACKERS_BURST_ANIMATION_SHOWN, - mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "${userStageStore.getUserAppStage() != AppStage.ESTABLISHED}"), - ) + privacyDashboardExternalPixelParams.setPixelParams(AFTER_CIRCLES_ANIMATION, "true") } } } diff --git a/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt index fc6d7431d6a5..46cc24924d1f 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt @@ -32,6 +32,7 @@ import com.duckduckgo.browser.api.UserBrowserProperties import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.privacy.dashboard.api.PrivacyDashboardExternalPixelParams import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels import com.duckduckgo.voice.api.VoiceSearchAvailability import com.duckduckgo.voice.api.VoiceSearchAvailabilityPixelLogger @@ -64,6 +65,7 @@ class OmnibarLayoutViewModelTest { private val userBrowserProperties: UserBrowserProperties = mock() private val fakeAppPersonalityFeature = FakeFeatureToggleFactory.create(AppPersonalityFeature::class.java) private val mockUserStageStore: UserStageStore = mock() + private val mockPrivacyDashboardExternalPixelParams: PrivacyDashboardExternalPixelParams = mock() private lateinit var testee: OmnibarLayoutViewModel @@ -86,6 +88,7 @@ class OmnibarLayoutViewModelTest { dispatcherProvider = coroutineTestRule.testDispatcherProvider, appPersonalityFeature = fakeAppPersonalityFeature, userStageStore = mockUserStageStore, + privacyDashboardExternalPixelParams = mockPrivacyDashboardExternalPixelParams, ) whenever(tabRepository.flowTabs).thenReturn(flowOf(emptyList())) @@ -960,30 +963,6 @@ class OmnibarLayoutViewModelTest { ) } - @Test - fun whenOnExperimentTrackersAnimationStartedCalledAfterOnboardingThenPixelSentWithParamFalse() = runTest { - whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) - - testee.onExperimentTrackersAnimationStarted() - - verify(pixel).fire( - AppPixelName.TRACKERS_BURST_ANIMATION_SHOWN, - mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "false"), - ) - } - - @Test - fun whenOnExperimentTrackersAnimationStartedCalledDuringOnboardingThenPixelSentWithParamTrue() = runTest { - whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.NEW) - - testee.onExperimentTrackersAnimationStarted() - - verify(pixel).fire( - AppPixelName.TRACKERS_BURST_ANIMATION_SHOWN, - mapOf(PixelParameter.TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING to "true"), - ) - } - private fun givenSiteLoaded(loadedUrl: String) { testee.onViewModeChanged(ViewMode.Browser(loadedUrl)) testee.onExternalStateChange( diff --git a/privacy-dashboard/privacy-dashboard-api/src/main/java/com/duckduckgo/privacy/dashboard/api/PrivacyDashboardExternalPixelsPlugin.kt b/privacy-dashboard/privacy-dashboard-api/src/main/java/com/duckduckgo/privacy/dashboard/api/PrivacyDashboardExternalPixelsPlugin.kt new file mode 100644 index 000000000000..d38620b67912 --- /dev/null +++ b/privacy-dashboard/privacy-dashboard-api/src/main/java/com/duckduckgo/privacy/dashboard/api/PrivacyDashboardExternalPixelsPlugin.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.privacy.dashboard.api + +interface PrivacyDashboardExternalPixelParams { + + fun getPixelParams(): Map + + fun setPixelParams(key: String, value: String) + + fun clearPixelParams() +} diff --git a/privacy-dashboard/privacy-dashboard-impl/src/main/java/com/duckduckgo/privacy/dashboard/impl/ui/PrivacyDashboardHybridViewModel.kt b/privacy-dashboard/privacy-dashboard-impl/src/main/java/com/duckduckgo/privacy/dashboard/impl/ui/PrivacyDashboardHybridViewModel.kt index dedd8f6b0e28..0216787780ce 100644 --- a/privacy-dashboard/privacy-dashboard-impl/src/main/java/com/duckduckgo/privacy/dashboard/impl/ui/PrivacyDashboardHybridViewModel.kt +++ b/privacy-dashboard/privacy-dashboard-impl/src/main/java/com/duckduckgo/privacy/dashboard/impl/ui/PrivacyDashboardHybridViewModel.kt @@ -35,6 +35,7 @@ import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.baseHost import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.privacy.dashboard.api.PrivacyDashboardExternalPixelParams import com.duckduckgo.privacy.dashboard.api.PrivacyProtectionTogglePlugin import com.duckduckgo.privacy.dashboard.api.PrivacyToggleOrigin import com.duckduckgo.privacy.dashboard.api.ui.DashboardOpener @@ -97,6 +98,7 @@ class PrivacyDashboardHybridViewModel @Inject constructor( private val brokenSiteSender: BrokenSiteSender, private val moshi: Moshi, private val privacyProtectionTogglePlugin: PluginPoint, + private val privacyDashboardExternalPixelParams: PrivacyDashboardExternalPixelParams, ) : ViewModel() { private val command = Channel(1, DROP_OLDEST) @@ -232,7 +234,7 @@ class PrivacyDashboardHybridViewModel @Inject constructor( init { viewModelScope.launch { - val pixelParams = privacyProtectionsPopupExperimentExternalPixels.getPixelParams() + val pixelParams = privacyProtectionsPopupExperimentExternalPixels.getPixelParams() + privacyDashboardExternalPixelParams.getPixelParams() pixel.fire(PRIVACY_DASHBOARD_OPENED, pixelParams, type = Count) pixel.fire( pixel = PRIVACY_DASHBOARD_FIRST_TIME_OPENED, diff --git a/privacy-dashboard/privacy-dashboard-impl/src/test/java/com/duckduckgo/privacy/dashboard/impl/ui/PrivacyDashboardHybridViewModelTest.kt b/privacy-dashboard/privacy-dashboard-impl/src/test/java/com/duckduckgo/privacy/dashboard/impl/ui/PrivacyDashboardHybridViewModelTest.kt index d4fd71b1a24d..6d490d1ed1bf 100644 --- a/privacy-dashboard/privacy-dashboard-impl/src/test/java/com/duckduckgo/privacy/dashboard/impl/ui/PrivacyDashboardHybridViewModelTest.kt +++ b/privacy-dashboard/privacy-dashboard-impl/src/test/java/com/duckduckgo/privacy/dashboard/impl/ui/PrivacyDashboardHybridViewModelTest.kt @@ -36,6 +36,7 @@ import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.privacy.config.api.ContentBlocking import com.duckduckgo.privacy.config.api.UnprotectedTemporary +import com.duckduckgo.privacy.dashboard.api.PrivacyDashboardExternalPixelParams import com.duckduckgo.privacy.dashboard.api.PrivacyProtectionTogglePlugin import com.duckduckgo.privacy.dashboard.api.PrivacyToggleOrigin import com.duckduckgo.privacy.dashboard.api.ui.DashboardOpener @@ -96,6 +97,8 @@ class PrivacyDashboardHybridViewModelTest { private val protectionTogglePlugin = FakePrivacyProtectionTogglePlugin() private val pluginPoint = FakePluginPoint(protectionTogglePlugin) + private val mockPrivacyDashboardExternalPixelParams: PrivacyDashboardExternalPixelParams = mock() + private val toggleReports: ToggleReports = mock { runBlocking { whenever(mock.shouldPrompt()).thenReturn(false) } } @@ -117,6 +120,7 @@ class PrivacyDashboardHybridViewModelTest { privacyProtectionTogglePlugin = pluginPoint, toggleReports = toggleReports, moshi = Moshi.Builder().build(), + privacyDashboardExternalPixelParams = mockPrivacyDashboardExternalPixelParams, ) } diff --git a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 010951ea9065..6839bcdee5fc 100644 --- a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -67,6 +67,8 @@ interface Pixel { const val TAB_INACTIVE_2W = "tab_inactive_2w" const val TAB_INACTIVE_3W = "tab_inactive_3w" const val TRACKERS_ANIMATION_SHOWN_DURING_ONBOARDING = "duringOnboarding" + const val AFTER_CIRCLES_ANIMATION = "after_circles_animation" + const val AFTER_BURST_ANIMATION = "after_burst_animation" } object PixelValues { From eec34bba47101c31835f023e7fbf2144522e2dcd Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Mon, 3 Feb 2025 16:55:14 +0000 Subject: [PATCH 3/3] Address feedback and known issues for Tracker Blocking Animation (#5538) Task/Issue URL: https://app.asana.com/0/1200581511062568/1209237907925618/f ### Description Fixed the initial tracker count after page refresh or page changed. Stacked PR. --- .../app/browser/BrowserTabFragment.kt | 46 ++++++++-- .../app/browser/BrowserTabViewModel.kt | 19 +++- .../duckduckgo/app/browser/PulseAnimation.kt | 39 +++++++-- ...t => ExperimentTrackersAnimationHelper.kt} | 14 ++- ...ottieExperimentTrackersAnimationHelper.kt} | 87 ++++++++++++------- .../app/browser/commands/Command.kt | 3 +- .../app/browser/omnibar/OmnibarLayout.kt | 15 ++-- .../app/browser/omnibar/TopAppBarBehavior.kt | 13 +-- .../TrackersBlockedViewSlideBehavior.kt | 36 +++++++- .../BrowserLottieTrackersAnimatorHelper.kt | 84 +++++++++--------- .../BrowserTrackersAnimatorHelper.kt | 4 +- .../app/global/model/SiteMonitor.kt | 4 + .../res/drawable/ic_circle_pulse_green.xml | 27 ++++++ .../main/res/layout/fragment_browser_tab.xml | 2 +- app/src/main/res/raw/shieldburst.json | 1 - app/src/main/res/raw/trackers_burst.json | 1 + app/src/main/res/values-pl/strings.xml | 1 + .../com/duckduckgo/app/global/model/Site.kt | 1 + 18 files changed, 284 insertions(+), 113 deletions(-) rename app/src/main/java/com/duckduckgo/app/browser/animations/{TrackersCircleAnimationHelper.kt => ExperimentTrackersAnimationHelper.kt} (76%) rename app/src/main/java/com/duckduckgo/app/browser/animations/{LottieTrackersCircleAnimationHelper.kt => LottieExperimentTrackersAnimationHelper.kt} (51%) create mode 100644 app/src/main/res/drawable/ic_circle_pulse_green.xml delete mode 100644 app/src/main/res/raw/shieldburst.json create mode 100644 app/src/main/res/raw/trackers_burst.json diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index a11c8a27d28c..fa2ad57c7ef2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -76,6 +76,7 @@ import androidx.core.net.toUri import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY import androidx.core.text.toSpannable +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.postDelayed import androidx.fragment.app.DialogFragment @@ -103,7 +104,7 @@ import com.duckduckgo.app.browser.R.string import com.duckduckgo.app.browser.SSLErrorType.NONE import com.duckduckgo.app.browser.WebViewErrorResponse.LOADING import com.duckduckgo.app.browser.WebViewErrorResponse.OMITTED -import com.duckduckgo.app.browser.animations.TrackersCircleAnimationHelper +import com.duckduckgo.app.browser.animations.ExperimentTrackersAnimationHelper import com.duckduckgo.app.browser.api.WebViewCapabilityChecker import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability import com.duckduckgo.app.browser.applinks.AppLinksLauncher @@ -532,7 +533,7 @@ class BrowserTabFragment : lateinit var webViewCapabilityChecker: WebViewCapabilityChecker @Inject - lateinit var animatorHelper: TrackersCircleAnimationHelper + lateinit var experimentTrackersAnimationHelper: ExperimentTrackersAnimationHelper @Inject lateinit var appPersonalityFeature: AppPersonalityFeature @@ -806,16 +807,27 @@ class BrowserTabFragment : private lateinit var privacyProtectionsPopup: PrivacyProtectionsPopup - private fun showNewTrackersBlockingAnimation(logos: List) { - animatorHelper.startTrackersCircleAnimation( + private fun showExperimentTrackersBurstAnimation(logos: List) { + experimentTrackersAnimationHelper.startTrackersBurstAnimation( context = requireContext(), - trackersCircleAnimationView = binding.newTrackersBlockingAnimationView, + trackersBurstAnimationView = binding.trackersBurstAnimationView, omnibarShieldAnimationView = omnibar.shieldIcon, omnibarPosition = omnibar.omnibarPosition, + omnibarView = if (omnibar.omnibarPosition == OmnibarPosition.TOP) { + binding.newOmnibar + } else { + binding.newOmnibarBottom + }, logos = logos, ) } + private fun showExperimentShieldPopAnimation() { + experimentTrackersAnimationHelper.startShieldPopAnimation( + omnibarShieldAnimationView = omnibar.shieldIcon, + ) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Timber.d("onCreate called for tabId=$tabId") @@ -947,15 +959,31 @@ class BrowserTabFragment : } private fun notifyVerticalOffsetChanged(scrollFraction: Float) { + // Ensure the trackersBlockedSlidingView is hidden on new tab or when scrolling is disabled. + if (binding.trackersBlockedSlidingView.isVisible && (binding.browserLayout.isGone || !binding.newOmnibar.isOmnibarScrollingEnabled())) { + binding.trackersBlockedSlidingView.hide() + return + } + + if (!viewModel.isSiteProtected() || scrollFraction == 1.0f) { + return + } + // Move the trackersBlockedSlidingView in sync with the top omnibar. binding.trackersBlockedSlidingView.translationY = -binding.trackersBlockedSlidingView.height * (1 - scrollFraction) if (scrollFraction == 0.0f) { binding.trackersBlockedSlidingView.gone() } else { + if (binding.trackersBurstAnimationView.isAnimating) { + binding.trackersBurstAnimationView.cancelAnimation() + } + val count = viewModel.trackersCount() + if (count != binding.trackers.text) { + binding.trackers.text = count + } + binding.website.text = viewModel.url?.extractDomain() binding.trackersBlockedSlidingView.show() } - binding.trackers.text = viewModel.trackersCount() - binding.website.text = viewModel.url?.extractDomain() } private fun onOmnibarTabsButtonPressed() { @@ -1218,6 +1246,7 @@ class BrowserTabFragment : override fun onStop() { alertDialog?.dismiss() + experimentTrackersAnimationHelper.cancelAnimations() super.onStop() } @@ -1880,7 +1909,8 @@ class BrowserTabFragment : binding.autoCompleteSuggestionsList.gone() browserActivity?.openExistingTab(it.tabId) } - is Command.StartTrackersLogosAnimation -> showNewTrackersBlockingAnimation(it.logos) + is Command.StartExperimentTrackersBurstAnimation -> showExperimentTrackersBurstAnimation(it.logos) + is Command.StartExperimentShieldPopAnimation -> showExperimentShieldPopAnimation() else -> { // NO OP } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 9e67ee30f1b1..b98e8668911b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1906,7 +1906,6 @@ class BrowserTabViewModel @Inject constructor( val privacyProtection: PrivacyShield = withContext(dispatchers.io()) { site?.privacyProtection() ?: PrivacyShield.UNKNOWN } - // TODO ANA: Send command to add / remove sliding view if protected / unprotected Timber.i("Shield: privacyProtection $privacyProtection") withContext(dispatchers.main()) { @@ -3103,6 +3102,7 @@ class BrowserTabViewModel @Inject constructor( } fun onWebViewRefreshed() { + site?.resetTrackingEvents() refreshBrowserError() resetAutoConsent() accessibilityViewState.value = currentAccessibilityViewState().copy(refreshWebView = false) @@ -3800,9 +3800,13 @@ class BrowserTabViewModel @Inject constructor( } fun onAnimationFinished(logos: List) { + if (logos.isEmpty()) { + return + } + if (appPersonalityFeature.self().isEnabled() && appPersonalityFeature.trackersBlockedAnimation().isEnabled()) { - command.value = Command.StartTrackersLogosAnimation(logos) - if (logos.size > 2) { + if (logos.size > TRACKER_LOGO_ANIMATION_THRESHOLD) { + command.value = Command.StartExperimentTrackersBurstAnimation(logos) viewModelScope.launch { pixel.fire( AppPixelName.TRACKERS_BURST_ANIMATION_SHOWN, @@ -3810,12 +3814,19 @@ class BrowserTabViewModel @Inject constructor( ) privacyDashboardExternalPixelParams.setPixelParams(AFTER_BURST_ANIMATION, "true") } + } else { + command.value = Command.StartExperimentShieldPopAnimation } } } fun trackersCount(): String = site?.trackerCount?.takeIf { it > 0 }?.toString() ?: "" + fun isSiteProtected(): Boolean { + val shield = site?.privacyProtection() ?: PrivacyShield.UNKNOWN + return shield == PrivacyShield.PROTECTED + } + companion object { private const val FIXED_PROGRESS = 50 @@ -3829,6 +3840,8 @@ class BrowserTabViewModel @Inject constructor( private const val HTTP_STATUS_CODE_CLIENT_ERROR_PREFIX = 4 // 4xx, client error status code prefix private const val HTTP_STATUS_CODE_SERVER_ERROR_PREFIX = 5 // 5xx, server error status code prefix + private const val TRACKER_LOGO_ANIMATION_THRESHOLD = 2 + // https://www.iso.org/iso-3166-country-codes.html private val PRINT_LETTER_FORMAT_COUNTRIES_ISO3166_2 = setOf( Locale.US.country, diff --git a/app/src/main/java/com/duckduckgo/app/browser/PulseAnimation.kt b/app/src/main/java/com/duckduckgo/app/browser/PulseAnimation.kt index f1199ab3bd14..66769d0ee649 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/PulseAnimation.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/PulseAnimation.kt @@ -30,11 +30,18 @@ import androidx.core.view.isVisible import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.duckduckgo.common.ui.view.setAllParentsClip +import com.duckduckgo.common.utils.ConflatedJob +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle class PulseAnimation(private val lifecycleOwner: LifecycleOwner) : DefaultLifecycleObserver { private var pulseAnimation: AnimatorSet = AnimatorSet() private var highlightImageView: View? = null + private val conflatedJob = ConflatedJob() + val isActive: Boolean get() = pulseAnimation.isRunning @@ -50,14 +57,15 @@ class PulseAnimation(private val lifecycleOwner: LifecycleOwner) : DefaultLifecy if (pulseAnimation.isRunning) { pulseAnimation.pause() } + conflatedJob.cancel() } - fun playOn(targetView: View) { + fun playOn(targetView: View, isExperimentAndShieldView: Boolean) { if (highlightImageView == null) { - highlightImageView = addHighlightView(targetView) + highlightImageView = addHighlightView(targetView, isExperimentAndShieldView) highlightImageView?.doOnLayout { it.setAllParentsClip(enabled = false) - startPulseAnimation(it) + startPulseAnimation(it, isExperimentAndShieldView) } lifecycleOwner.lifecycle.addObserver(this) } @@ -72,12 +80,22 @@ class PulseAnimation(private val lifecycleOwner: LifecycleOwner) : DefaultLifecy lifecycleOwner.lifecycle.removeObserver(this) } - private fun startPulseAnimation(view: View) { + @SuppressLint("NoHardcodedCoroutineDispatcher") + private fun startPulseAnimation(view: View, isExperimentAndShieldView: Boolean) { if (!pulseAnimation.isRunning) { val pulse = getPulseObjectAnimator(view) pulse.repeatCount = ObjectAnimator.INFINITE pulse.duration = 1100L + if (isExperimentAndShieldView) { + pulse.startDelay = 3500L + view.alpha = 0.0f + conflatedJob += CoroutineScope(Dispatchers.Main).launch { + delay(3500L) + view.alpha = 1.0f + } + } + pulseAnimation = AnimatorSet().apply { play(pulse) start() @@ -109,13 +127,20 @@ class PulseAnimation(private val lifecycleOwner: LifecycleOwner) : DefaultLifecy } } - private fun addHighlightView(targetView: View): View { + private fun addHighlightView(targetView: View, isExperimentAndShieldView: Boolean): View { if (targetView.parent !is ViewGroup) error("targetView parent should be ViewGroup") val highlightImageView = ImageView(targetView.context) highlightImageView.id = View.generateViewId() - highlightImageView.setImageResource(R.drawable.ic_circle_pulse_blue) - val layoutParams = FrameLayout.LayoutParams(targetView.width, targetView.height, Gravity.CENTER) + val gravity: Int + if (isExperimentAndShieldView) { + highlightImageView.setImageResource(R.drawable.ic_circle_pulse_green) + gravity = Gravity.START + } else { + highlightImageView.setImageResource(R.drawable.ic_circle_pulse_blue) + gravity = Gravity.CENTER + } + val layoutParams = FrameLayout.LayoutParams(targetView.width, targetView.height, gravity) (targetView.parent as ViewGroup).addView(highlightImageView, 0, layoutParams) return highlightImageView } diff --git a/app/src/main/java/com/duckduckgo/app/browser/animations/TrackersCircleAnimationHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/animations/ExperimentTrackersAnimationHelper.kt similarity index 76% rename from app/src/main/java/com/duckduckgo/app/browser/animations/TrackersCircleAnimationHelper.kt rename to app/src/main/java/com/duckduckgo/app/browser/animations/ExperimentTrackersAnimationHelper.kt index f363eb621ce9..a270173fe582 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/animations/TrackersCircleAnimationHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/animations/ExperimentTrackersAnimationHelper.kt @@ -17,17 +17,25 @@ package com.duckduckgo.app.browser.animations import android.content.Context +import android.view.View import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.app.browser.omnibar.animations.TrackerLogo import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition -interface TrackersCircleAnimationHelper { +interface ExperimentTrackersAnimationHelper { - fun startTrackersCircleAnimation( + fun startShieldPopAnimation( + omnibarShieldAnimationView: LottieAnimationView, + ) + + fun startTrackersBurstAnimation( context: Context, - trackersCircleAnimationView: LottieAnimationView, + trackersBurstAnimationView: LottieAnimationView, omnibarShieldAnimationView: LottieAnimationView, omnibarPosition: OmnibarPosition, + omnibarView: View, logos: List, ) + + fun cancelAnimations() } diff --git a/app/src/main/java/com/duckduckgo/app/browser/animations/LottieTrackersCircleAnimationHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/animations/LottieExperimentTrackersAnimationHelper.kt similarity index 51% rename from app/src/main/java/com/duckduckgo/app/browser/animations/LottieTrackersCircleAnimationHelper.kt rename to app/src/main/java/com/duckduckgo/app/browser/animations/LottieExperimentTrackersAnimationHelper.kt index c7f69338260e..20a610d6a3c7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/animations/LottieTrackersCircleAnimationHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/animations/LottieExperimentTrackersAnimationHelper.kt @@ -19,8 +19,9 @@ package com.duckduckgo.app.browser.animations import android.animation.Animator import android.animation.Animator.AnimatorListener import android.content.Context -import android.content.res.Resources +import android.graphics.Rect import android.view.Gravity +import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.app.browser.R @@ -35,50 +36,50 @@ import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject @ContributesBinding(FragmentScope::class) -class LottieTrackersCircleAnimationHelper @Inject constructor() : TrackersCircleAnimationHelper { +class LottieExperimentTrackersAnimationHelper @Inject constructor() : ExperimentTrackersAnimationHelper { - private lateinit var trackersCircleAnimationView: LottieAnimationView - private lateinit var omnibarShieldAnimationView: LottieAnimationView - private lateinit var resources: Resources + private var trackersBurstAnimationView: LottieAnimationView? = null + private var omnibarShieldAnimationView: LottieAnimationView? = null - override fun startTrackersCircleAnimation( + override fun startShieldPopAnimation( + omnibarShieldAnimationView: LottieAnimationView, + ) { + this.omnibarShieldAnimationView = omnibarShieldAnimationView + + omnibarShieldAnimationView.setAnimation(R.raw.protected_shield_experiment) + omnibarShieldAnimationView.setMaxProgress(1f) + omnibarShieldAnimationView.playAnimation() + } + + override fun startTrackersBurstAnimation( context: Context, - trackersCircleAnimationView: LottieAnimationView, + trackersBurstAnimationView: LottieAnimationView, omnibarShieldAnimationView: LottieAnimationView, omnibarPosition: OmnibarPosition, + omnibarView: View, logos: List, ) { - this.trackersCircleAnimationView = trackersCircleAnimationView + this.trackersBurstAnimationView = trackersBurstAnimationView this.omnibarShieldAnimationView = omnibarShieldAnimationView - this.resources = context.resources - - if (logos.size <= 2) { - // TODO ANA: We need to show the protected shield based on flags. - omnibarShieldAnimationView.setAnimation(R.raw.protected_shield_experiment) - omnibarShieldAnimationView.setMaxProgress(1f) - omnibarShieldAnimationView.playAnimation() - return - } val negativeMarginPx = (-72).toPx() val gravity = if (omnibarPosition == OmnibarPosition.BOTTOM) Gravity.BOTTOM else Gravity.NO_GRAVITY - val layoutParams = trackersCircleAnimationView.layoutParams as CoordinatorLayout.LayoutParams + val layoutParams = trackersBurstAnimationView.layoutParams as CoordinatorLayout.LayoutParams layoutParams.gravity = gravity layoutParams.marginStart = negativeMarginPx if (gravity == Gravity.BOTTOM) { - trackersCircleAnimationView.scaleY = -1f + trackersBurstAnimationView.scaleY = -1f } else { layoutParams.topMargin = negativeMarginPx - trackersCircleAnimationView.scaleY = 1f + trackersBurstAnimationView.scaleY = 1f } - trackersCircleAnimationView.setLayoutParams(layoutParams) + trackersBurstAnimationView.setLayoutParams(layoutParams) - // TODO ANA: We need to show the protected shield based on flags. omnibarShieldAnimationView.setAnimation(R.raw.protected_shield_experiment) - with(trackersCircleAnimationView) { + with(trackersBurstAnimationView) { this.setCacheComposition(false) - this.setAnimation(R.raw.shieldburst) + this.setAnimation(R.raw.trackers_burst) this.maintainOriginalImageBounds = true this.setImageAssetDelegate(TrackersLottieAssetDelegate(context, logos)) this.removeAllAnimatorListeners() @@ -87,13 +88,14 @@ class LottieTrackersCircleAnimationHelper @Inject constructor() : TrackersCircle this.addAnimatorListener( object : AnimatorListener { override fun onAnimationStart(animation: Animator) { - this@LottieTrackersCircleAnimationHelper.trackersCircleAnimationView.show() - this@LottieTrackersCircleAnimationHelper.omnibarShieldAnimationView.setMaxProgress(1f) - this@LottieTrackersCircleAnimationHelper.omnibarShieldAnimationView.playAnimation() + this@LottieExperimentTrackersAnimationHelper.trackersBurstAnimationView?.show() + this@LottieExperimentTrackersAnimationHelper.omnibarShieldAnimationView?.speed = 0.4f + this@LottieExperimentTrackersAnimationHelper.omnibarShieldAnimationView?.setMaxProgress(1f) + this@LottieExperimentTrackersAnimationHelper.omnibarShieldAnimationView?.playAnimation() } override fun onAnimationEnd(animation: Animator) { - this@LottieTrackersCircleAnimationHelper.trackersCircleAnimationView.gone() + this@LottieExperimentTrackersAnimationHelper.trackersBurstAnimationView?.gone() } override fun onAnimationCancel(animation: Animator) {} @@ -102,8 +104,33 @@ class LottieTrackersCircleAnimationHelper @Inject constructor() : TrackersCircle }, ) - this.setMaxProgress(1f) - this.playAnimation() + if (isViewInsideScreen(omnibarView)) { + this.setMaxProgress(1f) + this.playAnimation() + } } } + + override fun cancelAnimations() { + this.trackersBurstAnimationView?.cancelAnimation() + this.omnibarShieldAnimationView?.cancelAnimation() + } + + private fun isViewInsideScreen(view: View): Boolean { + val location = IntArray(2) + view.getLocationOnScreen(location) + val screenRect = Rect( + 0, + 0, + view.context.resources.displayMetrics.widthPixels, + view.context.resources.displayMetrics.heightPixels, + ) + val viewRect = Rect( + location[0], + location[1], + location[0] + view.width, + location[1] + view.height, + ) + return screenRect.contains(viewRect) + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index 74a5d4feb970..fd0f239949e5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -258,5 +258,6 @@ sealed class Command { class SetOnboardingDialogBackground(@DrawableRes val backgroundRes: Int) : Command() data class LaunchFireDialogFromOnboardingDialog(val onboardingCta: OnboardingDaxDialogCta) : Command() data class SwitchToTab(val tabId: String) : Command() - data class StartTrackersLogosAnimation(val logos: List) : Command() + data class StartExperimentTrackersBurstAnimation(val logos: List) : Command() + data object StartExperimentShieldPopAnimation : Command() } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt index 579ab39cc6b3..c4bc395918ab 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt @@ -44,6 +44,7 @@ import com.duckduckgo.app.browser.PulseAnimation import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.SmoothProgressAnimator import com.duckduckgo.app.browser.TabSwitcherButton +import com.duckduckgo.app.browser.apppersonality.AppPersonalityFeature import com.duckduckgo.app.browser.databinding.IncludeCustomTabToolbarBinding import com.duckduckgo.app.browser.databinding.IncludeFindInPageBinding import com.duckduckgo.app.browser.omnibar.Omnibar.OmnibarTextState @@ -89,7 +90,6 @@ import com.duckduckgo.di.scopes.FragmentScope import com.google.android.material.appbar.AppBarLayout import dagger.android.support.AndroidSupportInjection import javax.inject.Inject -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -145,6 +145,9 @@ class OmnibarLayout @JvmOverloads constructor( @Inject lateinit var dispatchers: DispatcherProvider + @Inject + lateinit var appPersonalityFeature: AppPersonalityFeature + private lateinit var pulseAnimation: PulseAnimation private var omnibarTextListener: Omnibar.TextListener? = null @@ -183,9 +186,6 @@ class OmnibarLayout @JvmOverloads constructor( internal val trackersAnimation: LottieAnimationView by lazy { findViewById(R.id.trackersAnimation) } internal val duckPlayerIcon: ImageView by lazy { findViewById(R.id.duckPlayerIcon) } - // internal val trackersBlockedAnimation: DaxTextView by lazy { findViewById(R.id.trackersBlockedTextView) } - // internal val trackersBlockedCountAnimation: DaxTextView by lazy { findViewById(R.id.trackersBlockedCountView) } - init { val attr = context.theme.obtainStyledAttributes(attrs, R.styleable.LegacyOmnibarView, defStyle, 0) @@ -631,13 +631,16 @@ class OmnibarLayout @JvmOverloads constructor( // omnibar only scrollable when browser showing and the fire button is not promoted if (targetView != null) { + // We need a different asset when the experiment is enabled and the animation is played on the Privacy Shield. + val isPrivacyShieldAnimation = targetView == placeholder + val isExperimentEnabled = appPersonalityFeature.self().isEnabled() && appPersonalityFeature.trackersBlockedAnimation().isEnabled() if (this::pulseAnimation.isInitialized) { if (pulseAnimation.isActive) { pulseAnimation.stop() } doOnLayout { if (this::pulseAnimation.isInitialized) { - pulseAnimation.playOn(targetView) + pulseAnimation.playOn(targetView, isPrivacyShieldAnimation && isExperimentEnabled) } } } @@ -693,7 +696,7 @@ class OmnibarLayout @JvmOverloads constructor( val trackersBlockedAnimation: DaxTextView = findViewById(R.id.trackersBlockedTextView) val trackersBlockedCountAnimation: DaxTextView = findViewById(R.id.trackersBlockedCountView) - animatorHelper.startNewTrackersAnimation( + animatorHelper.startExperimentTrackersAnimation( context = context, shieldAnimationView = shieldIcon, trackersBlockedAnimationView = trackersBlockedAnimation, diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/TopAppBarBehavior.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/TopAppBarBehavior.kt index e002801fc5cf..dccbeeb04e7c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/TopAppBarBehavior.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/TopAppBarBehavior.kt @@ -21,7 +21,6 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup.MarginLayoutParams import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.updateLayoutParams import com.duckduckgo.app.browser.R import com.google.android.material.appbar.AppBarLayout @@ -58,11 +57,15 @@ class TopAppBarBehavior( } private fun offsetBottomByToolbar(view: View?) { - if (view?.layoutParams is MarginLayoutParams) { - view.updateLayoutParams { - this.bottomMargin = omnibar.measuredHeight() + val omnibarHeight = omnibar.measuredHeight() + if (omnibarHeight > 0 && view is View && view.layoutParams is MarginLayoutParams) { + val layoutParams = view.layoutParams as MarginLayoutParams + if (layoutParams.bottomMargin != omnibarHeight) { + layoutParams.bottomMargin = omnibarHeight + view.postOnAnimation { + view.requestLayout() + } } - view.requestLayout() } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/TrackersBlockedViewSlideBehavior.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/TrackersBlockedViewSlideBehavior.kt index 4d5c7edecac2..df5d388ca4ac 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/TrackersBlockedViewSlideBehavior.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/TrackersBlockedViewSlideBehavior.kt @@ -22,8 +22,12 @@ import android.view.Gravity import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.ViewCompat +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.lifecycle.MutableLiveData +import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.app.browser.R +import com.duckduckgo.app.global.model.PrivacyShield import com.duckduckgo.app.global.model.Site import com.duckduckgo.common.ui.view.hide import com.duckduckgo.common.ui.view.show @@ -45,9 +49,21 @@ class TrackersBlockedViewSlideBehavior( private var gravity: Int? = null private var trackers: DaxTextView? = null private var website: DaxTextView? = null + private var trackersBurstAnimationView: LottieAnimationView? = null + private var browserLayout: View? = null override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean { - if (dependency.id == R.id.newOmnibarBottom) { + // Avoid any unnecessary operations. Hide the child if it's visible on new tab (the browser layout is gone or scrolling is disabled). + if (child.isVisible && (browserLayout?.isGone == true || bottomOmnibar?.isOmnibarScrollingEnabled() == false)) { + child.hide() + return false + } + + if (dependency.id == R.id.browserLayout) { + browserLayout = dependency + } else if (dependency.id == R.id.trackersBurstAnimationView) { + trackersBurstAnimationView = dependency as LottieAnimationView + } else if (dependency.id == R.id.newOmnibarBottom) { if (gravity == null) { val layoutParams = child.layoutParams as? CoordinatorLayout.LayoutParams gravity = layoutParams?.gravity @@ -71,17 +87,23 @@ class TrackersBlockedViewSlideBehavior( consumed: IntArray, type: Int, ) { - if (bottomOmnibar?.isOmnibarScrollingEnabled() == true) { + if (bottomOmnibar?.isOmnibarScrollingEnabled() == true && isSiteProtected()) { val translation = bottomOmnibar?.getTranslation() ?: 0f - if (translation == 0f) { + val bottomOmnibarHeight = bottomOmnibar?.height ?: 0 + if (translation == 0f || translation < bottomOmnibarHeight || browserLayout?.isGone == true) { child.hide() } else { val site = siteLiveData.value trackers?.text = site?.trackerCount.toString() website?.text = site?.url?.extractDomain() + if (trackersBurstAnimationView?.isAnimating == true) { + trackersBurstAnimationView?.cancelAnimation() + } child.show() } - super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) + child.postOnAnimation { + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) + } } } @@ -98,4 +120,10 @@ class TrackersBlockedViewSlideBehavior( } return axes == ViewCompat.SCROLL_AXIS_VERTICAL } + + private fun isSiteProtected(): Boolean { + val site = siteLiveData.value + val shield = site?.privacyProtection() ?: PrivacyShield.UNKNOWN + return shield == PrivacyShield.PROTECTED + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserLottieTrackersAnimatorHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserLottieTrackersAnimatorHelper.kt index 5d4b2c4f7db0..8b48f28914db 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserLottieTrackersAnimatorHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserLottieTrackersAnimatorHelper.kt @@ -37,6 +37,7 @@ import android.view.animation.AnimationSet import android.view.animation.TranslateAnimation import androidx.core.animation.addListener import androidx.core.animation.doOnEnd +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.omnibar.animations.TrackerLogo.ImageLogo @@ -60,8 +61,8 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( private var trackersAnimation: LottieAnimationView? = null private var shieldAnimation: LottieAnimationView? = null - private lateinit var trackersBlockedAnimationView: DaxTextView - private lateinit var trackersBlockedCountAnimationView: DaxTextView + private var trackersBlockedAnimationView: DaxTextView? = null + private var trackersBlockedCountAnimationView: DaxTextView? = null private lateinit var cookieView: LottieAnimationView private lateinit var cookieScene: ViewGroup @@ -133,7 +134,7 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( } } - override fun startNewTrackersAnimation( + override fun startExperimentTrackersAnimation( context: Context, shieldAnimationView: LottieAnimationView, trackersBlockedAnimationView: DaxTextView, @@ -156,29 +157,37 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( } animateTrackersBlockedView(omnibarViews) - animateTrackersBlockedCountView(entities, logos, omnibarViews) + animateTrackersBlockedCountView(context, entities, logos, omnibarViews) } private fun animateTrackersBlockedView(omnibarViews: List) { - val fadeInAnimation = AlphaAnimation(0f, 1f).apply { duration = 1000L } + val fadeInAnimation = AlphaAnimation(0f, 1f).apply { + duration = 500L + startOffset = 100L + } val slideInAnimation = TranslateAnimation( - Animation.RELATIVE_TO_PARENT, - -1.0f, - Animation.RELATIVE_TO_PARENT, + Animation.RELATIVE_TO_SELF, + -0.08f, + Animation.RELATIVE_TO_SELF, 0f, - Animation.RELATIVE_TO_PARENT, + Animation.RELATIVE_TO_SELF, 0f, - Animation.RELATIVE_TO_PARENT, + Animation.RELATIVE_TO_SELF, 0f, - ).apply { duration = 500L } + ).apply { + duration = 500L + startOffset = 200L + } + + slideInAnimation.interpolator = LinearOutSlowInInterpolator() - val animationSet = AnimationSet(true).apply { + val animationSet = AnimationSet(false).apply { addAnimation(fadeInAnimation) addAnimation(slideInAnimation) } - trackersBlockedAnimationView.show() - trackersBlockedAnimationView.startAnimation(animationSet) + trackersBlockedAnimationView?.show() + trackersBlockedAnimationView?.startAnimation(animationSet) animationSet.setAnimationListener( object : Animation.AnimationListener { @@ -193,36 +202,20 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( ) } - private fun animateTrackersBlockedCountView(entities: List, logos: List, omnibarViews: List) { + private fun animateTrackersBlockedCountView(context: Context, entities: List, logos: List, omnibarViews: List) { val fadeInAnimation = AlphaAnimation(0f, 1f).apply { - duration = 1000L - startOffset = 200L + duration = 200L } - val slideInAnimation = TranslateAnimation( - Animation.RELATIVE_TO_PARENT, - -1.0f, - Animation.RELATIVE_TO_PARENT, - 0f, - Animation.RELATIVE_TO_PARENT, - 0f, - Animation.RELATIVE_TO_PARENT, - 0f, - ).apply { duration = 1000L } - val animationSet = AnimationSet(true).apply { - addAnimation(fadeInAnimation) - addAnimation(slideInAnimation) - } + trackersBlockedCountAnimationView?.show() + trackersBlockedCountAnimationView?.startAnimation(fadeInAnimation) - trackersBlockedCountAnimationView.show() - trackersBlockedCountAnimationView.startAnimation(animationSet) - - animationSet.setAnimationListener( + fadeInAnimation.setAnimationListener( object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation?) {} override fun onAnimationEnd(animation: Animation?) { - updateTrackersCountWithAnimation(entities, logos, omnibarViews) + updateTrackersCountWithAnimation(context, entities, logos, omnibarViews) } override fun onAnimationRepeat(animation: Animation?) {} @@ -231,23 +224,29 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( } private fun updateTrackersCountWithAnimation( + context: Context, entities: List, logos: List, omnibarViews: List, ) { val handler = Handler(Looper.getMainLooper()) - val trackerCountUpdateDelay = 200L + val trackerCountUpdateDelay = if (entities.size >= TRACKER_THRESHOLD) { + HIGH_TRACKERS_COUNT_ANIMATION_DURATION + } else { + LOW_TRACKERS_COUNT_ANIMATION_DURATION + } val animationCompletionDelay = 1000L fun updateTackersCountText(index: Int) { if (index <= entities.size) { - trackersBlockedCountAnimationView.text = index.toString() + trackersBlockedCountAnimationView?.text = index.toString() handler.postDelayed({ updateTackersCountText(index + 1) }, trackerCountUpdateDelay) } else { handler.postDelayed( { - trackersBlockedAnimationView.gone() - trackersBlockedCountAnimationView.gone() + trackersBlockedAnimationView?.gone() + trackersBlockedCountAnimationView?.text = "" + trackersBlockedCountAnimationView?.gone() animateOmnibarIn(omnibarViews).start() listener?.onAnimationFinished(logos) }, @@ -256,7 +255,7 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( handler.postDelayed( { - tryToStartCookiesAnimation(trackersBlockedCountAnimationView.context, omnibarViews) + tryToStartCookiesAnimation(context, omnibarViews) }, 2500L, ) @@ -559,6 +558,9 @@ class BrowserLottieTrackersAnimatorHelper @Inject constructor( private const val COOKIES_ANIMATION_DELAY = 1000L private const val COOKIES_ANIMATION_DURATION = 300L private const val COOKIES_ANIMATION_FADE_OUT_DURATION = 800L + private const val TRACKER_THRESHOLD = 10 + private const val HIGH_TRACKERS_COUNT_ANIMATION_DURATION = 100L + private const val LOW_TRACKERS_COUNT_ANIMATION_DURATION = 150L } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserTrackersAnimatorHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserTrackersAnimatorHelper.kt index 1966e8f69d76..51ca2550089b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserTrackersAnimatorHelper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/BrowserTrackersAnimatorHelper.kt @@ -26,8 +26,6 @@ import com.duckduckgo.common.ui.view.text.DaxTextView /** Public interface for the Browser URL Bar Privacy and Trackers animations */ interface BrowserTrackersAnimatorHelper { - // TODO ANA: Add the new animation. - /** * This method takes [entities] to create an animation in [trackersAnimationView]. * Then it plays both animations, [shieldAnimationView] and [trackersAnimationView], at the same time. @@ -46,7 +44,7 @@ interface BrowserTrackersAnimatorHelper { entities: List?, ) - fun startNewTrackersAnimation( + fun startExperimentTrackersAnimation( context: Context, shieldAnimationView: LottieAnimationView, trackersBlockedAnimationView: DaxTextView, diff --git a/app/src/main/java/com/duckduckgo/app/global/model/SiteMonitor.kt b/app/src/main/java/com/duckduckgo/app/global/model/SiteMonitor.kt index bbd3323bfdc9..67b51fefbba1 100644 --- a/app/src/main/java/com/duckduckgo/app/global/model/SiteMonitor.kt +++ b/app/src/main/java/com/duckduckgo/app/global/model/SiteMonitor.kt @@ -184,6 +184,10 @@ class SiteMonitor( return PROTECTED } + override fun resetTrackingEvents() { + trackingEvents.clear() + } + @WorkerThread private fun isAllowListed(domain: String): Boolean { return userAllowListRepository.isDomainInUserAllowList(domain) || contentBlocking.isAnException(domain) diff --git a/app/src/main/res/drawable/ic_circle_pulse_green.xml b/app/src/main/res/drawable/ic_circle_pulse_green.xml new file mode 100644 index 000000000000..58ae87eef159 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_pulse_green.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index 2cb708ec7a71..14828a5e39e5 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -21,7 +21,7 @@ android:layout_height="match_parent"> Ochrona prywatności włączona dla %1$s Ochrona prywatności wyłączona dla %1$s Cofnij + Trackers Blocked Pobierz obraz diff --git a/browser-api/src/main/java/com/duckduckgo/app/global/model/Site.kt b/browser-api/src/main/java/com/duckduckgo/app/global/model/Site.kt index 4cb91005bba2..bbe87bfba9fe 100644 --- a/browser-api/src/main/java/com/duckduckgo/app/global/model/Site.kt +++ b/browser-api/src/main/java/com/duckduckgo/app/global/model/Site.kt @@ -69,6 +69,7 @@ interface Site { fun surrogateDetected(surrogate: SurrogateResponse) fun privacyProtection(): PrivacyShield + fun resetTrackingEvents() var urlParametersRemoved: Boolean var consentManaged: Boolean