Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Tab Switcher Animation: Add show/hide behaviour #5729

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ab7be9a
Add methods to manage animation tile dismissal state in TabSwitcherDa…
mikescamell Mar 4, 2025
5abdceb
Add functionality to show animated tile in DevSettingsActivity
mikescamell Mar 4, 2025
ee4eaed
Implement logic to conditionally show tracker animation tile based on…
mikescamell Mar 4, 2025
b2c3138
Add click listener for tracker animation tile close action
mikescamell Mar 4, 2025
b20782c
Remove redundant qualifier names
mikescamell Mar 4, 2025
01f2b25
formatting
mikescamell Mar 4, 2025
1ae4adc
Fix tests
mikescamell Mar 4, 2025
e28b958
Add tracker animation tile conditionally based on tracker count
mikescamell Mar 4, 2025
eedce31
Update tracker animation tile dismissal logic and conditionally inclu…
mikescamell Mar 4, 2025
d48ee6a
Move Tracker Animation Tile logic to TabSwitcherTileAnimationMonitor
mikescamell Mar 7, 2025
d8e8fab
Introduce Animation Tile Seen State
mikescamell Mar 7, 2025
8af5cd1
Add animation tile visibility logic and tests
mikescamell Mar 7, 2025
93f1274
Add tests for Animation Tile visibility in TabSwitcherViewModel
mikescamell Mar 7, 2025
f5ebb86
Formatting
mikescamell Mar 7, 2025
23ed0b2
Use new gradient background
mikescamell Mar 7, 2025
fdcacc1
Ensure list view text matches regular tab text
mikescamell Mar 7, 2025
c6ce1ef
Reset animation when "Close All Tabs" is pressed
mikescamell Mar 7, 2025
ff33b1b
Reset seen animation tile state on clear data
mikescamell Mar 7, 2025
8c12631
Reset setAnimationTileSeen in DevSettings
mikescamell Mar 7, 2025
a5c664a
Fix tests
mikescamell Mar 10, 2025
e14ac01
Add dark ripple background for close button and update layout
mikescamell Mar 10, 2025
22461c4
Switch to new shield animation with gradient
mikescamell Mar 10, 2025
fa14769
Prevent setting animation tile seen if the first item is already a Tr…
mikescamell Mar 10, 2025
4d29f15
Adjust animation speeds
mikescamell Mar 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<List<FireproofWebsiteEntity>> = MutableLiveData()

Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -139,6 +140,7 @@ class DevSettingsActivity : DuckDuckGoActivity() {
is CustomTabs -> showCustomTabs()
Notifications -> showNotifications()
Tabs -> showTabs()
is Command.Toast -> showToast(it.message)
}
}

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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())
Expand Down Expand Up @@ -147,4 +150,12 @@ class DevSettingsViewModel @Inject constructor(
fun tabsClicked() {
viewModelScope.launch { command.send(Command.Tabs) }
}

fun showAnimatedTileClicked() {
viewModelScope.launch {
tabSwitcherPrefsDataStore.setIsAnimationTileDismissed(isDismissed = false)
tabSwitcherPrefsDataStore.setAnimationTileSeen(isSeen = false)
command.send(Command.Toast("Animated tile dismissal has been reset"))
}
}
}
7 changes: 7 additions & 0 deletions app/src/internal/res/layout/activity_dev_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@
app:primaryText="@string/devSettingsScreenTabs"
app:secondaryText="@string/devSettingsScreenTabsSubtitle" />

<com.duckduckgo.common.ui.view.listitem.TwoLineListItem
android:id="@+id/showTabSwitcherAnimatedTile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:primaryText="@string/devSettingsShowTabSwitcherAnimatedTile"
app:secondaryText="@string/devSettingsShowTabSwitcherAnimatedSubtitle" />

<com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
android:id="@+id/privacyTitle"
android:layout_width="wrap_content"
Expand Down
2 changes: 2 additions & 0 deletions app/src/internal/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
<string name="devSettingsScreenUserAgentFirefox">Firefox</string>
<string name="devSettingsScreenUserAgentDefault">Default</string>
<string name="devSettingsScreenUserAgentWebView">UA WebView</string>
<string name="devSettingsShowTabSwitcherAnimatedTile">Reset TabSwitcher Animated Tile</string>
<string name="devSettingsShowTabSwitcherAnimatedSubtitle">Click here to reset the animated tile if you have previously dismissed it</string>

<!-- Tabs -->
<string name="devSettingsScreenTabs">Tabs</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,6 +86,8 @@ object PrivacyModule {
privacyProtectionsPopupDataClearer: PrivacyProtectionsPopupDataClearer,
navigationHistory: NavigationHistory,
dispatcherProvider: DispatcherProvider,
tabSwitcherAnimationFeature: TabSwitcherAnimationFeature,
tabSwitcherPrefsDataStore: TabSwitcherPrefsDataStore,
): ClearDataAction {
return ClearPersonalDataAction(
context,
Expand All @@ -102,6 +106,8 @@ object PrivacyModule {
privacyProtectionsPopupDataClearer,
navigationHistory,
dispatcherProvider,
tabSwitcherAnimationFeature,
tabSwitcherPrefsDataStore,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +35,10 @@ interface TabSwitcherDataStore {

suspend fun setUserState(userState: UserState)
suspend fun setTabLayoutType(layoutType: LayoutType)
fun isAnimationTileDismissed(): Flow<Boolean>
suspend fun setIsAnimationTileDismissed(isDismissed: Boolean)
fun hasAnimationTileBeenSeen(): Flow<Boolean>
suspend fun setAnimationTileSeen(isSeen: Boolean)
}

@ContributesBinding(AppScope::class)
Expand All @@ -43,6 +48,8 @@ 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"
const val KEY_HAS_ANIMATION_TILE_BEEN_SEEN = "KEY_HAS_ANIMATION_TILE_BEEN_SEEN"
}

override val data: Flow<TabSwitcherData> = store.data.map { preferences ->
Expand All @@ -63,4 +70,28 @@ class TabSwitcherPrefsDataStore @Inject constructor(
preferences[stringPreferencesKey(KEY_LAYOUT_TYPE)] = layoutType.name
}
}

override fun isAnimationTileDismissed(): Flow<Boolean> {
return store.data.map { preferences ->
preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] ?: false
}
}

override suspend fun setIsAnimationTileDismissed(isDismissed: Boolean) {
store.edit { preferences ->
preferences[booleanPreferencesKey(KEY_IS_ANIMATION_TILE_DISMISSED)] = isDismissed
}
}

override fun hasAnimationTileBeenSeen(): Flow<Boolean> {
return store.data.map { preferences ->
preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] ?: false
}
}

override suspend fun setAnimationTileSeen(isSeen: Boolean) {
store.edit { preferences ->
preferences[booleanPreferencesKey(KEY_HAS_ANIMATION_TILE_BEEN_SEEN)] = isSeen
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class TabSwitcherAdapter(
private val list = mutableListOf<TabSwitcherItem>()
private var isDragging: Boolean = false
private var layoutType: LayoutType = LayoutType.GRID
private var onAnimationTileCloseClickListener: (() -> Unit)? = null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already a TabSwitcherListener supplied in the constructor that contains a couple of callbacks, WDYT about moving it there?


init {
setHasStableIds(true)
Expand Down Expand Up @@ -147,7 +148,7 @@ class TabSwitcherAdapter(
trackerTextView = holder.binding.text,
)
holder.binding.close.setOnClickListener {
// TODO delete
onAnimationTileCloseClickListener?.invoke()
}
}
is TabSwitcherViewHolder.ListTrackerAnimationTileViewHolder -> {
Expand All @@ -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")
Expand Down Expand Up @@ -383,6 +384,10 @@ class TabSwitcherAdapter(
notifyDataSetChanged()
}

fun setAnimationTileCloseClickListener(onClick: () -> Unit) {
onAnimationTileCloseClickListener = onClick
}

companion object {
private const val DUCKDUCKGO_TITLE_SUFFIX = "at DuckDuckGo"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.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

private const val MINIMUM_TRACKER_COUNT = 10
private const val MINIMUM_TAB_COUNT = 2

class TabSwitcherTileAnimationMonitor @Inject constructor(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

private val dispatchProvider: DispatcherProvider,
private val tabSwitcherPrefsDataStore: TabSwitcherDataStore,
private val tabDataRepository: TabDataRepository,
private val webTrackersBlockedAppRepository: WebTrackersBlockedAppRepository,
) {

fun observeAnimationTileVisibility(): Flow<Boolean> = 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()
val trackerCountForLast7Days = webTrackersBlockedAppRepository.getTrackerCountForLast7Days()

return trackerCountForLast7Days >= MINIMUM_TRACKER_COUNT &&
openedTabs >= MINIMUM_TAB_COUNT
}
}
Loading
Loading