Skip to content

Commit c0dd08f

Browse files
authored
Tab Switcher Animation: Add pixels (#5802)
Task/Issue URL: https://app.asana.com/0/1207908166761516/1209726015931612/f ### Description Added tracking for the tab manager info panel with three new pixels: - `m_tab_manager_info_panel_impressions` - Fired when the panel becomes visible - `m_tab_manager_info_panel_dismissed` - Fired when the panel is dismissed, includes tracker count - `m_tab_manager_info_panel_tapped` - Fired when the panel is tapped Implemented visibility detection for the tracker animation info panel in the tab switcher to accurately track impressions. ### Steps to test this PR Pre-requisite: Enable `tabSwitcherAnimation` feature flag _m_tab_manager_info_panel_impressions_ - [x] Start with no tabs - [x] Open the TabSwitcher - [x] Verify `m_tab_manager_info_panel_impressions` fires - [x] Add many tabs (use developer settings to easily add 100) - [x] Ensure that the active tab is one where you cannot see the animated info panel - [x] Close the TabSwitcher - [x] Open the TabSwitcher - [x] Ensure that `m_tab_manager_info_panel_impressions` is **not** fired - [x] Scroll up slowly to the animated tile until the bottom of the info panel is barely visible - [x] Ensure that `m_tab_manager_info_panel_impressions` is **not** fired - [x] Scroll up until ~75% of the tile is visible - [x] Ensure that `m_tab_manager_info_panel_impressions` **is** fired - [x] Scroll away from the InfoPanel until it is not visible - [x] Scroll back to the InfoPanel - [x] Ensure that `m_tab_manager_info_panel_impressions` **is** fired - [x] Scroll away from the InfoPanel until it is not visible - [x] Switch layouts - [x] Ensure that `m_tab_manager_info_panel_impressions` is **not** fired _m_tab_manager_info_panel_tapped_ - [x] Open the TabSwitcher - [x] Tap on the panel and verify the tapped pixel is fired _m_tab_manager_info_panel_dismissed_ - [x] Dismiss the panel and verify the dismissed pixel is fired with tracker count ### UI changes N/A
1 parent 8d2e53e commit c0dd08f

File tree

4 files changed

+113
-9
lines changed

4 files changed

+113
-9
lines changed

app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,4 +407,8 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
407407
SET_AS_DEFAULT_IN_MENU_CLICK("m_set-as-default_in-menu_click"),
408408

409409
MALICIOUS_SITE_DETECTED_IN_IFRAME("m_malicious-site-protection_iframe-loaded"),
410+
411+
TAB_MANAGER_INFO_PANEL_IMPRESSIONS("m_tab_manager_info_panel_impressions"),
412+
TAB_MANAGER_INFO_PANEL_DISMISSED("m_tab_manager_info_panel_dismissed"),
413+
TAB_MANAGER_INFO_PANEL_TAPPED("m_tab_manager_info_panel_tapped"),
410414
}

app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
3333
import androidx.recyclerview.widget.ItemTouchHelper
3434
import androidx.recyclerview.widget.LinearLayoutManager
3535
import androidx.recyclerview.widget.RecyclerView
36+
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
3637
import com.duckduckgo.anvil.annotations.InjectWith
3738
import com.duckduckgo.app.browser.R
3839
import com.duckduckgo.app.browser.favicon.FaviconManager
@@ -73,6 +74,7 @@ import com.google.android.material.snackbar.BaseTransientBottomBar
7374
import com.google.android.material.snackbar.Snackbar
7475
import javax.inject.Inject
7576
import kotlin.coroutines.CoroutineContext
77+
import kotlin.math.max
7678
import kotlinx.coroutines.CoroutineScope
7779
import kotlinx.coroutines.SupervisorJob
7880
import kotlinx.coroutines.flow.filterNotNull
@@ -143,6 +145,17 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
143145
)
144146
}
145147

148+
private val onScrolledListener = object : OnScrollListener() {
149+
override fun onScrolled(
150+
recyclerView: RecyclerView,
151+
dx: Int,
152+
dy: Int,
153+
) {
154+
super.onScrolled(recyclerView, dx, dy)
155+
checkTrackerAnimationPanelVisibility()
156+
}
157+
}
158+
146159
// we need to scroll to show selected tab, but only if it is the first time loading the tabs.
147160
private var firstTimeLoadingTabsList = true
148161

@@ -158,6 +171,8 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
158171

159172
private var tabSwitcherAnimationTileRemovalDialog: DaxAlertDialog? = null
160173

174+
private var isTrackerAnimationPanelVisible = false
175+
161176
override fun onCreate(savedInstanceState: Bundle?) {
162177
super.onCreate(savedInstanceState)
163178
setContentView(R.layout.activity_tab_switcher)
@@ -216,6 +231,37 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
216231
tabsRecycler.setHasFixedSize(true)
217232
}
218233

234+
private fun checkTrackerAnimationPanelVisibility() {
235+
if (!tabSwitcherAnimationFeature.self().isEnabled()) {
236+
return
237+
}
238+
239+
val layoutManager = tabsRecycler.layoutManager as? LinearLayoutManager ?: return
240+
val firstVisible = layoutManager.findFirstVisibleItemPosition()
241+
val isPanelCurrentlyVisible = firstVisible == 0 && tabsAdapter.getTabSwitcherItem(0) is TrackerAnimationInfoPanel
242+
243+
if (!isPanelCurrentlyVisible) {
244+
isTrackerAnimationPanelVisible = false
245+
return
246+
}
247+
248+
val viewHolder = tabsRecycler.findViewHolderForAdapterPosition(0) ?: return
249+
val itemView = viewHolder.itemView
250+
251+
val itemHeight = itemView.height
252+
val visibleHeight = itemHeight - max(0, -itemView.top) -
253+
max(0, itemView.bottom - tabsRecycler.height)
254+
255+
val isEnoughVisible = visibleHeight > itemHeight * 0.75
256+
257+
if (isEnoughVisible && !isTrackerAnimationPanelVisible) {
258+
viewModel.onTrackerAnimationInfoPanelVisible()
259+
isTrackerAnimationPanelVisible = true
260+
} else if (!isEnoughVisible) {
261+
isTrackerAnimationPanelVisible = false
262+
}
263+
}
264+
219265
private fun configureObservers() {
220266
viewModel.tabSwitcherItems.observe(this) { tabSwitcherItems ->
221267

@@ -255,6 +301,7 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
255301

256302
private fun updateLayoutType(layoutType: LayoutType) {
257303
tabsRecycler.hide()
304+
tabsRecycler.removeOnScrollListener(onScrolledListener)
258305

259306
val centerOffsetPercent = getCurrentCenterOffset()
260307

@@ -276,7 +323,12 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
276323
tabsAdapter.onLayoutTypeChanged(layoutType)
277324
tabTouchHelper.onLayoutTypeChanged(layoutType)
278325

279-
scrollToPreviousCenterOffset(centerOffsetPercent)
326+
scrollToPreviousCenterOffset(
327+
centerOffsetPercent = centerOffsetPercent,
328+
onScrollCompleted = {
329+
tabsRecycler.addOnScrollListener(onScrolledListener)
330+
},
331+
)
280332

281333
tabsRecycler.show()
282334
}
@@ -298,12 +350,18 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
298350
}
299351
}
300352

301-
private fun scrollToPreviousCenterOffset(centerOffsetPercent: Float) {
353+
private fun scrollToPreviousCenterOffset(
354+
centerOffsetPercent: Float,
355+
onScrollCompleted: () -> Unit = {},
356+
) {
302357
tabsRecycler.post {
303358
val newRange = tabsRecycler.computeVerticalScrollRange()
304359
val newExtent = tabsRecycler.computeVerticalScrollExtent()
305360
val newOffset = (centerOffsetPercent * newRange - newExtent / 2).toInt()
306361
(tabsRecycler.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(0, -newOffset)
362+
tabsRecycler.post {
363+
onScrollCompleted()
364+
}
307365
}
308366
}
309367

@@ -456,14 +514,14 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
456514
)
457515
}
458516
}
459-
is TabSwitcherItem.TrackerAnimationInfoPanel -> Unit // TODO delete from list
517+
is TrackerAnimationInfoPanel -> Unit
460518
}
461519
}
462520
}
463521

464522
override fun onTabMoved(from: Int, to: Int) {
465523
if (tabSwitcherAnimationFeature.self().isEnabled()) {
466-
val isTrackerAnimationInfoPanelVisible = viewModel.tabSwitcherItems.value?.get(0) is TabSwitcherItem.TrackerAnimationInfoPanel
524+
val isTrackerAnimationInfoPanelVisible = viewModel.tabSwitcherItems.value?.get(0) is TrackerAnimationInfoPanel
467525
val canSwapFromIndex = if (isTrackerAnimationInfoPanelVisible) 1 else 0
468526
val tabSwitcherItemCount = viewModel.tabSwitcherItems.value?.count() ?: 0
469527

app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel
2828
import com.duckduckgo.app.browser.SwipingTabsFeatureProvider
2929
import com.duckduckgo.app.browser.session.WebViewSessionStorage
3030
import com.duckduckgo.app.pixels.AppPixelName
31+
import com.duckduckgo.app.pixels.AppPixelName.TAB_MANAGER_INFO_PANEL_DISMISSED
32+
import com.duckduckgo.app.pixels.AppPixelName.TAB_MANAGER_INFO_PANEL_TAPPED
3133
import com.duckduckgo.app.statistics.pixels.Pixel
3234
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
3335
import com.duckduckgo.app.tabs.TabSwitcherAnimationFeature
@@ -240,6 +242,7 @@ class TabSwitcherViewModel @Inject constructor(
240242
}
241243

242244
fun onTrackerAnimationInfoPanelClicked() {
245+
pixel.fire(TAB_MANAGER_INFO_PANEL_TAPPED)
243246
command.value = ShowAnimatedTileDismissalDialog
244247
}
245248

@@ -252,9 +255,15 @@ class TabSwitcherViewModel @Inject constructor(
252255
fun onTrackerAnimationTileNegativeButtonClicked() {
253256
viewModelScope.launch {
254257
tabSwitcherDataStore.setIsAnimationTileDismissed(isDismissed = true)
258+
val trackerCount = webTrackersBlockedAppRepository.getTrackerCountForLast7Days()
259+
pixel.fire(pixel = TAB_MANAGER_INFO_PANEL_DISMISSED, parameters = mapOf("trackerCount" to trackerCount.toString()))
255260
}
256261
}
257262

263+
fun onTrackerAnimationInfoPanelVisible() {
264+
pixel.fire(pixel = AppPixelName.TAB_MANAGER_INFO_PANEL_IMPRESSIONS)
265+
}
266+
258267
private suspend fun LiveDataScope<List<TabSwitcherItem>>.collectTabItemsWithOptionalAnimationTile(
259268
tabEntities: List<TabEntity>,
260269
) {

app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ class TabSwitcherViewModelTest {
490490
}
491491

492492
@Test
493-
fun `when Animation Tile Visible then Tab Switcher Items Include Animation Tile And Tabs`() = runTest {
493+
fun `when animated info panel then tab switcher items include animation tile and tabs`() = runTest {
494494
tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = true))
495495

496496
val tab1 = TabEntity("1", position = 1)
@@ -511,7 +511,7 @@ class TabSwitcherViewModelTest {
511511
}
512512

513513
@Test
514-
fun `when Animation Tile Not Visible then Tab Switcher Items Contain Only Tabs`() = runTest {
514+
fun `when animated info panel not visible then tab switcher items contain only tabs`() = runTest {
515515
tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = true))
516516

517517
val tab1 = TabEntity("1", position = 1)
@@ -531,7 +531,7 @@ class TabSwitcherViewModelTest {
531531
}
532532

533533
@Test
534-
fun `when Tab Switcher Animation Feature disabled then Tab Switcher Items Contain Only Tabs`() = runTest {
534+
fun `when tab switcher animation feature disabled then tab switcher items contain only tabs`() = runTest {
535535
initializeViewModel(FakeTabSwitcherDataStore())
536536
tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = false))
537537
whenever(mockTabSwitcherPrefsDataStore.isAnimationTileDismissed()).thenReturn(flowOf(true))
@@ -549,7 +549,7 @@ class TabSwitcherViewModelTest {
549549
}
550550

551551
@Test
552-
fun `when Animation Tile positive button clicked then Animation Tile is still visible`() = runTest {
552+
fun `when animated info panel positive button clicked then animated info panel is still visible`() = runTest {
553553
initializeViewModel(FakeTabSwitcherDataStore())
554554
whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(15)
555555

@@ -567,7 +567,7 @@ class TabSwitcherViewModelTest {
567567
}
568568

569569
@Test
570-
fun `when Animation Tile positive button clicked then Animation Tile is removed`() = runTest {
570+
fun `when animated info panel negative button clicked then animated info panel is removed`() = runTest {
571571
initializeViewModel(FakeTabSwitcherDataStore())
572572

573573
tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = true))
@@ -585,6 +585,39 @@ class TabSwitcherViewModelTest {
585585
assertFalse(items.first() is TabSwitcherItem.TrackerAnimationInfoPanel)
586586
}
587587

588+
@Test
589+
fun `when animated info panel visible then impressions pixel fired`() = runTest {
590+
initializeViewModel(FakeTabSwitcherDataStore())
591+
tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = true))
592+
whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(15)
593+
594+
testee.onTrackerAnimationInfoPanelVisible()
595+
596+
verify(mockPixel).fire(AppPixelName.TAB_MANAGER_INFO_PANEL_IMPRESSIONS)
597+
}
598+
599+
@Test
600+
fun `when animated info panel clicked then tapped pixel fired`() = runTest {
601+
initializeViewModel(FakeTabSwitcherDataStore())
602+
tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = true))
603+
whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(15)
604+
605+
testee.onTrackerAnimationInfoPanelClicked()
606+
607+
verify(mockPixel).fire(AppPixelName.TAB_MANAGER_INFO_PANEL_TAPPED)
608+
}
609+
610+
@Test
611+
fun `when animated info panel negative button clicked then dismiss pixel fired`() = runTest {
612+
initializeViewModel(FakeTabSwitcherDataStore())
613+
tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = true))
614+
whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(15)
615+
616+
testee.onTrackerAnimationTileNegativeButtonClicked()
617+
618+
verify(mockPixel).fire(pixel = AppPixelName.TAB_MANAGER_INFO_PANEL_DISMISSED, parameters = mapOf("trackerCount" to "15"))
619+
}
620+
588621
private class FakeTabSwitcherDataStore : TabSwitcherDataStore {
589622

590623
private val animationTileDismissedFlow = MutableStateFlow(false)

0 commit comments

Comments
 (0)