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

Swiping tabs: Fix duplicate animations & edit mode detection #5481

Open
wants to merge 19 commits into
base: feature/ondrej/swiping-tabs
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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())
Expand Down
41 changes: 8 additions & 33 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -165,14 +163,6 @@ open class BrowserActivity : DuckDuckGoActivity() {
_currentTab = value
}

private val isInEditMode = SwitcherFlow<Boolean>()

private val isSwipingEnabled by lazy {
combine(viewModel.isOnboardingCompleted, isInEditMode) { isOnboardingCompleted, isInEditMode ->
isOnboardingCompleted && !isInEditMode
}
}

private val viewModel: BrowserViewModel by bindViewModel()

private var instanceStateBundles: CombinedInstanceState? = null
Expand Down Expand Up @@ -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()
}
}

Expand All @@ -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) {
Expand All @@ -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)

Expand Down Expand Up @@ -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 {
Expand All @@ -760,6 +727,10 @@ open class BrowserActivity : DuckDuckGoActivity() {
} else {
showWebContent()
}

if (swipingTabsFeature.isEnabled) {
tabPager.isUserInputEnabled = viewState.isTabSwipingEnabled
}
}
}

Expand Down Expand Up @@ -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?,
Expand Down
13 changes: 11 additions & 2 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 5 additions & 7 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,7 +86,6 @@ class BrowserViewModel @Inject constructor(
private val showOnAppLaunchFeature: ShowOnAppLaunchFeature,
private val showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler,
private val swipingTabsFeature: SwipingTabsFeatureProvider,
userStageStore: UserStageStore,
) : ViewModel(),
CoroutineScope {

Expand All @@ -97,6 +94,7 @@ class BrowserViewModel @Inject constructor(

data class ViewState(
val hideWebContent: Boolean = true,
val isTabSwipingEnabled: Boolean = false,
)

sealed class Command {
Expand Down Expand Up @@ -136,10 +134,6 @@ class BrowserViewModel @Inject constructor(
tabs.indexOf(selectedTab)
}.filterNot { it == -1 }

val isOnboardingCompleted: Flow<Boolean> = userStageStore.currentAppStage
.distinctUntilChanged()
.map { it != AppStage.DAX_ONBOARDING }

private var dataClearingObserver = Observer<ApplicationClearDataState> { state ->
when (state) {
ApplicationClearDataState.INITIALIZING -> {
Expand Down Expand Up @@ -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)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -193,6 +198,8 @@ class OmnibarLayout @JvmOverloads constructor(
R.layout.view_new_omnibar
}
inflate(context, layout, this)

AndroidSupportInjection.inject(this)
}

private fun omnibarViews(): List<View> = listOf(
Expand Down Expand Up @@ -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!!)
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading