diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 7a0a14167e19..a64860fd20b0 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -2334,7 +2334,7 @@ class BrowserTabViewModelTest { @Test fun whenUserRequestedToOpenNewTabAndEmptyTabExistsThenSelectTheEmptyTab() = runTest { val emptyTabId = "EMPTY_TAB" - tabsLiveData.value = listOf(TabEntity(emptyTabId)) + whenever(mockTabRepository.flowTabs).thenReturn(flowOf(listOf(TabEntity(emptyTabId)))) testee.userRequestedOpeningNewTab() verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 1b07eb3208bb..23e06ae3b0f2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -83,7 +83,6 @@ import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.view.toPx import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.common.utils.SwitcherFlow import com.duckduckgo.common.utils.playstore.PlayStoreUtils import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.navigation.api.GlobalActivityStarter @@ -93,7 +92,6 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -165,14 +163,6 @@ open class BrowserActivity : DuckDuckGoActivity() { _currentTab = value } - private val isInEditMode = SwitcherFlow() - - private val isSwipingEnabled by lazy { - combine(viewModel.isOnboardingCompleted, isInEditMode) { isOnboardingCompleted, isInEditMode -> - isOnboardingCompleted && !isInEditMode - } - } - private val viewModel: BrowserViewModel by bindViewModel() private var instanceStateBundles: CombinedInstanceState? = null @@ -519,9 +509,6 @@ open class BrowserActivity : DuckDuckGoActivity() { lifecycleScope.launch { viewModel.selectedTabFlow.flowWithLifecycle(lifecycle).collectLatest { tabManager.onSelectedTabChanged(it) - - // update the observed edit mode flow observer to the current tab - switchEditModeObserver() } } @@ -530,13 +517,6 @@ open class BrowserActivity : DuckDuckGoActivity() { onMoveToTabRequested(it) } } - - // enable/disable swiping based on the edit mode and onboarding state - lifecycleScope.launch { - isSwipingEnabled.flowWithLifecycle(lifecycle).collectLatest { - tabPager.isUserInputEnabled = it - } - } } else { viewModel.selectedTab.observe(this) { if (it != null) { @@ -552,17 +532,6 @@ open class BrowserActivity : DuckDuckGoActivity() { } } - private fun switchEditModeObserver() { - // switch the edit mode flow to the current tab - tabPager.postDelayed(TAB_SWIPING_OBSERVER_DELAY) { - currentTab?.isInEditMode?.let { - lifecycleScope.launch { - isInEditMode.switch(it) - } - } - } - } - private fun removeObservers() { viewModel.command.removeObservers(this) @@ -742,8 +711,6 @@ open class BrowserActivity : DuckDuckGoActivity() { private const val LAUNCH_FROM_DEDICATED_WEBVIEW = "LAUNCH_FROM_DEDICATED_WEBVIEW" private const val MAX_ACTIVE_TABS = 40 - - private const val TAB_SWIPING_OBSERVER_DELAY = 500L } inner class BrowserStateRenderer { @@ -760,6 +727,10 @@ open class BrowserActivity : DuckDuckGoActivity() { } else { showWebContent() } + + if (swipingTabsFeature.isEnabled) { + tabPager.isUserInputEnabled = viewState.isTabSwipingEnabled + } } } @@ -965,6 +936,10 @@ open class BrowserActivity : DuckDuckGoActivity() { } } + fun onEditModeChanged(isInEditMode: Boolean) { + viewModel.onOmnibarEditModeChanged(isInEditMode) + } + private data class CombinedInstanceState( val originalInstanceState: Bundle?, val newInstanceState: Bundle?, diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index f6d2fee36a26..40c807d20c52 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -627,8 +627,6 @@ class BrowserTabFragment : } } - val isInEditMode by lazy { omnibar.isInEditMode } - private val activityResultPrivacyDashboard = registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> if (result.resultCode == PrivacyDashboardHybridScreenResult.REPORT_SUBMITTED) { binding.rootView.makeSnackbarWithNoBottomInset( @@ -908,6 +906,17 @@ class BrowserTabFragment : configureOmnibarTextInput() configureItemPressedListener() configureCustomTab() + configureEditModeChangeDetection() + } + + private fun configureEditModeChangeDetection() { + if (swipingTabsFeature.isEnabled) { + omnibar.isInEditMode.onEach { isInEditMode -> + if (isActiveTab) { + browserActivity?.onEditModeChanged(isInEditMode) + } + }.launchIn(lifecycleScope) + } } private fun onOmnibarTabsButtonPressed() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 2b5a4241866d..9a65717f589e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -362,6 +362,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn @@ -2568,13 +2569,13 @@ class BrowserTabViewModel @Inject constructor( command.value = GenerateWebViewPreviewImage if (swipingTabsFeature.isEnabled) { - val emptyTab = tabs.value?.firstOrNull { it.url.isNullOrBlank() }?.tabId - if (emptyTab != null) { - viewModelScope.launch { + viewModelScope.launch { + val emptyTab = tabRepository.flowTabs.first().firstOrNull { it.url.isNullOrBlank() }?.tabId + if (emptyTab != null) { tabRepository.select(tabId = emptyTab) + } else { + command.value = LaunchNewTab } - } else { - command.value = LaunchNewTab } } else { command.value = LaunchNewTab diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 9cb3077d8b8a..4fe1be3cc44f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -33,8 +33,6 @@ import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder import com.duckduckgo.app.global.rating.PromptCount -import com.duckduckgo.app.onboarding.store.AppStage -import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.APP_ENJOYMENT_DIALOG_SHOWN import com.duckduckgo.app.pixels.AppPixelName.APP_ENJOYMENT_DIALOG_USER_CANCELLED @@ -88,7 +86,6 @@ class BrowserViewModel @Inject constructor( private val showOnAppLaunchFeature: ShowOnAppLaunchFeature, private val showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler, private val swipingTabsFeature: SwipingTabsFeatureProvider, - userStageStore: UserStageStore, ) : ViewModel(), CoroutineScope { @@ -97,6 +94,7 @@ class BrowserViewModel @Inject constructor( data class ViewState( val hideWebContent: Boolean = true, + val isTabSwipingEnabled: Boolean = false, ) sealed class Command { @@ -136,10 +134,6 @@ class BrowserViewModel @Inject constructor( tabs.indexOf(selectedTab) }.filterNot { it == -1 } - val isOnboardingCompleted: Flow = userStageStore.currentAppStage - .distinctUntilChanged() - .map { it != AppStage.DAX_ONBOARDING } - private var dataClearingObserver = Observer { state -> when (state) { ApplicationClearDataState.INITIALIZING -> { @@ -359,6 +353,10 @@ class BrowserViewModel @Inject constructor( pixel.fire(AppPixelName.SWIPE_TABS_USED) pixel.fire(pixel = AppPixelName.SWIPE_TABS_USED_DAILY, type = Daily()) } + + fun onOmnibarEditModeChanged(isInEditMode: Boolean) { + viewState.value = currentViewState.copy(isTabSwipingEnabled = !isInEditMode) + } } /** diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt index 7e60c0e4894c..4953fada7c40 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayout.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.browser.omnibar +import android.annotation.SuppressLint import android.content.Context import android.graphics.Color import android.graphics.drawable.ColorDrawable @@ -31,15 +32,15 @@ import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.ViewCompat.isAttachedToWindow import androidx.core.view.doOnLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.viewmodel.viewModelFactory import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.PulseAnimation @@ -86,11 +87,9 @@ import com.duckduckgo.di.scopes.FragmentScope import com.google.android.material.appbar.AppBarLayout import dagger.android.support.AndroidSupportInjection import javax.inject.Inject -import kotlinx.coroutines.cancel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber @InjectWith(FragmentScope::class) @@ -143,7 +142,13 @@ class OmnibarLayout @JvmOverloads constructor( @Inject lateinit var dispatchers: DispatcherProvider - private lateinit var pulseAnimation: PulseAnimation + private val lifecycleOwner: LifecycleOwner by lazy { + requireNotNull(findViewTreeLifecycleOwner()) + } + + private val pulseAnimation: PulseAnimation by lazy { + PulseAnimation(lifecycleOwner) + } private var omnibarTextListener: Omnibar.TextListener? = null private var omnibarItemPressedListener: Omnibar.ItemPressedListener? = null @@ -193,6 +198,8 @@ class OmnibarLayout @JvmOverloads constructor( R.layout.view_new_omnibar } inflate(context, layout, this) + + AndroidSupportInjection.inject(this) } private fun omnibarViews(): List = listOf( @@ -243,22 +250,21 @@ class OmnibarLayout @JvmOverloads constructor( private val conflatedCommandJob = ConflatedJob() override fun onAttachedToWindow() { - AndroidSupportInjection.inject(this) super.onAttachedToWindow() - pulseAnimation = PulseAnimation(findViewTreeLifecycleOwner()!!) + val coroutineScope = requireNotNull(findViewTreeLifecycleOwner()?.lifecycleScope) - val coroutineScope = findViewTreeLifecycleOwner()?.lifecycleScope - - conflatedStateJob += viewModel.viewState - .onEach { render(it) } - .launchIn(coroutineScope!!) - - conflatedCommandJob += viewModel.commands() - .onEach { processCommand(it) } - .launchIn(coroutineScope!!) + conflatedStateJob += coroutineScope.launch { + viewModel.viewState.flowWithLifecycle(lifecycleOwner.lifecycle).collectLatest { + render(it) + } + } - viewModel.onAttachedToWindow() + conflatedCommandJob += coroutineScope.launch { + viewModel.commands().flowWithLifecycle(lifecycleOwner.lifecycle).collectLatest { + processCommand(it) + } + } if (decoration != null) { decorateDeferred(decoration!!) @@ -425,8 +431,8 @@ class OmnibarLayout @JvmOverloads constructor( private fun renderTabIcon(viewState: ViewState) { if (viewState.shouldUpdateTabsCount) { - tabsMenu.count = viewState.tabs.count() - tabsMenu.hasUnread = viewState.tabs.firstOrNull { !it.viewed } != null + tabsMenu.count = viewState.tabCount + tabsMenu.hasUnread = viewState.hasUnreadTabs } } @@ -621,33 +627,20 @@ class OmnibarLayout @JvmOverloads constructor( null } - // omnibar only scrollable when browser showing and the fire button is not promoted if (targetView != null) { - if (this::pulseAnimation.isInitialized) { - if (pulseAnimation.isActive) { - pulseAnimation.stop() - } - doOnLayout { - if (this::pulseAnimation.isInitialized) { - pulseAnimation.playOn(targetView) - } - } - } - } else { - if (this::pulseAnimation.isInitialized) { + if (pulseAnimation.isActive) { pulseAnimation.stop() } - } - } - - fun isPulseAnimationPlaying(): Boolean { - return if (this::pulseAnimation.isInitialized) { - pulseAnimation.isActive + doOnLayout { + pulseAnimation.playOn(targetView) + } } else { - false + pulseAnimation.stop() } } + fun isPulseAnimationPlaying() = pulseAnimation.isActive + private fun createCookiesAnimation(isCosmetic: Boolean) { if (this::animatorHelper.isInitialized) { animatorHelper.createCookiesAnimation( diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt index 364bd611eea8..0b5865ea7b3a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt @@ -44,7 +44,6 @@ import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.FIRE_BUTTON_STATE import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Unique -import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.trackerdetection.model.Entity import com.duckduckgo.browser.api.UserBrowserProperties @@ -59,11 +58,11 @@ import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -81,7 +80,13 @@ class OmnibarLayoutViewModel @Inject constructor( ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) - val viewState = _viewState.asStateFlow() + val viewState = combine(_viewState, tabRepository.flowTabs) { state, tabs -> + state.copy( + shouldUpdateTabsCount = tabs.size != state.tabCount && tabs.isNotEmpty(), + tabCount = tabs.size, + hasUnreadTabs = tabs.firstOrNull { !it.viewed } != null, + ) + }.flowOn(dispatcherProvider.io()).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), ViewState()) private val command = Channel(1, DROP_OLDEST) fun commands(): Flow = command.receiveAsFlow() @@ -99,7 +104,8 @@ class OmnibarLayoutViewModel @Inject constructor( val updateOmnibarText: Boolean = false, val shouldMoveCaretToEnd: Boolean = false, val shouldMoveCaretToStart: Boolean = false, - val tabs: List = emptyList(), + val tabCount: Int = 0, + val hasUnreadTabs: Boolean = false, val shouldUpdateTabsCount: Boolean = false, val showVoiceSearch: Boolean = false, val showClearButton: Boolean = false, @@ -126,18 +132,7 @@ class OmnibarLayoutViewModel @Inject constructor( GLOBE, } - fun onAttachedToWindow() { - tabRepository.flowTabs - .onEach { tabs -> - _viewState.update { - it.copy( - shouldUpdateTabsCount = tabs.count() != it.tabs.count() || tabs.isNotEmpty(), - tabs = tabs, - ) - } - }.flowOn(dispatcherProvider.io()) - .launchIn(viewModelScope) - + init { logVoiceSearchAvailability() } diff --git a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index 1d74917360fa..234610690b7b 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -29,7 +29,6 @@ import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder import com.duckduckgo.app.global.rating.PromptCount -import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter @@ -39,7 +38,6 @@ import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State -import com.duckduckgo.tabs.model.TabDataRepositoryTest.Companion.TAB_ID import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.After @@ -85,8 +83,6 @@ class BrowserViewModelTest { @Mock private lateinit var showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler - @Mock private lateinit var userStageStore: UserStageStore - private val fakeShowOnAppLaunchFeatureToggle = FakeFeatureToggleFactory.create(ShowOnAppLaunchFeature::class.java) private lateinit var testee: BrowserViewModel @@ -324,6 +320,24 @@ class BrowserViewModelTest { verify(showOnAppLaunchOptionHandler).handleAppLaunchOption() } + @Test + fun whenOmnibarIsInEditModeTabSwipingIsDisabled() { + swipingTabsFeature.self().setRawStoredState(State(enable = true)) + + val isInEditMode = true + testee.onOmnibarEditModeChanged(isInEditMode) + assertEquals(!isInEditMode, testee.viewState.value!!.isTabSwipingEnabled) + } + + @Test + fun whenOmnibarIsInNotEditModeTabSwipingIsEnabled() { + swipingTabsFeature.self().setRawStoredState(State(enable = true)) + + val isInEditMode = false + testee.onOmnibarEditModeChanged(isInEditMode) + assertEquals(!isInEditMode, testee.viewState.value!!.isTabSwipingEnabled) + } + private fun initTestee() { testee = BrowserViewModel( tabRepository = mockTabRepository, @@ -337,7 +351,6 @@ class BrowserViewModelTest { skipUrlConversionOnNewTabFeature = skipUrlConversionOnNewTabFeature, showOnAppLaunchFeature = fakeShowOnAppLaunchFeatureToggle, showOnAppLaunchOptionHandler = showOnAppLaunchOptionHandler, - userStageStore = userStageStore, swipingTabsFeature = swipingTabsFeatureProvider, ) } diff --git a/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt index 906e4e78e1d8..c3be5a0a49ea 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModelTest.kt @@ -68,29 +68,18 @@ class OmnibarLayoutViewModelTest { @Before fun before() { - testee = OmnibarLayoutViewModel( - tabRepository = tabRepository, - voiceSearchAvailability = voiceSearchAvailability, - voiceSearchPixelLogger = voiceSearchPixelLogger, - duckDuckGoUrlDetector = duckDuckGoUrlDetector, - duckPlayer = duckPlayer, - pixel = pixel, - userBrowserProperties = userBrowserProperties, - dispatcherProvider = coroutineTestRule.testDispatcherProvider, - ) - whenever(tabRepository.flowTabs).thenReturn(flowOf(emptyList())) whenever(voiceSearchAvailability.shouldShowVoiceSearch(any(), any(), any(), any())).thenReturn(true) whenever(duckPlayer.isDuckPlayerUri(DUCK_PLAYER_URL)).thenReturn(true) + + initializeViewModel() } @Test fun whenViewModelAttachedAndNoTabsOpenThenTabsRetrieved() = runTest { - testee.onAttachedToWindow() - testee.viewState.test { val viewState = awaitItem() - assertTrue(viewState.tabs.isEmpty()) + assertTrue(viewState.tabCount == 0) } } @@ -98,20 +87,33 @@ class OmnibarLayoutViewModelTest { fun whenViewModelAttachedAndTabsOpenedThenTabsRetrieved() = runTest { whenever(tabRepository.flowTabs).thenReturn(flowOf(listOf(TabEntity(tabId = "0", position = 0)))) - testee.onAttachedToWindow() + initializeViewModel() testee.viewState.test { val viewState = awaitItem() - assertTrue(viewState.tabs.size == 1) + assertTrue(viewState.tabCount == 1) assertTrue(viewState.shouldUpdateTabsCount) } } + private fun initializeViewModel() { + testee = OmnibarLayoutViewModel( + tabRepository = tabRepository, + voiceSearchAvailability = voiceSearchAvailability, + voiceSearchPixelLogger = voiceSearchPixelLogger, + duckDuckGoUrlDetector = duckDuckGoUrlDetector, + duckPlayer = duckPlayer, + pixel = pixel, + userBrowserProperties = userBrowserProperties, + dispatcherProvider = coroutineTestRule.testDispatcherProvider, + ) + } + @Test fun whenViewModelAttachedAndVoiceSearchSupportedThenPixelLogged() = runTest { whenever(voiceSearchAvailability.isVoiceSearchSupported).thenReturn(true) - testee.onAttachedToWindow() + initializeViewModel() verify(voiceSearchPixelLogger).log() } @@ -120,8 +122,6 @@ class OmnibarLayoutViewModelTest { fun whenViewModelAttachedAndVoiceSearchNotSupportedThenPixelLogged() = runTest { whenever(voiceSearchAvailability.isVoiceSearchSupported).thenReturn(false) - testee.onAttachedToWindow() - verifyNoInteractions(voiceSearchPixelLogger) } diff --git a/common/common-utils/src/main/java/com/duckduckgo/common/utils/SwitcherFlow.kt b/common/common-utils/src/main/java/com/duckduckgo/common/utils/SwitcherFlow.kt deleted file mode 100644 index b8472868b7c5..000000000000 --- a/common/common-utils/src/main/java/com/duckduckgo/common/utils/SwitcherFlow.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.common.utils - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest - -/** - * SwitcherFlow is a utility class that allows switching between multiple upstream flows. - * It uses a MutableSharedFlow to emit the latest upstream flow and ensures that only distinct - * values are emitted by using distinctUntilChanged. - * - * @param T the type of elements emitted by the upstream flows - * @property flowOfSources a MutableSharedFlow that holds the current upstream flow - * - */ -@OptIn(ExperimentalCoroutinesApi::class) -class SwitcherFlow( - private val flowOfSources: MutableSharedFlow> = MutableSharedFlow>(), -) : Flowby flowOfSources.flatMapLatest({ it }).distinctUntilChanged() { - suspend fun switch(upstream: Flow) { - flowOfSources.emit(upstream) - } -}