Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pixels for Tracker Blocking Animation #5537

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -675,6 +677,8 @@ class BrowserTabViewModelTest {
tabStatsBucketing = mockTabStatsBucketing,
maliciousSiteBlockerWebViewIntegration = mock(),
appPersonalityFeature = fakeAppPersonalityFeature,
userStageStore = mockUserStageStore,
privacyDashboardExternalPixelParams = mockPrivacyDashboardExternalPixelParams,
)

testee.loadData("abc", null, false, false)
Expand Down
46 changes: 38 additions & 8 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -532,7 +533,7 @@ class BrowserTabFragment :
lateinit var webViewCapabilityChecker: WebViewCapabilityChecker

@Inject
lateinit var animatorHelper: TrackersCircleAnimationHelper
lateinit var experimentTrackersAnimationHelper: ExperimentTrackersAnimationHelper

@Inject
lateinit var appPersonalityFeature: AppPersonalityFeature
Expand Down Expand Up @@ -806,16 +807,27 @@ class BrowserTabFragment :

private lateinit var privacyProtectionsPopup: PrivacyProtectionsPopup

private fun showNewTrackersBlockingAnimation(logos: List<TrackerLogo>) {
animatorHelper.startTrackersCircleAnimation(
private fun showExperimentTrackersBurstAnimation(logos: List<TrackerLogo>) {
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")
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -1218,6 +1246,7 @@ class BrowserTabFragment :

override fun onStop() {
alertDialog?.dismiss()
experimentTrackersAnimationHelper.cancelAnimations()
super.onStop()
}

Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -1898,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()) {
Expand Down Expand Up @@ -3095,6 +3102,7 @@ class BrowserTabViewModel @Inject constructor(
}

fun onWebViewRefreshed() {
site?.resetTrackingEvents()
refreshBrowserError()
resetAutoConsent()
accessibilityViewState.value = currentAccessibilityViewState().copy(refreshWebView = false)
Expand Down Expand Up @@ -3792,13 +3800,33 @@ class BrowserTabViewModel @Inject constructor(
}

fun onAnimationFinished(logos: List<TrackerLogo>) {
if (logos.isEmpty()) {
return
}

if (appPersonalityFeature.self().isEnabled() && appPersonalityFeature.trackersBlockedAnimation().isEnabled()) {
command.value = Command.StartTrackersLogosAnimation(logos)
if (logos.size > TRACKER_LOGO_ANIMATION_THRESHOLD) {
command.value = Command.StartExperimentTrackersBurstAnimation(logos)
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")
}
} 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

Expand All @@ -3812,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,
Expand Down
39 changes: 32 additions & 7 deletions app/src/main/java/com/duckduckgo/app/browser/PulseAnimation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
Expand All @@ -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()
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TrackerLogo>,
)

fun cancelAnimations()
}
Loading
Loading