From ab7be9ae98f608bc68db5eb40a4f806414e9fb91 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 12:24:55 +0100 Subject: [PATCH 01/24] Add methods to manage animation tile dismissal state in TabSwitcherDataStore --- .../app/tabs/store/TabSwitcherDataStore.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt b/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt index 587bdb144254..55c7e225a560 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.tabs.store import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import com.duckduckgo.app.tabs.model.TabSwitcherData @@ -43,6 +44,7 @@ class TabSwitcherPrefsDataStore @Inject constructor( companion object { const val KEY_USER_STATE = "KEY_USER_STATE" const val KEY_LAYOUT_TYPE = "KEY_LAYOUT_TYPE" + const val KEY_IS_ANIMATION_TILE_DISMISSED = "KEY_IS_ANIMATION_TILE_DISMISSED" } override val data: Flow = store.data.map { preferences -> @@ -63,4 +65,16 @@ class TabSwitcherPrefsDataStore @Inject constructor( preferences[stringPreferencesKey(KEY_LAYOUT_TYPE)] = layoutType.name } } + + fun isAnimationTileDismissed(): Flow { + return store.data.map { preferences -> + preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] ?: false + } + } + + suspend fun setIsAnimationTileDismissed(isDismissed: Boolean) { + store.edit { preferences -> + preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] = isDismissed + } + } } From 5abdcebfd235c58a11ef2800168f511e9b28c603 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 12:26:33 +0100 Subject: [PATCH 02/24] Add functionality to show animated tile in DevSettingsActivity Allows to reshow the animation without having to uninstall and reinstall --- .../duckduckgo/app/dev/settings/DevSettingsActivity.kt | 6 ++++++ .../app/dev/settings/DevSettingsViewModel.kt | 10 ++++++++++ app/src/internal/res/layout/activity_dev_settings.xml | 7 +++++++ app/src/internal/res/values/donottranslate.xml | 2 ++ 4 files changed, 25 insertions(+) diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsActivity.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsActivity.kt index 2012518ef5c6..c7146d79f1e3 100644 --- a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsActivity.kt +++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsActivity.kt @@ -109,6 +109,7 @@ class DevSettingsActivity : DuckDuckGoActivity() { binding.customTabs.setOnClickListener { viewModel.customTabsClicked() } binding.notifications.setOnClickListener { viewModel.notificationsClicked() } binding.tabs.setOnClickListener { viewModel.tabsClicked() } + binding.showTabSwitcherAnimatedTile.setOnClickListener { viewModel.showAnimatedTileClicked() } } private fun observeViewModel() { @@ -139,6 +140,7 @@ class DevSettingsActivity : DuckDuckGoActivity() { is CustomTabs -> showCustomTabs() Notifications -> showNotifications() Tabs -> showTabs() + is Command.Toast -> showToast(it.message) } } @@ -187,6 +189,10 @@ class DevSettingsActivity : DuckDuckGoActivity() { startActivity(DevTabsActivity.intent(this)) } + private fun showToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } + companion object { fun intent(context: Context): Intent { return Intent(context, DevSettingsActivity::class.java) diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt index acce1e3e5d0d..606d9a4a9c3e 100644 --- a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt +++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt @@ -22,6 +22,7 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.dev.settings.db.DevSettingsDataStore import com.duckduckgo.app.dev.settings.db.UAOverride import com.duckduckgo.app.survey.api.SurveyEndpointDataStore +import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.savedsites.api.SavedSitesRepository @@ -45,6 +46,7 @@ class DevSettingsViewModel @Inject constructor( private val savedSitesRepository: SavedSitesRepository, private val dispatcherProvider: DispatcherProvider, private val surveyEndpointDataStore: SurveyEndpointDataStore, + private val tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore, ) : ViewModel() { data class ViewState( @@ -62,6 +64,7 @@ class DevSettingsViewModel @Inject constructor( object CustomTabs : Command() data object Notifications : Command() data object Tabs : Command() + data class Toast(val message: String) : Command() } private val viewState = MutableStateFlow(ViewState()) @@ -147,4 +150,11 @@ class DevSettingsViewModel @Inject constructor( fun tabsClicked() { viewModelScope.launch { command.send(Command.Tabs) } } + + fun showAnimatedTileClicked() { + viewModelScope.launch { + tabSwitcherPrefsDataStore.setIsAnimationTileDismissed(isDismissed = false) + command.send(Command.Toast("Animated tile will be shown on next tab switcher open")) + } + } } diff --git a/app/src/internal/res/layout/activity_dev_settings.xml b/app/src/internal/res/layout/activity_dev_settings.xml index a461c1421792..b3f3f52f1340 100644 --- a/app/src/internal/res/layout/activity_dev_settings.xml +++ b/app/src/internal/res/layout/activity_dev_settings.xml @@ -102,6 +102,13 @@ app:primaryText="@string/devSettingsScreenTabs" app:secondaryText="@string/devSettingsScreenTabsSubtitle" /> + + Firefox Default UA WebView + Show TabSwitcher Animated Tile + Click here to show the animated tile if you have previously dismissed it Tabs From ee4eaed408a98f2e9734d64639562bb7e201fa35 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 12:28:21 +0100 Subject: [PATCH 03/24] Implement logic to conditionally show tracker animation tile based on dismissal state --- .../app/tabs/ui/TabSwitcherViewModel.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index 9a2f70afdf61..4a59c1c42a40 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -17,6 +17,7 @@ package com.duckduckgo.app.tabs.ui import androidx.lifecycle.LiveData +import androidx.lifecycle.LiveDataScope import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData @@ -34,6 +35,8 @@ import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType.GRID import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType.LIST +import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore +import com.duckduckgo.app.tabs.ui.TabSwitcherItem.Tab import com.duckduckgo.app.tabs.ui.TabSwitcherItem.TrackerAnimationTile import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository import com.duckduckgo.common.utils.DispatcherProvider @@ -61,19 +64,16 @@ class TabSwitcherViewModel @Inject constructor( private val duckChat: DuckChat, private val tabSwitcherAnimationFeature: TabSwitcherAnimationFeature, private val webTrackersBlockedAppRepository: WebTrackersBlockedAppRepository, + private val tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore, ) : ViewModel() { val tabSwitcherItems: LiveData> = tabRepository.liveTabs.switchMap { tabEntities -> - // TODO use dismissal logic and or test framework to determine whether to show tracker animation tile - if (tabSwitcherAnimationFeature.self().isEnabled()) { - liveData { - val trackerAnimationTile = createTrackerAnimationTile() - val tabItems = tabEntities.map { TabSwitcherItem.Tab(it) } - emit(listOf(trackerAnimationTile) + tabItems) - } - } else { - liveData { - val tabItems = tabEntities.map { TabSwitcherItem.Tab(it) } + // TODO use test framework to determine whether to show tracker animation tile + liveData { + if (tabSwitcherAnimationFeature.self().isEnabled()) { + collectTabItemsWithOptionalAnimationTile(tabEntities) + } else { + val tabItems = tabEntities.map { Tab(it) } emit(tabItems) } } @@ -236,4 +236,19 @@ class TabSwitcherViewModel @Inject constructor( ) return TrackerAnimationTile(trackerCount) } + + private suspend fun LiveDataScope>.collectTabItemsWithOptionalAnimationTile( + tabEntities: List, + ) { + tabSwitcherPrefsDataStore.isAnimationTileDismissed().collect { isDismissed -> + val tabItems = tabEntities.map { Tab(it) } + + val tabSwitcherItems = if (!isDismissed) { + listOf(createTrackerAnimationTile()) + tabItems + } else { + tabItems + } + emit(tabSwitcherItems) + } + } } From b2c3138d00364999c1c1f394493ae433a73b57a6 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 12:28:44 +0100 Subject: [PATCH 04/24] Add click listener for tracker animation tile close action --- .../com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt | 6 ++++++ .../com/duckduckgo/app/tabs/ui/TabSwitcherAdapter.kt | 9 +++++++-- .../com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt | 6 ++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt index 2cfc82df10a6..6824cd56d61c 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt @@ -156,6 +156,12 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine firstTimeLoadingTabsList = savedInstanceState?.getBoolean(KEY_FIRST_TIME_LOADING) ?: true + if(tabSwitcherAnimationFeature.self().isEnabled()) { + tabsAdapter.setAnimationTileCloseClickListener { + viewModel.onTrackerAnimationTileCloseClicked() + } + } + extractIntentExtras() configureViewReferences() setupToolbar(toolbar) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherAdapter.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherAdapter.kt index d6f396dfc062..713fd48daab8 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherAdapter.kt @@ -77,6 +77,7 @@ class TabSwitcherAdapter( private val list = mutableListOf() private var isDragging: Boolean = false private var layoutType: LayoutType = LayoutType.GRID + private var onAnimationTileCloseClickListener: (() -> Unit)? = null init { setHasStableIds(true) @@ -147,7 +148,7 @@ class TabSwitcherAdapter( trackerTextView = holder.binding.text, ) holder.binding.close.setOnClickListener { - // TODO delete + onAnimationTileCloseClickListener?.invoke() } } is TabSwitcherViewHolder.ListTrackerAnimationTileViewHolder -> { @@ -160,7 +161,7 @@ class TabSwitcherAdapter( trackerTextView = holder.binding.title, ) holder.binding.close.setOnClickListener { - // TODO delete + onAnimationTileCloseClickListener?.invoke() } } else -> throw IllegalArgumentException("Unknown ViewHolder type: $holder") @@ -383,6 +384,10 @@ class TabSwitcherAdapter( notifyDataSetChanged() } + fun setAnimationTileCloseClickListener(onClick: () -> Unit) { + onAnimationTileCloseClickListener = onClick + } + companion object { private const val DUCKDUCKGO_TITLE_SUFFIX = "at DuckDuckGo" } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index 4a59c1c42a40..32379cf243a9 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -227,6 +227,12 @@ class TabSwitcherViewModel @Inject constructor( } } + fun onTrackerAnimationTileCloseClicked() { + viewModelScope.launch(dispatcherProvider.io()) { + tabSwitcherPrefsDataStore.setIsAnimationTileDismissed(isDismissed = true) + } + } + private suspend fun createTrackerAnimationTile(): TrackerAnimationTile { val now = LocalDateTime.now() val trackerCount = From b20782cd5ed9117f288a3883f3b0322751fd4212 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 12:29:58 +0100 Subject: [PATCH 05/24] Remove redundant qualifier names --- .../java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index 32379cf243a9..55fdffb41748 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -97,7 +97,7 @@ class TabSwitcherViewModel @Inject constructor( suspend fun onNewTabRequested(fromOverflowMenu: Boolean) { if (swipingTabsFeature.isEnabled) { - val tabItemList = tabSwitcherItems.value?.filterIsInstance() + val tabItemList = tabSwitcherItems.value?.filterIsInstance() val emptyTabItem = tabItemList?.firstOrNull { tabItem -> tabItem.tabEntity.url.isNullOrBlank() } val emptyTabId = emptyTabItem?.tabEntity?.tabId @@ -159,8 +159,8 @@ class TabSwitcherViewModel @Inject constructor( viewModelScope.launch(dispatcherProvider.io()) { tabSwitcherItems.value?.forEach { tabSwitcherItem -> when (tabSwitcherItem) { - is TabSwitcherItem.Tab -> onTabDeleted(tabSwitcherItem.tabEntity) - is TabSwitcherItem.TrackerAnimationTile -> Unit // TODO delete + is Tab -> onTabDeleted(tabSwitcherItem.tabEntity) + is TrackerAnimationTile -> Unit // TODO delete } } // Make sure all exemptions are removed as all tabs are deleted. From 01f2b255ddd21aca0f314a1ae50de6ed48846aea Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 12:31:18 +0100 Subject: [PATCH 06/24] formatting --- .../main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt index 6824cd56d61c..1fbb750dfcb9 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt @@ -156,7 +156,7 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine firstTimeLoadingTabsList = savedInstanceState?.getBoolean(KEY_FIRST_TIME_LOADING) ?: true - if(tabSwitcherAnimationFeature.self().isEnabled()) { + if (tabSwitcherAnimationFeature.self().isEnabled()) { tabsAdapter.setAnimationTileCloseClickListener { viewModel.onTrackerAnimationTileCloseClicked() } From 1ae4adca3ed70655bc4e7d3743014ee9da209b19 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 12:38:58 +0100 Subject: [PATCH 07/24] Fix tests --- .../com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt index f4271bf63a59..7a1de2c14ac3 100644 --- a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt @@ -37,6 +37,7 @@ import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType.GRID import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType.LIST import com.duckduckgo.app.tabs.model.TabSwitcherData.UserState.EXISTING import com.duckduckgo.app.tabs.model.TabSwitcherData.UserState.NEW +import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository import com.duckduckgo.common.test.CoroutineTestRule @@ -102,6 +103,9 @@ class TabSwitcherViewModelTest { @Mock private lateinit var mockWebTrackersBlockedAppRepository: WebTrackersBlockedAppRepository + @Mock + private lateinit var mockTabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore + private val swipingTabsFeature = FakeFeatureToggleFactory.create(SwipingTabsFeature::class.java) private val tabSwitcherAnimationFeature = FakeFeatureToggleFactory.create(TabSwitcherAnimationFeature::class.java) @@ -146,6 +150,7 @@ class TabSwitcherViewModelTest { duckChatMock, tabSwitcherAnimationFeature, mockWebTrackersBlockedAppRepository, + mockTabSwitcherPrefsDataStore, ) testee.command.observeForever(mockCommandObserver) } From e28b958763c6daf264118f3951a66237451d278d Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 16:59:16 +0100 Subject: [PATCH 08/24] Add tracker animation tile conditionally based on tracker count We only show if you have 10 trackers or more Next we'll check the tab count --- .../duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index 55fdffb41748..c5d330e82276 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -248,13 +248,21 @@ class TabSwitcherViewModel @Inject constructor( ) { tabSwitcherPrefsDataStore.isAnimationTileDismissed().collect { isDismissed -> val tabItems = tabEntities.map { Tab(it) } + val trackerCount = getTrackerCountForLast7Days() - val tabSwitcherItems = if (!isDismissed) { - listOf(createTrackerAnimationTile()) + tabItems + val tabSwitcherItems = if (!isDismissed && trackerCount >= 10) { + listOf(TrackerAnimationTile(trackerCount)) + tabItems } else { tabItems } emit(tabSwitcherItems) } } + + private suspend fun getTrackerCountForLast7Days(): Int { + return webTrackersBlockedAppRepository.getTrackersCountBetween( + startTime = LocalDateTime.now().minusDays(7), + endTime = LocalDateTime.now(), + ) + } } From eedce311f49de05052dd24c1ea97c31ff3e225f0 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 4 Mar 2025 17:03:41 +0100 Subject: [PATCH 09/24] Update tracker animation tile dismissal logic and conditionally include based on tab count Only show if we have 2 or more tabs open Next we'll extract this logic out --- .../java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index c5d330e82276..da0b8a6e8a18 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -250,7 +250,7 @@ class TabSwitcherViewModel @Inject constructor( val tabItems = tabEntities.map { Tab(it) } val trackerCount = getTrackerCountForLast7Days() - val tabSwitcherItems = if (!isDismissed && trackerCount >= 10) { + val tabSwitcherItems = if (!isDismissed && trackerCount >= 10 && tabEntities.count() >= 2) { listOf(TrackerAnimationTile(trackerCount)) + tabItems } else { tabItems From d48ee6a83355f9c6c4df3f2aafae63070b9e061c Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 09:06:58 +0100 Subject: [PATCH 10/24] Move Tracker Animation Tile logic to TabSwitcherTileAnimationMonitor The logic for determining the visibility of the Tracker Animation Tile has been moved to a new `TabSwitcherTileAnimationMonitor` class. This includes: - Determining the tracker count in the last 7 days. - Determining the number of open tabs. - Checking if the animation tile is dismissed. - Defining the minimum requirements for displaying the tile (minimum tracker count and minimum tab count). The `WebTrackersBlockedAppRepository` has been updated to have a new public method to retrieve the number of tracker count for last 7 days. The `TabSwitcherViewModel` was refactored to use the new `TabSwitcherTileAnimationMonitor` and the `observeAnimationTileVisibility` function. The `createTrackerAnimationTile` and `getTrackerCountForLast7Days` were deleted. --- .../ui/TabSwitcherTileAnimationMonitor.kt | 54 +++++++++++++++++++ .../app/tabs/ui/TabSwitcherViewModel.kt | 28 +++------- .../api/WebTrackersBlockedAppRepository.kt | 9 +++- 3 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt new file mode 100644 index 000000000000..7274401734f1 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt @@ -0,0 +1,54 @@ +/* + * 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.tabs.ui + +import com.duckduckgo.app.tabs.model.TabDataRepository +import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore +import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository +import com.duckduckgo.common.utils.DispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private const val MINIMUM_TRACKER_COUNT = 10 +private const val MINIMUM_TAB_COUNT = 2 + +class TabSwitcherTileAnimationMonitor @Inject constructor( + private val dispatchProvider: DispatcherProvider, + private val tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore, + private val tabDataRepository: TabDataRepository, + private val webTrackersBlockedAppRepository: WebTrackersBlockedAppRepository, +) { + + fun observeAnimationTileVisibility(): Flow = tabSwitcherPrefsDataStore.isAnimationTileDismissed() + .map { isAnimationTileDismissed -> + if (!isAnimationTileDismissed) { + shouldDisplayAnimationTile() + } else { + false + } + }.flowOn(dispatchProvider.io()) + + private suspend fun shouldDisplayAnimationTile(): Boolean { + val openedTabs = tabDataRepository.getOpenTabCount() + val trackerCountForLast7Days = webTrackersBlockedAppRepository.getTrackerCountForLast7Days() + + return trackerCountForLast7Days >= MINIMUM_TRACKER_COUNT && + openedTabs >= MINIMUM_TAB_COUNT + } +} diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index da0b8a6e8a18..c03085930c1b 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -45,7 +45,6 @@ import com.duckduckgo.common.utils.extensions.toBinaryString import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.duckchat.api.DuckChat import com.duckduckgo.duckchat.impl.DuckChatPixelName -import java.time.LocalDateTime import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.first @@ -65,6 +64,7 @@ class TabSwitcherViewModel @Inject constructor( private val tabSwitcherAnimationFeature: TabSwitcherAnimationFeature, private val webTrackersBlockedAppRepository: WebTrackersBlockedAppRepository, private val tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore, + private val tabSwitcherTileAnimationMonitor: TabSwitcherTileAnimationMonitor, ) : ViewModel() { val tabSwitcherItems: LiveData> = tabRepository.liveTabs.switchMap { tabEntities -> @@ -233,36 +233,20 @@ class TabSwitcherViewModel @Inject constructor( } } - private suspend fun createTrackerAnimationTile(): TrackerAnimationTile { - val now = LocalDateTime.now() - val trackerCount = - webTrackersBlockedAppRepository.getTrackersCountBetween( - startTime = now.minusDays(7), - endTime = now, - ) - return TrackerAnimationTile(trackerCount) - } - private suspend fun LiveDataScope>.collectTabItemsWithOptionalAnimationTile( tabEntities: List, ) { - tabSwitcherPrefsDataStore.isAnimationTileDismissed().collect { isDismissed -> + tabSwitcherTileAnimationMonitor.observeAnimationTileVisibility().collect { isVisible -> val tabItems = tabEntities.map { Tab(it) } - val trackerCount = getTrackerCountForLast7Days() - val tabSwitcherItems = if (!isDismissed && trackerCount >= 10 && tabEntities.count() >= 2) { - listOf(TrackerAnimationTile(trackerCount)) + tabItems + val tabSwitcherItems = if (isVisible) { + val trackerCountForLast7Days = webTrackersBlockedAppRepository.getTrackerCountForLast7Days() + + listOf(TrackerAnimationTile(trackerCountForLast7Days)) + tabItems } else { tabItems } emit(tabSwitcherItems) } } - - private suspend fun getTrackerCountForLast7Days(): Int { - return webTrackersBlockedAppRepository.getTrackersCountBetween( - startTime = LocalDateTime.now().minusDays(7), - endTime = LocalDateTime.now(), - ) - } } diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/api/WebTrackersBlockedAppRepository.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/api/WebTrackersBlockedAppRepository.kt index 798e65f5cdfb..3846867f2cb3 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/api/WebTrackersBlockedAppRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/api/WebTrackersBlockedAppRepository.kt @@ -42,7 +42,14 @@ class WebTrackersBlockedAppRepository @Inject constructor(appDatabase: AppDataba } // TODO move to public API if experiment kept - suspend fun getTrackersCountBetween( + suspend fun getTrackerCountForLast7Days(): Int { + return getTrackersCountBetween( + startTime = LocalDateTime.now().minusDays(7), + endTime = LocalDateTime.now(), + ) + } + + private suspend fun getTrackersCountBetween( startTime: LocalDateTime, endTime: LocalDateTime, ): Int = dao.getTrackersCountBetween( From d8e8fab2be81fe834e312e7e2f263efd5a2e733c Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 09:42:52 +0100 Subject: [PATCH 11/24] Introduce Animation Tile Seen State - Adds a new state to track if the animation tile has been seen. - Modifies the logic for displaying the animation tile to include the new state. - The animation tile will show if NOT dismissed but already seen, even if we have less than 2 tabs --- .../app/tabs/store/TabSwitcherDataStore.kt | 13 ++++++++++++ .../ui/TabSwitcherTileAnimationMonitor.kt | 20 ++++++++++--------- .../app/tabs/ui/TabSwitcherViewModel.kt | 1 + 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt b/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt index 55c7e225a560..021af14c1eba 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt @@ -45,6 +45,7 @@ class TabSwitcherPrefsDataStore @Inject constructor( const val KEY_USER_STATE = "KEY_USER_STATE" const val KEY_LAYOUT_TYPE = "KEY_LAYOUT_TYPE" const val KEY_IS_ANIMATION_TILE_DISMISSED = "KEY_IS_ANIMATION_TILE_DISMISSED" + const val KEY_HAS_ANIMATION_TILE_BEEN_SEEN = "KEY_HAS_ANIMATION_TILE_BEEN_SEEN" } override val data: Flow = store.data.map { preferences -> @@ -77,4 +78,16 @@ class TabSwitcherPrefsDataStore @Inject constructor( preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] = isDismissed } } + + fun hasAnimationTileBeenSeen(): Flow { + return store.data.map { preferences -> + preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] ?: false + } + } + + suspend fun setAnimationTileSeen() { + store.edit { preferences -> + preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] = true + } + } } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt index 7274401734f1..82ca073b17bb 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt @@ -21,8 +21,8 @@ import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository import com.duckduckgo.common.utils.DispatcherProvider import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import javax.inject.Inject private const val MINIMUM_TRACKER_COUNT = 10 @@ -35,14 +35,16 @@ class TabSwitcherTileAnimationMonitor @Inject constructor( private val webTrackersBlockedAppRepository: WebTrackersBlockedAppRepository, ) { - fun observeAnimationTileVisibility(): Flow = tabSwitcherPrefsDataStore.isAnimationTileDismissed() - .map { isAnimationTileDismissed -> - if (!isAnimationTileDismissed) { - shouldDisplayAnimationTile() - } else { - false - } - }.flowOn(dispatchProvider.io()) + fun observeAnimationTileVisibility(): Flow = combine( + tabSwitcherPrefsDataStore.isAnimationTileDismissed(), + tabSwitcherPrefsDataStore.hasAnimationTileBeenSeen(), + ) { isAnimationTileDismissed, hasAnimationTileBeenSeen -> + when { + isAnimationTileDismissed -> false + hasAnimationTileBeenSeen -> true + else -> shouldDisplayAnimationTile() + } + }.flowOn(dispatchProvider.io()) private suspend fun shouldDisplayAnimationTile(): Boolean { val openedTabs = tabDataRepository.getOpenTabCount() diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index c03085930c1b..7827b333376b 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -240,6 +240,7 @@ class TabSwitcherViewModel @Inject constructor( val tabItems = tabEntities.map { Tab(it) } val tabSwitcherItems = if (isVisible) { + tabSwitcherPrefsDataStore.setAnimationTileSeen() val trackerCountForLast7Days = webTrackersBlockedAppRepository.getTrackerCountForLast7Days() listOf(TrackerAnimationTile(trackerCountForLast7Days)) + tabItems From 8af5cd149fb50ea1238b1c0d2ec346faa154e6f6 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 10:58:30 +0100 Subject: [PATCH 12/24] Add animation tile visibility logic and tests --- .../app/tabs/store/TabSwitcherDataStore.kt | 12 +- .../ui/TabSwitcherTileAnimationMonitor.kt | 4 +- .../ui/TabSwitcherTileAnimationMonitorTest.kt | 173 ++++++++++++++++++ 3 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt diff --git a/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt b/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt index 021af14c1eba..bd3a313dab33 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt @@ -35,6 +35,10 @@ interface TabSwitcherDataStore { suspend fun setUserState(userState: UserState) suspend fun setTabLayoutType(layoutType: LayoutType) + fun isAnimationTileDismissed(): Flow + suspend fun setIsAnimationTileDismissed(isDismissed: Boolean) + fun hasAnimationTileBeenSeen(): Flow + suspend fun setAnimationTileSeen() } @ContributesBinding(AppScope::class) @@ -67,25 +71,25 @@ class TabSwitcherPrefsDataStore @Inject constructor( } } - fun isAnimationTileDismissed(): Flow { + override fun isAnimationTileDismissed(): Flow { return store.data.map { preferences -> preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] ?: false } } - suspend fun setIsAnimationTileDismissed(isDismissed: Boolean) { + override suspend fun setIsAnimationTileDismissed(isDismissed: Boolean) { store.edit { preferences -> preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] = isDismissed } } - fun hasAnimationTileBeenSeen(): Flow { + override fun hasAnimationTileBeenSeen(): Flow { return store.data.map { preferences -> preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] ?: false } } - suspend fun setAnimationTileSeen() { + override suspend fun setAnimationTileSeen() { store.edit { preferences -> preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] = true } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt index 82ca073b17bb..dd84aa8b1e9c 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt @@ -17,7 +17,7 @@ package com.duckduckgo.app.tabs.ui import com.duckduckgo.app.tabs.model.TabDataRepository -import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore +import com.duckduckgo.app.tabs.store.TabSwitcherDataStore import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository import com.duckduckgo.common.utils.DispatcherProvider import kotlinx.coroutines.flow.Flow @@ -30,7 +30,7 @@ private const val MINIMUM_TAB_COUNT = 2 class TabSwitcherTileAnimationMonitor @Inject constructor( private val dispatchProvider: DispatcherProvider, - private val tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore, + private val tabSwitcherPrefsDataStore: TabSwitcherDataStore, private val tabDataRepository: TabDataRepository, private val webTrackersBlockedAppRepository: WebTrackersBlockedAppRepository, ) { diff --git a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt new file mode 100644 index 000000000000..793f3771cf31 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt @@ -0,0 +1,173 @@ +package com.duckduckgo.app.tabs.ui + +import app.cash.turbine.test +import com.duckduckgo.app.tabs.model.TabDataRepository +import com.duckduckgo.app.tabs.model.TabSwitcherData +import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType +import com.duckduckgo.app.tabs.model.TabSwitcherData.UserState +import com.duckduckgo.app.tabs.store.TabSwitcherDataStore +import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository +import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever + +class TabSwitcherTileAnimationMonitorTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private val testDispatcherProvider = coroutinesTestRule.testDispatcherProvider + private lateinit var fakeTabSwitcherPrefsDataStore: TabSwitcherDataStore + + @Mock + private lateinit var mockTabDataRepository: TabDataRepository + + @Mock + private lateinit var mockWebTrackersBlockedAppRepository: WebTrackersBlockedAppRepository + + private lateinit var testee: TabSwitcherTileAnimationMonitor + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + + fakeTabSwitcherPrefsDataStore = FakeTabSwitcherPrefsDataStore() + + testee = TabSwitcherTileAnimationMonitor( + testDispatcherProvider, + fakeTabSwitcherPrefsDataStore, + mockTabDataRepository, + mockWebTrackersBlockedAppRepository + ) + } + + @Test + fun `when animation tile has been dismissed then animation tile not shown`() = runTest { + fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(true) + + testee.observeAnimationTileVisibility().test { + assertEquals(false, awaitItem()) + } + } + + @Test + fun `when animation tile has been seen then animation tile shown`() = runTest { + fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) + fakeTabSwitcherPrefsDataStore.setAnimationTileSeen() + + testee.observeAnimationTileVisibility().test { + assertEquals(true, awaitItem()) + } + } + + @Test + fun `when animation tile has not been dismissed and minimum requirements met then animation tile shown`() = runTest { + fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) + + whenever(mockTabDataRepository.getOpenTabCount()).thenReturn(2) + whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(10) + + testee.observeAnimationTileVisibility().test { + assertEquals(true, awaitItem()) + } + } + + @Test + fun `when animation tile not dismissed and seen tracker count below minimum then animation tile not shown`() = runTest { + fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) + + whenever(mockTabDataRepository.getOpenTabCount()).thenReturn(2) + whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(9) + + testee.observeAnimationTileVisibility().test { + assertEquals(false, awaitItem()) + } + } + + @Test + fun `when animation tile not dismissed and tab count below minimum then animation tile not shown`() = runTest { + fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) + + whenever(mockTabDataRepository.getOpenTabCount()).thenReturn(1) + whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(10) + + testee.observeAnimationTileVisibility().test { + assertEquals(false, awaitItem()) + } + } + + @Test + fun `when animation tile not dismissed and tracker count and tab count below minimum then animation tile not shown`() = runTest { + fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) + + whenever(mockTabDataRepository.getOpenTabCount()).thenReturn(1) + whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(9) + + testee.observeAnimationTileVisibility().test { + assertEquals(false, awaitItem()) + } + } + + @Test + fun `when animation tile not dismissed and tracker count and tab count empty then animation tile not shown`() = runTest { + fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) + + whenever(mockTabDataRepository.getOpenTabCount()).thenReturn(0) + whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(0) + + testee.observeAnimationTileVisibility().test { + assertEquals(false, awaitItem()) + } + } + + @Test + fun `when animation tile not dismissed and has been seen and tracker count tab count below minimum then animation tile shown`() = runTest { + fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) + fakeTabSwitcherPrefsDataStore.setAnimationTileSeen() + + whenever(mockTabDataRepository.getOpenTabCount()).thenReturn(1) + whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(10) + + testee.observeAnimationTileVisibility().test { + assertEquals(true, awaitItem()) + } + } + + private class FakeTabSwitcherPrefsDataStore : TabSwitcherDataStore { + + private val _isAnimationTileDismissedFlow = MutableStateFlow(false) + private val _hasAnimationTileBeenSeenFlow = MutableStateFlow(false) + + override val data: Flow + get() = flowOf() // No-op + + override suspend fun setUserState(userState: UserState) { + // No-op + } + + override suspend fun setTabLayoutType(layoutType: LayoutType) { + // No-op + } + + override fun isAnimationTileDismissed(): Flow = _isAnimationTileDismissedFlow + + override suspend fun setIsAnimationTileDismissed(isDismissed: Boolean) { + _isAnimationTileDismissedFlow.value = isDismissed + } + + override fun hasAnimationTileBeenSeen(): Flow = _hasAnimationTileBeenSeenFlow + + override suspend fun setAnimationTileSeen() { + _hasAnimationTileBeenSeenFlow.value = true + } + } +} From 93f12747f56465cfcfd40cdc1b5465e17f82a1a8 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 10:58:43 +0100 Subject: [PATCH 13/24] Add tests for Animation Tile visibility in TabSwitcherViewModel --- .../app/tabs/ui/TabSwitcherViewModelTest.kt | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt index 7a1de2c14ac3..a2986bfbd720 100644 --- a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt @@ -106,6 +106,9 @@ class TabSwitcherViewModelTest { @Mock private lateinit var mockTabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore + @Mock + private lateinit var mockTabSwitcherTileAnimationMonitor: TabSwitcherTileAnimationMonitor + private val swipingTabsFeature = FakeFeatureToggleFactory.create(SwipingTabsFeature::class.java) private val tabSwitcherAnimationFeature = FakeFeatureToggleFactory.create(TabSwitcherAnimationFeature::class.java) @@ -151,6 +154,7 @@ class TabSwitcherViewModelTest { tabSwitcherAnimationFeature, mockWebTrackersBlockedAppRepository, mockTabSwitcherPrefsDataStore, + mockTabSwitcherTileAnimationMonitor, ) testee.command.observeForever(mockCommandObserver) } @@ -434,4 +438,65 @@ class TabSwitcherViewModelTest { verify(mockPixel).fire(DuckChatPixelName.DUCK_CHAT_OPEN_NEW_TAB_MENU, mapOf("was_used_before" to "1")) verify(duckChatMock).openDuckChat() } + + @Test + fun `when Animation Tile Visible then Tab Switcher Items Include Animation Tile And Tabs`() = runTest { + tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = true)) + + val tab1 = TabEntity("1", position = 1) + val tab2 = TabEntity("2", position = 2) + tabs.value = listOf(tab1, tab2) + + whenever(mockTabSwitcherTileAnimationMonitor.observeAnimationTileVisibility()).thenReturn(flowOf(true)) + whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(15) + + initializeViewModel() + + val items = testee.tabSwitcherItems.blockingObserve() ?: listOf() + + assertEquals(3, items.size) + assert(items.first() is TabSwitcherItem.TrackerAnimationTile) + assert(items[1] is TabSwitcherItem.Tab) + assert(items[2] is TabSwitcherItem.Tab) + } + + @Test + fun `when Animation Tile Not Visible then Tab Switcher Items Contain Only Tabs`() = runTest { + tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = true)) + + val tab1 = TabEntity("1", position = 1) + val tab2 = TabEntity("2", position = 2) + tabs.value = listOf(tab1, tab2) + + whenever(mockTabSwitcherTileAnimationMonitor.observeAnimationTileVisibility()).thenReturn(flowOf(false)) + + initializeViewModel() + + val items = testee.tabSwitcherItems.blockingObserve() ?: listOf() + + assertEquals(2, items.size) + items.forEach { item -> + assert(item is TabSwitcherItem.Tab) + } + } + + @Test + fun `when Tab Switcher Animation Feature disabled then Tab Switcher Items Contain Only Tabs`() = runTest { + tabSwitcherAnimationFeature.self().setRawStoredState(State(enable = false)) + + val tab1 = TabEntity("1", position = 1) + val tab2 = TabEntity("2", position = 2) + tabs.value = listOf(tab1, tab2) + + whenever(mockTabSwitcherTileAnimationMonitor.observeAnimationTileVisibility()).thenReturn(flowOf(true)) + + initializeViewModel() + + val items = testee.tabSwitcherItems.blockingObserve() ?: listOf() + + assertEquals(2, items.size) + items.forEach { item -> + assert(item is TabSwitcherItem.Tab) + } + } } From f5ebb86d67a871f23ebb6342d8acdac3d58bec48 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 10:59:47 +0100 Subject: [PATCH 14/24] Formatting --- .../duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt | 2 +- .../app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt index dd84aa8b1e9c..2db708635fef 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitor.kt @@ -20,10 +20,10 @@ import com.duckduckgo.app.tabs.model.TabDataRepository import com.duckduckgo.app.tabs.store.TabSwitcherDataStore import com.duckduckgo.app.trackerdetection.api.WebTrackersBlockedAppRepository import com.duckduckgo.common.utils.DispatcherProvider +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject private const val MINIMUM_TRACKER_COUNT = 10 private const val MINIMUM_TAB_COUNT = 2 diff --git a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt index 793f3771cf31..c91f97c7e632 100644 --- a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt +++ b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt @@ -46,7 +46,7 @@ class TabSwitcherTileAnimationMonitorTest { testDispatcherProvider, fakeTabSwitcherPrefsDataStore, mockTabDataRepository, - mockWebTrackersBlockedAppRepository + mockWebTrackersBlockedAppRepository, ) } From 23ed0b2faedd3fbecc37dfa5098b37588fe377c0 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 11:44:28 +0100 Subject: [PATCH 15/24] Use new gradient background --- .../res/drawable/background_animated_tile.xml | 22 +++++++++++++++++++ .../item_grid_tracker_animation_tile.xml | 2 +- .../item_list_tracker_animation_tile.xml | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/background_animated_tile.xml diff --git a/app/src/main/res/drawable/background_animated_tile.xml b/app/src/main/res/drawable/background_animated_tile.xml new file mode 100644 index 000000000000..ae44256962a1 --- /dev/null +++ b/app/src/main/res/drawable/background_animated_tile.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_grid_tracker_animation_tile.xml b/app/src/main/res/layout/item_grid_tracker_animation_tile.xml index 933550625671..04848ff8d164 100644 --- a/app/src/main/res/layout/item_grid_tracker_animation_tile.xml +++ b/app/src/main/res/layout/item_grid_tracker_animation_tile.xml @@ -19,12 +19,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/keyline_2" - app:cardBackgroundColor="@color/green0" app:cardCornerRadius="10dp" tools:ignore="InvalidColorAttribute"> diff --git a/app/src/main/res/layout/item_list_tracker_animation_tile.xml b/app/src/main/res/layout/item_list_tracker_animation_tile.xml index 31089a3cb977..f14f005b8671 100644 --- a/app/src/main/res/layout/item_list_tracker_animation_tile.xml +++ b/app/src/main/res/layout/item_list_tracker_animation_tile.xml @@ -22,7 +22,6 @@ android:layout_marginTop="6dp" android:layout_marginEnd="7dp" android:layout_marginBottom="10dp" - app:cardBackgroundColor="@color/green0" app:cardCornerRadius="10dp" tools:ignore="InvalidColorAttribute"> @@ -31,6 +30,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:paddingStart="0dp" + android:background="@drawable/background_animated_tile" android:paddingTop="@dimen/twoLineItemVerticalPadding" android:paddingEnd="@dimen/twoLineItemVerticalPadding" android:paddingBottom="@dimen/twoLineItemVerticalPadding"> From fdcacc131427daa144028f962d5fd54636c4ddfd Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 12:00:12 +0100 Subject: [PATCH 16/24] Ensure list view text matches regular tab text We want the text in the Animation tile to match the text in the tabs. --- .../layout/item_list_tracker_animation_tile.xml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/item_list_tracker_animation_tile.xml b/app/src/main/res/layout/item_list_tracker_animation_tile.xml index f14f005b8671..6ffefb85b9d9 100644 --- a/app/src/main/res/layout/item_list_tracker_animation_tile.xml +++ b/app/src/main/res/layout/item_list_tracker_animation_tile.xml @@ -53,33 +53,37 @@ android:layout_height="wrap_content" android:layout_marginEnd="@dimen/keyline_1" android:ellipsize="end" + android:lineSpacingExtra="2sp" android:lines="1" android:textColor="@color/green80" android:textIsSelectable="false" android:textSize="16dp" - android:textStyle="bold" app:layout_constraintBottom_toTopOf="@id/subtitle" app:layout_constraintEnd_toStartOf="@id/close" app:layout_constraintStart_toEndOf="@id/shieldAnimation" app:layout_constraintTop_toTopOf="parent" - tools:ignore="DeprecatedWidgetInXml,SpUsage" - tools:text="93 Trackers blocked" /> + tools:ignore="DeprecatedWidgetInXml,SpUsage,UnusedAttribute" + tools:text="93 Trackers blocked" + tools:textStyle="bold" /> + tools:ignore="DeprecatedWidgetInXml,SpUsage,UnusedAttribute" /> Date: Fri, 7 Mar 2025 15:38:45 +0100 Subject: [PATCH 17/24] Reset animation when "Close All Tabs" is pressed If the user presses close all tabs we reset the tile and it will show again if 2 tabs are visible and more than 10 trackers --- .../java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index 7827b333376b..6e445925b2fb 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -160,7 +160,9 @@ class TabSwitcherViewModel @Inject constructor( tabSwitcherItems.value?.forEach { tabSwitcherItem -> when (tabSwitcherItem) { is Tab -> onTabDeleted(tabSwitcherItem.tabEntity) - is TrackerAnimationTile -> Unit // TODO delete + is TrackerAnimationTile -> { + tabSwitcherPrefsDataStore.setAnimationTileSeen(isSeen = false) + } } } // Make sure all exemptions are removed as all tabs are deleted. From ff33b1bfab76e807161738a664fb814870b895d1 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 16:14:57 +0100 Subject: [PATCH 18/24] Reset seen animation tile state on clear data We've decided to reset the tile if a user clears data as it's done so often that they might not see the tile much. We do not permanently dismiss the tile unless the user clicks the close button themselves --- .../view/ClearPersonalDataActionTest.kt | 26 +++++++++++++++++++ .../com/duckduckgo/app/di/PrivacyModule.kt | 6 +++++ .../global/view/ClearPersonalDataAction.kt | 7 +++++ 3 files changed, 39 insertions(+) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/view/ClearPersonalDataActionTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/view/ClearPersonalDataActionTest.kt index 0357ad5c82ef..7be1f7128beb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/view/ClearPersonalDataActionTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/view/ClearPersonalDataActionTest.kt @@ -27,7 +27,9 @@ import com.duckduckgo.app.fire.UnsentForgetAllPixelStore import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.settings.db.SettingsDataStore +import com.duckduckgo.app.tabs.TabSwitcherAnimationFeature import com.duckduckgo.app.tabs.model.TabRepository +import com.duckduckgo.app.tabs.store.TabSwitcherDataStore import com.duckduckgo.cookies.api.DuckDuckGoCookieManager import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupDataClearer @@ -38,6 +40,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -63,6 +66,10 @@ class ClearPersonalDataActionTest { private val mockSitePermissionsManager: SitePermissionsManager = mock() private val mockPrivacyProtectionsPopupDataClearer: PrivacyProtectionsPopupDataClearer = mock() private val mockNavigationHistory: NavigationHistory = mock() + private val mockTabSwitcherAnimationFeature: TabSwitcherAnimationFeature = mock { + on { self() } doReturn mock() + } + private val mockTabSwitcherDataStore: TabSwitcherDataStore = mock() private val fireproofWebsites: LiveData> = MutableLiveData() @@ -84,6 +91,8 @@ class ClearPersonalDataActionTest { privacyProtectionsPopupDataClearer = mockPrivacyProtectionsPopupDataClearer, sitePermissionsManager = mockSitePermissionsManager, navigationHistory = mockNavigationHistory, + tabSwitcherAnimationFeature = mockTabSwitcherAnimationFeature, + tabSwitcherDataStore = mockTabSwitcherDataStore, ) whenever(mockFireproofWebsiteRepository.getFireproofWebsites()).thenReturn(fireproofWebsites) whenever(mockDeviceSyncState.isUserSignedInOnDevice()).thenReturn(true) @@ -155,4 +164,21 @@ class ClearPersonalDataActionTest { testee.clearTabsAndAllDataAsync(appInForeground = false, shouldFireDataClearPixel = false) verify(mockPrivacyProtectionsPopupDataClearer).clearPersonalData() } + + @Test + fun whenClearCalledAndAnimationTileEnabledThenAnimationTileIsReset() = runTest { + whenever(mockTabSwitcherAnimationFeature.self().isEnabled(any())).thenReturn(true) + + testee.clearTabsAndAllDataAsync(appInForeground = false, shouldFireDataClearPixel = false) + verify(mockTabSwitcherDataStore, never()).setIsAnimationTileDismissed(false) + verify(mockTabSwitcherDataStore).setAnimationTileSeen(false) + } + + @Test + fun whenClearCalledAndAnimationTileDisabledThenAnimationTileIsNotReset() = runTest { + whenever(mockTabSwitcherAnimationFeature.self().isEnabled(any())).thenReturn(false) + + testee.clearTabsAndAllDataAsync(appInForeground = false, shouldFireDataClearPixel = false) + verifyNoInteractions(mockTabSwitcherDataStore) + } } diff --git a/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt b/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt index 3a01a4feacfc..933541cb5777 100644 --- a/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt @@ -36,7 +36,9 @@ import com.duckduckgo.app.location.data.LocationPermissionsDao import com.duckduckgo.app.location.data.LocationPermissionsRepository import com.duckduckgo.app.location.data.LocationPermissionsRepositoryImpl import com.duckduckgo.app.settings.db.SettingsDataStore +import com.duckduckgo.app.tabs.TabSwitcherAnimationFeature import com.duckduckgo.app.tabs.model.TabRepository +import com.duckduckgo.app.tabs.store.TabSwitcherPrefsDataStore import com.duckduckgo.app.trackerdetection.EntityLookup import com.duckduckgo.app.trackerdetection.TdsEntityLookup import com.duckduckgo.app.trackerdetection.db.TdsDomainEntityDao @@ -84,6 +86,8 @@ object PrivacyModule { privacyProtectionsPopupDataClearer: PrivacyProtectionsPopupDataClearer, navigationHistory: NavigationHistory, dispatcherProvider: DispatcherProvider, + tabSwitcherAnimationFeature: TabSwitcherAnimationFeature, + tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore, ): ClearDataAction { return ClearPersonalDataAction( context, @@ -102,6 +106,8 @@ object PrivacyModule { privacyProtectionsPopupDataClearer, navigationHistory, dispatcherProvider, + tabSwitcherAnimationFeature, + tabSwitcherPrefsDataStore, ) } diff --git a/app/src/main/java/com/duckduckgo/app/global/view/ClearPersonalDataAction.kt b/app/src/main/java/com/duckduckgo/app/global/view/ClearPersonalDataAction.kt index e78546ffd9c9..ec2f2c6381e6 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/ClearPersonalDataAction.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/ClearPersonalDataAction.kt @@ -29,7 +29,9 @@ import com.duckduckgo.app.fire.FireActivity import com.duckduckgo.app.fire.UnsentForgetAllPixelStore import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.settings.db.SettingsDataStore +import com.duckduckgo.app.tabs.TabSwitcherAnimationFeature import com.duckduckgo.app.tabs.model.TabRepository +import com.duckduckgo.app.tabs.store.TabSwitcherDataStore import com.duckduckgo.common.utils.DefaultDispatcherProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.cookies.api.DuckDuckGoCookieManager @@ -73,6 +75,8 @@ class ClearPersonalDataAction( private val privacyProtectionsPopupDataClearer: PrivacyProtectionsPopupDataClearer, private val navigationHistory: NavigationHistory, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), + private val tabSwitcherAnimationFeature: TabSwitcherAnimationFeature, + private val tabSwitcherDataStore: TabSwitcherDataStore, ) : ClearDataAction { override fun killAndRestartProcess(notifyDataCleared: Boolean, enableTransitionAnimation: Boolean) { @@ -121,6 +125,9 @@ class ClearPersonalDataAction( dataManager.clearWebViewSessions() tabRepository.deleteAll() adClickManager.clearAll() + if (tabSwitcherAnimationFeature.self().isEnabled()) { + tabSwitcherDataStore.setAnimationTileSeen(isSeen = false) + } setAppUsedSinceLastClearFlag(appInForeground) Timber.d("Finished clearing tabs") } From 8c12631b6bda136adc5998738707d17bb33095ca Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Fri, 7 Mar 2025 18:32:02 +0100 Subject: [PATCH 19/24] Reset setAnimationTileSeen in DevSettings Makes it much easier to test. Renamed the text to make it clear we're resetting as the new logic for showing/hiding determines whether you will see it. --- .../com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt | 3 ++- app/src/internal/res/values/donottranslate.xml | 4 ++-- .../com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt | 6 +++--- .../java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt index 606d9a4a9c3e..744fcce90b0a 100644 --- a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt +++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt @@ -154,7 +154,8 @@ class DevSettingsViewModel @Inject constructor( fun showAnimatedTileClicked() { viewModelScope.launch { tabSwitcherPrefsDataStore.setIsAnimationTileDismissed(isDismissed = false) - command.send(Command.Toast("Animated tile will be shown on next tab switcher open")) + tabSwitcherPrefsDataStore.setAnimationTileSeen(isSeen = false) + command.send(Command.Toast("Animated tile dismissal has been reset")) } } } diff --git a/app/src/internal/res/values/donottranslate.xml b/app/src/internal/res/values/donottranslate.xml index f71840e6acc6..069fd61ce2eb 100644 --- a/app/src/internal/res/values/donottranslate.xml +++ b/app/src/internal/res/values/donottranslate.xml @@ -46,8 +46,8 @@ Firefox Default UA WebView - Show TabSwitcher Animated Tile - Click here to show the animated tile if you have previously dismissed it + Reset TabSwitcher Animated Tile + Click here to reset the animated tile if you have previously dismissed it Tabs diff --git a/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt b/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt index bd3a313dab33..83621c622842 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/store/TabSwitcherDataStore.kt @@ -38,7 +38,7 @@ interface TabSwitcherDataStore { fun isAnimationTileDismissed(): Flow suspend fun setIsAnimationTileDismissed(isDismissed: Boolean) fun hasAnimationTileBeenSeen(): Flow - suspend fun setAnimationTileSeen() + suspend fun setAnimationTileSeen(isSeen: Boolean) } @ContributesBinding(AppScope::class) @@ -89,9 +89,9 @@ class TabSwitcherPrefsDataStore @Inject constructor( } } - override suspend fun setAnimationTileSeen() { + override suspend fun setAnimationTileSeen(isSeen: Boolean) { store.edit { preferences -> - preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] = true + preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] = isSeen } } } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index 6e445925b2fb..d2566c10d8b6 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -242,7 +242,7 @@ class TabSwitcherViewModel @Inject constructor( val tabItems = tabEntities.map { Tab(it) } val tabSwitcherItems = if (isVisible) { - tabSwitcherPrefsDataStore.setAnimationTileSeen() + tabSwitcherPrefsDataStore.setAnimationTileSeen(isSeen = true) val trackerCountForLast7Days = webTrackersBlockedAppRepository.getTrackerCountForLast7Days() listOf(TrackerAnimationTile(trackerCountForLast7Days)) + tabItems From a5c664a0e504b22b7e79c1ebb17db554c98c486e Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Mon, 10 Mar 2025 10:45:37 +0100 Subject: [PATCH 20/24] Fix tests --- .../app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt index c91f97c7e632..c074bccf0c8d 100644 --- a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt +++ b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherTileAnimationMonitorTest.kt @@ -62,7 +62,7 @@ class TabSwitcherTileAnimationMonitorTest { @Test fun `when animation tile has been seen then animation tile shown`() = runTest { fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) - fakeTabSwitcherPrefsDataStore.setAnimationTileSeen() + fakeTabSwitcherPrefsDataStore.setAnimationTileSeen(true) testee.observeAnimationTileVisibility().test { assertEquals(true, awaitItem()) @@ -132,7 +132,7 @@ class TabSwitcherTileAnimationMonitorTest { @Test fun `when animation tile not dismissed and has been seen and tracker count tab count below minimum then animation tile shown`() = runTest { fakeTabSwitcherPrefsDataStore.setIsAnimationTileDismissed(false) - fakeTabSwitcherPrefsDataStore.setAnimationTileSeen() + fakeTabSwitcherPrefsDataStore.setAnimationTileSeen(true) whenever(mockTabDataRepository.getOpenTabCount()).thenReturn(1) whenever(mockWebTrackersBlockedAppRepository.getTrackerCountForLast7Days()).thenReturn(10) @@ -166,7 +166,7 @@ class TabSwitcherTileAnimationMonitorTest { override fun hasAnimationTileBeenSeen(): Flow = _hasAnimationTileBeenSeenFlow - override suspend fun setAnimationTileSeen() { + override suspend fun setAnimationTileSeen(isSeen: Boolean) { _hasAnimationTileBeenSeenFlow.value = true } } From e14ac013afd80100a5401d48aa09ed707dbc5bb8 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Mon, 10 Mar 2025 12:11:08 +0100 Subject: [PATCH 21/24] Add dark ripple background for close button and update layout In dark mode we end up with a white button --- .../item_grid_tracker_animation_tile.xml | 5 ++-- .../item_list_tracker_animation_tile.xml | 3 ++- .../selectable_circular_ripple_dark.xml | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 common/common-ui/src/main/res/drawable/selectable_circular_ripple_dark.xml diff --git a/app/src/main/res/layout/item_grid_tracker_animation_tile.xml b/app/src/main/res/layout/item_grid_tracker_animation_tile.xml index 04848ff8d164..4c1d1193bd7e 100644 --- a/app/src/main/res/layout/item_grid_tracker_animation_tile.xml +++ b/app/src/main/res/layout/item_grid_tracker_animation_tile.xml @@ -32,13 +32,14 @@ android:id="@+id/close" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@drawable/selectable_circular_ripple" + android:background="@drawable/selectable_circular_ripple_dark" android:contentDescription="@string/closeContentDescription" android:padding="@dimen/keyline_2" android:scaleType="center" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_close_24_small" /> + app:srcCompat="@drawable/ic_close_24_small" + app:tint="?attr/daxColorBlack" /> + + + + + + + \ No newline at end of file From 22461c4faf0a31a6c39848b7f7763bb505c67080 Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Mon, 10 Mar 2025 14:52:22 +0100 Subject: [PATCH 22/24] Switch to new shield animation with gradient Something wen wrong with the previous Lottie export for the animation and we were missing the shield gradient. This new animation now has that missing gradient. --- app/src/main/res/layout/item_grid_tracker_animation_tile.xml | 2 +- app/src/main/res/layout/item_list_tracker_animation_tile.xml | 2 +- app/src/main/res/raw/shield_tabswitcher.json | 1 + app/src/main/res/raw/tab_shield.json | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/raw/shield_tabswitcher.json delete mode 100644 app/src/main/res/raw/tab_shield.json diff --git a/app/src/main/res/layout/item_grid_tracker_animation_tile.xml b/app/src/main/res/layout/item_grid_tracker_animation_tile.xml index 4c1d1193bd7e..d1da2178bb82 100644 --- a/app/src/main/res/layout/item_grid_tracker_animation_tile.xml +++ b/app/src/main/res/layout/item_grid_tracker_animation_tile.xml @@ -53,7 +53,7 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" app:lottie_autoPlay="true" - app:lottie_rawRes="@raw/tab_shield" /> + app:lottie_rawRes="@raw/shield_tabswitcher" /> + app:lottie_rawRes="@raw/shield_tabswitcher" /> Date: Mon, 10 Mar 2025 18:26:44 +0100 Subject: [PATCH 23/24] Prevent setting animation tile seen if the first item is already a TrackerAnimationTile This prevents a race condition when close all tabs is called and stops the animated tile staying visible due to the reactive changes that stem from setting the tab as seen/unseen. We need this as a result of recent behaviour changes to close all tabs/fire button scenarios where we do NOT want the animated tile permanently dismissed. Only if a user presses the close button. --- .../java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt index d2566c10d8b6..c235169b4bf7 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt @@ -242,7 +242,9 @@ class TabSwitcherViewModel @Inject constructor( val tabItems = tabEntities.map { Tab(it) } val tabSwitcherItems = if (isVisible) { - tabSwitcherPrefsDataStore.setAnimationTileSeen(isSeen = true) + if (tabSwitcherItems.value?.first() !is TrackerAnimationTile) { + tabSwitcherPrefsDataStore.setAnimationTileSeen(isSeen = true) + } val trackerCountForLast7Days = webTrackersBlockedAppRepository.getTrackerCountForLast7Days() listOf(TrackerAnimationTile(trackerCountForLast7Days)) + tabItems From 4d29f159a54204a3f0a7a347c4c67e1c6b6e631a Mon Sep 17 00:00:00 2001 From: Mike Scamell Date: Tue, 11 Mar 2025 08:18:34 +0100 Subject: [PATCH 24/24] Adjust animation speeds While lower and upper thresholds are now the same I'm leaving them as separate as there might be changes come ship review --- .../java/com/duckduckgo/app/tabs/ui/TrackerCountAnimator.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TrackerCountAnimator.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TrackerCountAnimator.kt index dcb3795b8c20..e5ac246e66fb 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TrackerCountAnimator.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TrackerCountAnimator.kt @@ -28,11 +28,11 @@ import javax.inject.Inject import kotlin.math.roundToInt import kotlin.time.Duration.Companion.seconds -private const val TRACKER_COUNT_LOWER_THRESHOLD_PERCENTAGE = 0.25f -private const val TRACKER_COUNT_UPPER_THRESHOLD_PERCENTAGE = 0.75f +private const val TRACKER_COUNT_LOWER_THRESHOLD_PERCENTAGE = 0.75f +private const val TRACKER_COUNT_UPPER_THRESHOLD_PERCENTAGE = 0.85f private const val TRACKER_COUNT_UPPER_THRESHOLD = 40 private const val TRACKER_TOTAL_MAX_LIMIT = 9999 -private val TRACKER_COUNT_LOWER_THRESHOLD_ANIMATION_DURATION = 0.5.seconds +private val TRACKER_COUNT_LOWER_THRESHOLD_ANIMATION_DURATION = 1.seconds private val TRACKER_COUNT_UPPER_THRESHOLD_ANIMATION_DURATION = 1.seconds class TrackerCountAnimator @Inject constructor() {