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 swiping: Integration of the new tab management #5322

Open
wants to merge 51 commits into
base: feature/ondrej/tab-swiping-ff-cache
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
990c806
Add a new swiping tabs feature flag
0nko Oct 30, 2024
31521ba
Add a class for checking if tab swiping is enabled
0nko Oct 30, 2024
214b4df
Add an interface for checking if tab swiping is enabled
0nko Oct 30, 2024
ce74105
Update app/src/main/java/com/duckduckgo/app/browser/RealTabSwipingAva…
0nko Nov 9, 2024
25fce10
Optimize imports
0nko Nov 9, 2024
273bbb2
Remove the extra class since onboarding check needs to happen reactively
0nko Nov 11, 2024
df8b2f6
Move the existing tab management functionality to a separate class
0nko Nov 9, 2024
3579d31
Move the tab-related calls to TabManager
0nko Nov 11, 2024
eb1c607
Remove unused code
0nko Nov 11, 2024
b29d761
Clean up code
0nko Nov 11, 2024
d9f13c4
Add onCleanup function
0nko Nov 11, 2024
a6b8d7e
Extract a TabManager interface
0nko Nov 11, 2024
c2b0cd2
Use an injected TabManager for tab-related operations
0nko Nov 11, 2024
d865d18
Fix ktlint issues
0nko Nov 11, 2024
23cf2d4
Rename function for easier readability
0nko Nov 11, 2024
a0dc5cc
Inject the TabManager interface instead of the implementation class
0nko Nov 11, 2024
16af89e
Remove unnecessary null checks
0nko Nov 11, 2024
d3454d5
Add view pager to the layout and create an adapter
0nko Nov 11, 2024
a323814
Initialize the adapter in the tab manager
0nko Nov 11, 2024
c443b8d
Add method to get tab by ID
0nko Nov 11, 2024
57016fd
Optimize the fragment initialization for the view pager use case
0nko Nov 11, 2024
84ba944
Add methods to observe the onboarding status
0nko Nov 11, 2024
d1f1413
Fix the new tab creation
0nko Nov 12, 2024
9b665a0
Handle the new tab creation using a message from another tab
0nko Nov 12, 2024
b581903
Initialize the tab fragment when it becomes active
0nko Nov 17, 2024
9e4c066
Rename the tab manager impelemntation
0nko Nov 18, 2024
1402075
Disable tab deletion when there is just one
0nko Nov 18, 2024
a311686
Switch to the existing empty tab instead of opening a new one
0nko Nov 20, 2024
1635f4d
Update tab callbacks
0nko Nov 21, 2024
e92daba
Change the onActivityCreated to onViewCreated
0nko Nov 21, 2024
c5af1d0
Disable viewpager swiping over the webview
0nko Nov 21, 2024
2b4abae
Check if the tab has changed before calling the callback
0nko Nov 21, 2024
cbf1df2
Clean up
0nko Nov 21, 2024
dedd69a
Use an existing empty tab instead of opening a new one
0nko Nov 21, 2024
24c8fa8
Use a flow to access the currently selected tab
0nko Nov 21, 2024
7dc5e96
Use LiveData instead of Flows
0nko Nov 21, 2024
5eaa361
Merge branch 'develop' into feature/ondrej/multi-tab-refactor
0nko Nov 21, 2024
f9e9255
Fix the merge
0nko Nov 21, 2024
d3ceaea
Merge branch 'feature/ondrej/multi-tab-refactor' into feature/ondrej/…
0nko Nov 21, 2024
ca31c56
Make selected tab flow nullable
0nko Nov 25, 2024
f186a6c
Fix the tab update mechanism and only use 1 tab when swiping disabled
0nko Nov 25, 2024
d780577
Refactor the tab switching mechanism
0nko Nov 26, 2024
f7fdff1
Merge branch 'feature/ondrej/swiping-tabs' into feature/ondrej/swipin…
0nko Nov 26, 2024
da06b78
Merge branch 'feature/ondrej/tab-swiping-ff-cache' into feature/ondre…
0nko Nov 27, 2024
fda2287
Fix the merge
0nko Nov 27, 2024
dcef654
Enable the tab swiping internally
0nko Nov 27, 2024
b5b0df7
Optimize imports
0nko Nov 27, 2024
dbe281c
Clean up
0nko Nov 27, 2024
bb80ee3
Fix unit tests
0nko Nov 27, 2024
181dbb1
Fix the logic for new tab creation on long press
0nko Nov 27, 2024
75535ba
Fix the uni tests
0nko Nov 27, 2024
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 @@ -2423,14 +2423,29 @@ class BrowserTabViewModelTest {
}

@Test
fun whenUserRequestedToOpenNewTabThenNewTabCommandIssued() {
fun whenUserRequestedToOpenNewTabAndNoEmptyTabExistsThenNewTabCommandIssued() {
tabsLiveData.value = listOf(TabEntity("1", "https://example.com", position = 0))
testee.userRequestedOpeningNewTab()
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
val command = commandCaptor.lastValue
assertTrue(command is Command.LaunchNewTab)
verify(mockPixel, never()).fire(AppPixelName.TAB_MANAGER_NEW_TAB_LONG_PRESSED)
}

@Test
fun whenUserRequestedToOpenNewTabAndEmptyTabExistsThenSelectTheEmptyTab() = runTest {
val emptyTabId = "EMPTY_TAB"
tabsLiveData.value = listOf(TabEntity(emptyTabId))
testee.userRequestedOpeningNewTab()

verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
val command = commandCaptor.lastValue
assertFalse(command is Command.LaunchNewTab)

verify(mockTabRepository).select(emptyTabId)
verify(mockPixel, never()).fire(AppPixelName.TAB_MANAGER_NEW_TAB_LONG_PRESSED)
}

@Test
fun whenUserRequestedToOpenNewTabByLongPressThenPixelFired() {
testee.userRequestedOpeningNewTab(longPress = true)
Expand Down
85 changes: 79 additions & 6 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.MarginPageTransformer
import androidx.viewpager2.widget.ViewPager2
import androidx.webkit.ServiceWorkerClientCompat
import androidx.webkit.ServiceWorkerControllerCompat
import androidx.webkit.WebViewFeature
Expand Down Expand Up @@ -71,6 +74,7 @@ import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder
import com.duckduckgo.common.ui.view.gone
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.playstore.PlayStoreUtils
Expand All @@ -80,6 +84,7 @@ import com.duckduckgo.privacy.dashboard.api.ui.PrivacyDashboardHybridScreenParam
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksActivity.Companion.SAVED_SITE_URL_EXTRA
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber

Expand Down Expand Up @@ -129,6 +134,9 @@ open class BrowserActivity : DuckDuckGoActivity() {
@Inject
lateinit var appBuildConfig: AppBuildConfig

@Inject
lateinit var isSwipingTabsFeatureEnabled: IsSwipingTabsFeatureEnabled

@Inject
lateinit var tabManager: TabManager

Expand All @@ -148,8 +156,33 @@ open class BrowserActivity : DuckDuckGoActivity() {

private val binding: ActivityBrowserBinding by viewBinding()

val tabPager: ViewPager2 by lazy {
binding.tabPager
}

private lateinit var toolbarMockupBinding: IncludeOmnibarToolbarMockupBinding

private val onTabPageChangeListener = object : ViewPager2.OnPageChangeCallback() {
private var wasSwipingStarted = false

override fun onPageSelected(position: Int) {
super.onPageSelected(position)
Timber.d("### onPageChanged requested for: $position")
if (wasSwipingStarted) {
Timber.d("### onPageChanged: $position")
tabManager.tabPagerAdapter.onPageChanged(position)
wasSwipingStarted = false
}
}

override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
if (state == ViewPager2.SCROLL_STATE_DRAGGING) {
wasSwipingStarted = true
}
}
}

@VisibleForTesting
var destroyedByBackPress: Boolean = false

Expand Down Expand Up @@ -205,6 +238,10 @@ open class BrowserActivity : DuckDuckGoActivity() {

override fun onDestroy() {
currentTab = null

binding.tabPager.adapter = null
binding.tabPager.unregisterOnPageChangeCallback(onTabPageChangeListener)

super.onDestroy()
}

Expand Down Expand Up @@ -341,18 +378,31 @@ open class BrowserActivity : DuckDuckGoActivity() {
viewModel.command.observe(this) {
processCommand(it)
}
viewModel.selectedTab.observe(this) {
tabManager.onSelectedTabChanged(it)

lifecycleScope.launch {
viewModel.tabs.flowWithLifecycle(lifecycle).collectLatest {
tabManager.onTabsUpdated(it)
lifecycleScope.launch {
viewModel.onTabsUpdated(it.isEmpty())
}
}
}
viewModel.tabs.observe(this) {
tabManager.onTabsUpdated(it)

lifecycleScope.launch {
viewModel.selectedTab.flowWithLifecycle(lifecycle).collectLatest {
tabManager.onSelectedTabChanged(it)
}
}

// listen to onboarding completion to enable/disable swiping
viewModel.isOnboardingCompleted.observe(this) { isOnboardingCompleted ->
tabPager.isUserInputEnabled = isOnboardingCompleted
}
}

private fun removeObservers() {
viewModel.command.removeObservers(this)
viewModel.selectedTab.removeObservers(this)
viewModel.tabs.removeObservers(this)
viewModel.isOnboardingCompleted.removeObservers(this)
}

private fun processCommand(command: Command) {
Expand Down Expand Up @@ -515,6 +565,7 @@ open class BrowserActivity : DuckDuckGoActivity() {

private fun showWebContent() {
Timber.d("BrowserActivity can now start displaying web content. instance state is $instanceStateBundles")
initializeTabs()
configureObservers()
binding.clearingInProgressView.gone()

Expand All @@ -533,6 +584,22 @@ open class BrowserActivity : DuckDuckGoActivity() {
}
}

@SuppressLint("ClickableViewAccessibility", "WrongConstant")
private fun initializeTabs() {
if (isSwipingTabsFeatureEnabled()) {
tabPager.adapter = tabManager.tabPagerAdapter
tabPager.offscreenPageLimit = 1
tabPager.registerOnPageChangeCallback(onTabPageChangeListener)
tabPager.setPageTransformer(MarginPageTransformer(resources.getDimension(com.duckduckgo.mobile.android.R.dimen.keyline_2).toPx().toInt()))

binding.fragmentContainer.gone()
tabPager.show()
} else {
binding.fragmentContainer.show()
tabPager.gone()
}
}

private val Intent.launchedFromRecents: Boolean
get() = (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY

Expand Down Expand Up @@ -632,6 +699,12 @@ open class BrowserActivity : DuckDuckGoActivity() {
playStoreUtils.launchPlayStore()
}

fun onMoveToTabRequested(index: Int) {
Timber.d("### onMoveToTabRequested: $index")

tabPager.currentItem = index
}

private data class CombinedInstanceState(
val originalInstanceState: Bundle?,
val newInstanceState: Bundle?,
Expand Down
57 changes: 46 additions & 11 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,8 @@ class BrowserTabFragment :

private var webView: DuckDuckGoWebView? = null

var isInitialized = false

private val activityResultHandlerEmailProtectionInContextSignup = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
when (result.resultCode) {
EmailProtectionInContextSignUpScreenResult.SUCCESS -> {
Expand Down Expand Up @@ -814,13 +816,16 @@ class BrowserTabFragment :
}

private fun resumeWebView() {
Timber.d("Resuming webview: $tabId")
webView?.let {
if (it.isShown) it.onResume()
}
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
omnibar = Omnibar(settingsDataStore.omnibarPosition, changeOmnibarPositionFeature.refactor().isEnabled(), binding)

webViewContainer = binding.webViewContainer
Expand All @@ -836,9 +841,8 @@ class BrowserTabFragment :
configureOmnibar()

if (savedInstanceState == null) {
viewModel.onViewReady()
messageFromPreviousTab?.let {
processMessage(it)
if (isActiveTab) {
initFragmentIfNecessary()
}
} else {
viewModel.onViewRecreated()
Expand All @@ -847,7 +851,7 @@ class BrowserTabFragment :
lifecycle.addObserver(
@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle
object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
override fun onPause(owner: LifecycleOwner) {
if (isVisible) {
updateOrDeleteWebViewPreview()
}
Expand All @@ -859,6 +863,16 @@ class BrowserTabFragment :
(dialog as EditSavedSiteDialogFragment).listener = viewModel
dialog.deleteBookmarkListener = viewModel
}

disableSwipingOutsideTheOmnibar()
}

private fun disableSwipingOutsideTheOmnibar() {
newBrowserTab.newTabLayout.setOnTouchListener { v, event ->
(v as FrameLayout).requestDisallowInterceptTouchEvent(true)
return@setOnTouchListener true
}
binding.legacyOmnibar.setOnTouchListener { v, event -> false }
}

private fun configureOmnibar() {
Expand Down Expand Up @@ -1136,9 +1150,22 @@ class BrowserTabFragment :
startActivity(TabSwitcherActivity.intent(activity, tabId))
}

private fun initFragmentIfNecessary() {
if (!isInitialized) {
isInitialized = true

viewModel.onViewReady()
messageFromPreviousTab?.let {
processMessage(it)
}
}
}

override fun onResume() {
super.onResume()

initFragmentIfNecessary()

if (viewModel.hasOmnibarPositionChanged(omnibar.omnibarPosition)) {
requireActivity().recreate()
return
Expand Down Expand Up @@ -1308,6 +1335,10 @@ class BrowserTabFragment :
// want to ensure that we aren't offering to inject credentials from an inactive tab
hideDialogWithTag(CredentialAutofillPickerDialog.TAG)
}

if (isActiveTab) {
initFragmentIfNecessary()
}
}
},
)
Expand All @@ -1331,7 +1362,7 @@ class BrowserTabFragment :
newBrowserTab.newTabContainerLayout.show()
binding.browserLayout.gone()
webViewContainer.gone()
omnibar.setViewMode(Omnibar.ViewMode.NewTab)
omnibar.setViewMode(ViewMode.NewTab)
webView?.onPause()
webView?.hide()
errorView.errorLayout.gone()
Expand All @@ -1347,7 +1378,7 @@ class BrowserTabFragment :
webView?.onResume()
errorView.errorLayout.gone()
sslErrorView.gone()
omnibar.setViewMode(Omnibar.ViewMode.Browser(viewModel.url))
omnibar.setViewMode(ViewMode.Browser(viewModel.url))
}

private fun showError(
Expand All @@ -1358,7 +1389,7 @@ class BrowserTabFragment :
newBrowserTab.newTabLayout.gone()
newBrowserTab.newTabContainerLayout.gone()
sslErrorView.gone()
omnibar.setViewMode(Omnibar.ViewMode.Error)
omnibar.setViewMode(ViewMode.Error)
webView?.onPause()
webView?.hide()
errorView.errorMessage.text = getString(errorType.errorId, url).html(requireContext())
Expand All @@ -1379,7 +1410,7 @@ class BrowserTabFragment :
newBrowserTab.newTabContainerLayout.gone()
webView?.onPause()
webView?.hide()
omnibar.setViewMode(Omnibar.ViewMode.SSLWarning)
omnibar.setViewMode(ViewMode.SSLWarning)
errorView.errorLayout.gone()
binding.browserLayout.gone()
sslErrorView.bind(handler, errorResponse) { action ->
Expand Down Expand Up @@ -2424,7 +2455,11 @@ class BrowserTabFragment :
cancelPendingAutofillRequestsToChooseCredentials()
} else {
omnibar.omnibarTextInput.hideKeyboard()
binding.focusDummy.requestFocus()

// prevent a crash when the view is not initiliazed yet
if (view != null) {
binding.focusDummy.requestFocus()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2618,7 +2618,15 @@ class BrowserTabViewModel @Inject constructor(

fun userRequestedOpeningNewTab(longPress: Boolean = false) {
command.value = GenerateWebViewPreviewImage
command.value = LaunchNewTab

val emptyTab = tabs.value?.firstOrNull { it.url.isNullOrBlank() }?.tabId
if (emptyTab != null) {
viewModelScope.launch {
tabRepository.select(tabId = emptyTab)
}
} else {
command.value = LaunchNewTab
}
if (longPress) {
pixel.fire(AppPixelName.TAB_MANAGER_NEW_TAB_LONG_PRESSED)
}
Expand Down
Loading
Loading