diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 56c66641c..000cc6201 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -393,40 +393,86 @@ class HomeFragment : Fragment() { } bundleArgs.getString(SESSION_TO_DELETE)?.also { - sessionManager.findSessionById(it)?.let { session -> - val snapshot = sessionManager.createSessionSnapshot(session) - val state = snapshot.engineSession?.saveState() - val isSelected = - session.id == requireComponents.core.store.state.selectedTabId ?: false - - val snackbarMessage = if (snapshot.session.private) { - requireContext().getString(R.string.snackbar_private_tab_closed) - } else { - requireContext().getString(R.string.snackbar_tab_closed) - } - - viewLifecycleOwner.lifecycleScope.allowUndo( - requireView(), - snackbarMessage, - requireContext().getString(R.string.snackbar_deleted_undo), - { - sessionManager.add( - snapshot.session, - isSelected, - engineSessionState = state - ) - findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)) - }, - operation = { }, - anchorView = snackbarAnchorView - ) - requireComponents.useCases.tabsUseCases.removeTab.invoke(session) + if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) { + removeAllTabsAndShowSnackbar(it) + } else { + removeTabAndShowSnackbar(it) } } updateTabCounter(requireComponents.core.store.state) } + private fun removeAllTabsAndShowSnackbar(sessionCode: String) { + val tabs = sessionManager.sessionsOfType(private = sessionCode == ALL_PRIVATE_TABS).toList() + val selectedIndex = sessionManager + .selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0 + + val snapshot = tabs + .map(sessionManager::createSessionSnapshot) + .map { + it.copy( + engineSession = null, + engineSessionState = it.engineSession?.saveState() + ) + } + .let { SessionManager.Snapshot(it, selectedIndex) } + + tabs.forEach { + sessionManager.remove(it) + } + + val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) { + getString(R.string.snackbar_private_tabs_closed) + } else { + getString(R.string.snackbar_tabs_closed) + } + + viewLifecycleOwner.lifecycleScope.allowUndo( + requireView(), + snackbarMessage, + requireContext().getString(R.string.snackbar_deleted_undo), + { + sessionManager.restore(snapshot) + }, + operation = { }, + anchorView = snackbarAnchorView + ) + } + + private fun removeTabAndShowSnackbar(sessionId: String) { + sessionManager.findSessionById(sessionId)?.let { session -> + val snapshot = sessionManager.createSessionSnapshot(session) + val state = snapshot.engineSession?.saveState() + val isSelected = + session.id == requireComponents.core.store.state.selectedTabId ?: false + + sessionManager.remove(session) + + val snackbarMessage = if (snapshot.session.private) { + requireContext().getString(R.string.snackbar_private_tab_closed) + } else { + requireContext().getString(R.string.snackbar_tab_closed) + } + + viewLifecycleOwner.lifecycleScope.allowUndo( + requireView(), + snackbarMessage, + requireContext().getString(R.string.snackbar_deleted_undo), + { + sessionManager.add( + snapshot.session, + isSelected, + engineSessionState = state + ) + findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)) + }, + operation = { }, + anchorView = snackbarAnchorView + ) + } + } + override fun onDestroyView() { super.onDestroyView() _sessionControlInteractor = null @@ -888,6 +934,9 @@ class HomeFragment : Fragment() { } companion object { + const val ALL_NORMAL_TABS = "all_normal" + const val ALL_PRIVATE_TABS = "all_private" + private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar" private const val SESSION_TO_DELETE = "session_to_delete" private const val ANIMATION_DELAY = 100L diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt index d2383185d..3afd4c9fe 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -6,8 +6,8 @@ package org.mozilla.fenix.tabtray import androidx.annotation.VisibleForTesting import androidx.navigation.NavController +import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.prompt.ShareData import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -15,6 +15,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.sessionsOfType +import org.mozilla.fenix.home.HomeFragment /** * [TabTrayDialogFragment] controller. @@ -34,7 +35,7 @@ class DefaultTabTrayController( private val activity: HomeActivity, private val navController: NavController, private val dismissTabTray: () -> Unit, - private val showUndoSnackbar: (String, SessionManager.Snapshot) -> Unit, + private val dismissTabTrayAndNavigateHome: (String) -> Unit, private val registerCollectionStorageObserver: () -> Unit ) : TabTrayController { override fun onNewTabTapped(private: Boolean) { @@ -89,35 +90,15 @@ class DefaultTabTrayController( navController.navigate(directions) } + @OptIn(ExperimentalCoroutinesApi::class) override fun onCloseAllTabsClicked(private: Boolean) { - val sessionManager = activity.components.core.sessionManager - val tabs = getListOfSessions(private) - - val selectedIndex = sessionManager - .selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0 - - val snapshot = tabs - .map(sessionManager::createSessionSnapshot) - .map { - it.copy( - engineSession = null, - engineSessionState = it.engineSession?.saveState() - ) - } - .let { SessionManager.Snapshot(it, selectedIndex) } - - tabs.forEach { - sessionManager.remove(it) - } - - val snackbarMessage = if (private) { - activity.getString(R.string.snackbar_private_tabs_closed) + val sessionsToClose = if (private) { + HomeFragment.ALL_PRIVATE_TABS } else { - activity.getString(R.string.snackbar_tabs_closed) + HomeFragment.ALL_NORMAL_TABS } - showUndoSnackbar(snackbarMessage, snapshot) - dismissTabTray() + dismissTabTrayAndNavigateHome(sessionsToClose) } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt index c86dbe38a..c04818f50 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -22,10 +22,6 @@ import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.state.selector.normalTabs -import mozilla.components.browser.state.selector.privateTabs -import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases @@ -34,11 +30,11 @@ import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.utils.allowUndo @@ -82,13 +78,24 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { override fun invoke(sessionId: String) { requireContext().components.analytics.metrics.track(Event.ClosedExistingTab) showUndoSnackbarForTab(sessionId) - requireComponents.useCases.tabsUseCases.removeTab(sessionId) + removeIfNotLastTab(sessionId) } override fun invoke(session: Session) { requireContext().components.analytics.metrics.track(Event.ClosedExistingTab) showUndoSnackbarForTab(session.id) - requireComponents.useCases.tabsUseCases.removeTab(session) + removeIfNotLastTab(session.id) + } + } + + private fun removeIfNotLastTab(sessionId: String) { + // We only want to *immediately* remove a tab if there are more than one in the tab tray + // If there is only one, the HomeFragment handles deleting the tab (to better support snackbars) + val sessionManager = view?.context?.components?.core?.sessionManager + val sessionToRemove = sessionManager?.findSessionById(sessionId) + + if (sessionManager?.sessions?.filter { sessionToRemove?.private == it.private }?.size != 1) { + requireComponents.useCases.tabsUseCases.removeTab(sessionId) } } @@ -127,7 +134,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { activity = (activity as HomeActivity), navController = findNavController(), dismissTabTray = ::dismissAllowingStateLoss, - showUndoSnackbar = ::showUndoSnackbar, + dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome, registerCollectionStorageObserver = ::registerCollectionStorageObserver ) ), @@ -177,7 +184,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { consumeFrom(requireComponents.core.store) { tabTrayView.updateState(it) - navigateHomeIfNeeded(it) } } @@ -191,11 +197,20 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { private fun showUndoSnackbarForTab(sessionId: String) { val sessionManager = view?.context?.components?.core?.sessionManager + val snapshot = sessionManager ?.findSessionById(sessionId)?.let { sessionManager.createSessionSnapshot(it) } ?: return + // Check if this is the last tab of this session type + val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1 + + if (isLastOpenTab) { + dismissTabTrayAndNavigateHome(sessionId) + return + } + val state = snapshot.engineSession?.saveState() val isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false @@ -205,13 +220,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { getString(R.string.snackbar_tab_closed) } - // Check if this is the last tab of this session type - val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1 - val rootView = if (isLastOpenTab) { requireActivity().getRootView()!! } else { requireView().tabLayout } - val anchorView = if (isLastOpenTab) { null } else { snackbarAnchor } - - requireActivity().lifecycleScope.allowUndo( - rootView, + lifecycleScope.allowUndo( + requireView().tabLayout, snackbarMessage, getString(R.string.snackbar_deleted_undo), { @@ -220,18 +230,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { }, operation = { }, elevation = ELEVATION, - paddedForBottomToolbar = isLastOpenTab, - anchorView = anchorView + anchorView = snackbarAnchor ) - - dismissTabTrayIfNecessary() } - private fun dismissTabTrayIfNecessary() { - if (requireComponents.core.sessionManager.sessions.size == 1) { - findNavController().popBackStack(R.id.homeFragment, false) - dismissAllowingStateLoss() - } + private fun dismissTabTrayAndNavigateHome(sessionId: String) { + val directions = BrowserFragmentDirections.actionGlobalHome(sessionToDelete = sessionId) + findNavController().navigate(directions) + dismissAllowingStateLoss() } override fun onDestroyView() { @@ -247,39 +253,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { } } - private fun navigateHomeIfNeeded(state: BrowserState) { - val shouldPop = if (tabTrayView.isPrivateModeSelected) { - state.privateTabs.isEmpty() - } else { - state.normalTabs.isEmpty() - } - - if (shouldPop) { - findNavController().popBackStack(R.id.homeFragment, false) - } - } - private fun registerCollectionStorageObserver() { requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) } - private fun showUndoSnackbar(snackbarMessage: String, snapshot: SessionManager.Snapshot) { - // Warning: removing this definition and using it directly in the onCancel block will fail silently. - val sessionManager = view?.context?.components?.core?.sessionManager - - requireActivity().lifecycleScope.allowUndo( - requireActivity().getRootView()!!, - snackbarMessage, - getString(R.string.snackbar_deleted_undo), - { - sessionManager?.restore(snapshot) - }, - operation = { }, - elevation = ELEVATION, - paddedForBottomToolbar = true - ) - } - private fun showCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) { view.let { val messageStringRes = when { diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt index a352a925b..8c13acc74 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt @@ -35,6 +35,7 @@ class DefaultTabTrayControllerTest { private val navController: NavController = mockk() private val sessionManager: SessionManager = mockk(relaxed = true) private val dismissTabTray: (() -> Unit) = mockk(relaxed = true) + private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true) private val showUndoSnackbar: ((String, SessionManager.Snapshot) -> Unit) = mockk(relaxed = true) private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true) @@ -78,7 +79,7 @@ class DefaultTabTrayControllerTest { activity = activity, navController = navController, dismissTabTray = dismissTabTray, - showUndoSnackbar = showUndoSnackbar, + dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome, registerCollectionStorageObserver = registerCollectionStorageObserver ) } @@ -155,12 +156,9 @@ class DefaultTabTrayControllerTest { @Test fun onCloseAllTabsClicked() { controller.onCloseAllTabsClicked(private = false) - val snackbarMessage = activity.getString(R.string.snackbar_tabs_closed) verify { - sessionManager.createSessionSnapshot(nonPrivateSession) - sessionManager.remove(nonPrivateSession) - showUndoSnackbar(snackbarMessage, any()) + dismissTabTrayAndNavigateHome(any()) } } }