Skip to content

Commit

Permalink
Spike to understand the feasibility of implementing the tracker block…
Browse files Browse the repository at this point in the history
…ing animation (#5448)

Task/Issue URL:
https://app.asana.com/0/1202552961248957/1209007963871276/f

### Description
Initial draft.

Stacked PR.

---------

Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
  • Loading branch information
anikiki and daxmobile authored Feb 3, 2025
1 parent 6139bad commit 7aa078b
Show file tree
Hide file tree
Showing 60 changed files with 1,290 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ 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.applinks.AppLinksHandler
import com.duckduckgo.app.browser.apppersonality.AppPersonalityFeature
import com.duckduckgo.app.browser.camera.CameraHardwareChecker
import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository
import com.duckduckgo.app.browser.certificates.remoteconfig.SSLCertificatesFeature
Expand Down Expand Up @@ -225,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 @@ -502,6 +504,8 @@ class BrowserTabViewModelTest {
private val mockBrokenSitePrompt: BrokenSitePrompt = mock()
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 @@ -672,6 +676,9 @@ class BrowserTabViewModelTest {
brokenSitePrompt = mockBrokenSitePrompt,
tabStatsBucketing = mockTabStatsBucketing,
maliciousSiteBlockerWebViewIntegration = mock(),
appPersonalityFeature = fakeAppPersonalityFeature,
userStageStore = mockUserStageStore,
privacyDashboardExternalPixelParams = mockPrivacyDashboardExternalPixelParams,
)

testee.loadData("abc", null, false, false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,27 @@ package com.duckduckgo.app.browser.omnibar.animations

import com.airbnb.lottie.LottieAnimationView
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.apppersonality.AppPersonalityFeature
import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.UNPROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.WARNING
import com.duckduckgo.common.ui.store.AppTheme
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

class LottiePrivacyShieldAnimationHelperTest {

private val feature = FakeFeatureToggleFactory.create(AppPersonalityFeature::class.java)

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

testee.setAnimationView(holder, PROTECTED)

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

testee.setAnimationView(holder, PROTECTED)

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

testee.setAnimationView(holder, UNPROTECTED)

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

testee.setAnimationView(holder, UNPROTECTED)

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

testee.setAnimationView(holder, WARNING)

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

testee.setAnimationView(holder, WARNING)

Expand Down
113 changes: 111 additions & 2 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import android.text.SpannableString
import android.text.Spanned
import android.text.style.StyleSpan
import android.view.ContextMenu
import android.view.Gravity
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
Expand All @@ -69,11 +70,13 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.AnyThread
import androidx.annotation.StringRes
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
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 @@ -101,10 +104,12 @@ 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.ExperimentTrackersAnimationHelper
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability
import com.duckduckgo.app.browser.applinks.AppLinksLauncher
import com.duckduckgo.app.browser.applinks.AppLinksSnackBarConfigurator
import com.duckduckgo.app.browser.apppersonality.AppPersonalityFeature
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter
import com.duckduckgo.app.browser.autocomplete.SuggestionItemDecoration
import com.duckduckgo.app.browser.commands.Command
Expand Down Expand Up @@ -138,6 +143,9 @@ import com.duckduckgo.app.browser.newtab.NewTabPageProvider
import com.duckduckgo.app.browser.omnibar.Omnibar
import com.duckduckgo.app.browser.omnibar.Omnibar.OmnibarTextState
import com.duckduckgo.app.browser.omnibar.Omnibar.ViewMode
import com.duckduckgo.app.browser.omnibar.TrackersBlockedViewSlideBehavior
import com.duckduckgo.app.browser.omnibar.animations.TrackerLogo
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
import com.duckduckgo.app.browser.print.PrintDocumentAdapterFactory
import com.duckduckgo.app.browser.print.PrintInjector
import com.duckduckgo.app.browser.print.SinglePrintSafeguardFeature
Expand Down Expand Up @@ -524,6 +532,12 @@ class BrowserTabFragment :
@Inject
lateinit var webViewCapabilityChecker: WebViewCapabilityChecker

@Inject
lateinit var experimentTrackersAnimationHelper: ExperimentTrackersAnimationHelper

@Inject
lateinit var appPersonalityFeature: AppPersonalityFeature

/**
* We use this to monitor whether the user was seeing the in-context Email Protection signup prompt
* This is needed because the activity stack will be cleared if an external link is opened in our browser
Expand Down Expand Up @@ -650,7 +664,14 @@ class BrowserTabFragment :
delay(COOKIES_ANIMATION_DELAY)
}
context?.let {
omnibar.createCookiesAnimation(isCosmetic)
if (appPersonalityFeature.self().isEnabled() &&
appPersonalityFeature.trackersBlockedAnimation().isEnabled() &&
viewModel.trackersCount().isNotEmpty()
) {
omnibar.enqueueCookiesAnimation(isCosmetic)
} else {
omnibar.createCookiesAnimation(isCosmetic)
}
}
}
}
Expand Down Expand Up @@ -786,6 +807,27 @@ class BrowserTabFragment :

private lateinit var privacyProtectionsPopup: PrivacyProtectionsPopup

private fun showExperimentTrackersBurstAnimation(logos: List<TrackerLogo>) {
experimentTrackersAnimationHelper.startTrackersBurstAnimation(
context = requireContext(),
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 @@ -849,6 +891,7 @@ class BrowserTabFragment :
createPopupMenu()

configureOmnibar()
configureTrackersBlockedSlidingView()

if (savedInstanceState == null) {
viewModel.onViewReady()
Expand Down Expand Up @@ -883,6 +926,66 @@ class BrowserTabFragment :
configureCustomTab()
}

private fun configureTrackersBlockedSlidingView() {
if (!appPersonalityFeature.self().isEnabled() || !appPersonalityFeature.trackersBlockedAnimation().isEnabled()) {
return
}

val displayMetrics = resources.displayMetrics
val layoutParams = binding.trackersBlockedSlidingView.layoutParams as CoordinatorLayout.LayoutParams
when (omnibar.omnibarPosition) {
OmnibarPosition.TOP -> {
val elevationInDp = 6
binding.trackersBlockedSlidingView.elevation = elevationInDp * displayMetrics.density
layoutParams.gravity = Gravity.NO_GRAVITY
layoutParams.behavior = null
configureTopOmnibarOffsetChangedListener()
}
OmnibarPosition.BOTTOM -> {
val elevationInDp = 4
binding.trackersBlockedSlidingView.elevation = elevationInDp * displayMetrics.density
layoutParams.gravity = Gravity.BOTTOM
layoutParams.behavior = TrackersBlockedViewSlideBehavior(viewModel.siteLiveData, requireContext())
}
}
}

private fun configureTopOmnibarOffsetChangedListener() {
binding.newOmnibar.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
val totalScrollRange = appBarLayout.totalScrollRange
val scrollFraction = -verticalOffset / totalScrollRange.toFloat()
notifyVerticalOffsetChanged(scrollFraction)
}
}

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()
}
}

private fun onOmnibarTabsButtonPressed() {
launch { viewModel.userLaunchingTabSwitcher() }
}
Expand Down Expand Up @@ -1143,6 +1246,7 @@ class BrowserTabFragment :

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

Expand Down Expand Up @@ -1805,7 +1909,8 @@ class BrowserTabFragment :
binding.autoCompleteSuggestionsList.gone()
browserActivity?.openExistingTab(it.tabId)
}

is Command.StartExperimentTrackersBurstAnimation -> showExperimentTrackersBurstAnimation(it.logos)
is Command.StartExperimentShieldPopAnimation -> showExperimentShieldPopAnimation()
else -> {
// NO OP
}
Expand Down Expand Up @@ -2477,6 +2582,10 @@ class BrowserTabFragment :
true,
)
}

override fun onTrackersCountFinished(logos: List<TrackerLogo>) {
viewModel.onAnimationFinished(logos)
}
},
)
}
Expand Down
Loading

0 comments on commit 7aa078b

Please sign in to comment.