diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index bad6f7cd46..06e44ec7c9 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -22,6 +22,7 @@ import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.view.* +import kotlinx.android.synthetic.main.tab_header.view.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job @@ -71,6 +72,7 @@ import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.readermode.DefaultReaderModeController +import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.StoreProvider @@ -96,6 +98,7 @@ import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.SharedViewModel import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.theme.ThemeManager +import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration import java.lang.ref.WeakReference @@ -117,6 +120,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session protected val browserToolbarView: BrowserToolbarView get() = _browserToolbarView!! + private val sessionManager: SessionManager + get() = requireComponents.core.sessionManager + protected val readerViewFeature = ViewBoundFeatureWrapper() private val sessionFeature = ViewBoundFeatureWrapper() @@ -225,6 +231,75 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session tabTrayDialog.dismiss() findNavController().navigate(BrowserFragmentDirections.actionGlobalHome()) } + + override fun onShareTabsClicked(private: Boolean) { + share(getListOfSessions(private)) + } + + override fun onCloseAllTabsClicked(private: Boolean) { + 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 isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate + val snackbarMessage = if (isPrivate) { + getString(R.string.snackbar_private_tabs_closed) + } else { + getString(R.string.snackbar_tabs_closed) + } + + viewLifecycleOwner.lifecycleScope.allowUndo( + requireView(), + snackbarMessage, + getString(R.string.snackbar_deleted_undo), + { + sessionManager.restore(snapshot) + }, + operation = { }, + anchorView = view.tabs_header + ) + } + + override fun onSaveToCollectionClicked() { + val tabs = getListOfSessions(false) + val tabIds = tabs.map { it.id }.toList().toTypedArray() + val tabCollectionStorage = (activity as HomeActivity).components.core.tabCollectionStorage + val navController = findNavController() + + val step = when { + // Show the SelectTabs fragment if there are multiple opened tabs to select which tabs + // you want to save to a collection. + tabs.size > 1 -> SaveCollectionStep.SelectTabs + // If there is an existing tab collection, show the SelectCollection fragment to save + // the selected tab to a collection of your choice. + tabCollectionStorage.cachedTabCollections.isNotEmpty() -> + SaveCollectionStep.SelectCollection + // Show the NameCollection fragment to create a new collection for the selected tab. + else -> SaveCollectionStep.NameCollection + } + + if (navController.currentDestination?.id == R.id.collectionCreationFragment) return + + val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment( + tabIds = tabIds, + previousFragmentId = R.id.tabTrayFragment, + saveCollectionStep = step, + selectedTabIds = tabIds + ) + navController.nav(R.id.browserFragment, directions) + } } } ) @@ -965,6 +1040,23 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session } } + private fun share(tabs: List) { + val data = tabs.map { + ShareData(url = it.url, title = it.title) + } + val directions = BrowserFragmentDirections.actionGlobalShareFragment( + data = data.toTypedArray() + ) + nav(R.id.browserFragment, directions) + } + + private fun getListOfSessions( + private: Boolean = (activity as HomeActivity).browsingModeManager.mode.isPrivate + ): List { + return requireComponents.core.sessionManager.sessionsOfType(private = private) + .toList() + } + /* * Dereference these views when the fragment view is destroyed to prevent memory leaks */ 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 24bc20d17b..cb38140c3d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -45,6 +45,7 @@ import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.view.* +import kotlinx.android.synthetic.main.tab_header.view.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -59,6 +60,7 @@ import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.state.MediaState.State.PLAYING import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount @@ -75,6 +77,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.addons.runIfFragmentIsAttached import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.StoreProvider @@ -371,6 +374,72 @@ class HomeFragment : Fragment() { (activity as HomeActivity).browsingModeManager.mode = BrowsingMode.fromBoolean(private) tabTrayDialog.dismiss() } + + override fun onShareTabsClicked(private: Boolean) { + share(getListOfSessions(private)) + } + + override fun onCloseAllTabsClicked(private: Boolean) { + 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 isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate + val snackbarMessage = if (isPrivate) { + getString(R.string.snackbar_private_tabs_closed) + } else { + getString(R.string.snackbar_tabs_closed) + } + + viewLifecycleOwner.lifecycleScope.allowUndo( + requireView(), + snackbarMessage, + getString(R.string.snackbar_deleted_undo), + { + sessionManager.restore(snapshot) + }, + operation = { }, + anchorView = view.tabs_header + ) + } + + override fun onSaveToCollectionClicked() { + val tabs = getListOfSessions(false) + val tabIds = tabs.map { it.id }.toList().toTypedArray() + val tabCollectionStorage = (activity as HomeActivity).components.core.tabCollectionStorage + val navController = findNavController() + + val step = when { + // Show the SelectTabs fragment if there are multiple opened tabs to select which tabs + // you want to save to a collection. + tabs.size > 1 -> SaveCollectionStep.SelectTabs + // If there is an existing tab collection, show the SelectCollection fragment to save + // the selected tab to a collection of your choice. + tabCollectionStorage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection + // Show the NameCollection fragment to create a new collection for the selected tab. + else -> SaveCollectionStep.NameCollection + } + + if (navController.currentDestination?.id == R.id.collectionCreationFragment) return + + val directions = HomeFragmentDirections.actionHomeFragmentToCreateCollectionFragment( + tabIds = tabIds, + previousFragmentId = R.id.tabTrayFragment, + saveCollectionStep = step, + selectedTabIds = tabIds + ) + navController.nav(R.id.homeFragment, directions) + } } } @@ -846,8 +915,8 @@ class HomeFragment : Fragment() { } } - private fun getListOfSessions(): List { - return sessionManager.sessionsOfType(private = browsingModeManager.mode.isPrivate) + private fun getListOfSessions(private: Boolean = browsingModeManager.mode.isPrivate): List { + return sessionManager.sessionsOfType(private = private) .filter { session: Session -> session.id != pendingSessionDeletion?.sessionId } .toList() } @@ -1022,6 +1091,16 @@ class HomeFragment : Fragment() { } } + private fun share(tabs: List) { + val data = tabs.map { + ShareData(url = it.url, title = it.title) + } + val directions = HomeFragmentDirections.actionGlobalShareFragment( + data = data.toTypedArray() + ) + nav(R.id.homeFragment, directions) + } + companion object { private const val ANIMATION_DELAY = 100L 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 c6a649fdcc..7a0a93479b 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -15,6 +15,7 @@ import kotlinx.android.synthetic.main.component_tabstray.view.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.* import mozilla.components.concept.tabstray.Tab +import mozilla.components.lib.state.ext.consumeFrom import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.ext.components @@ -25,6 +26,9 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { interface Interactor { fun onTabSelected(tab: Tab) fun onNewTabTapped(private: Boolean) + fun onShareTabsClicked(private: Boolean) + fun onSaveToCollectionClicked() + fun onCloseAllTabsClicked(private: Boolean) } private lateinit var tabTrayView: TabTrayView @@ -49,7 +53,9 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { (activity as HomeActivity).browsingModeManager.mode.isPrivate ) - tabLayout.setOnClickListener { dismissAllowingStateLoss() } + tabLayout.setOnClickListener { + dismissAllowingStateLoss() + } view.tabLayout.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( @@ -64,6 +70,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { insets } + + consumeFrom(requireComponents.core.store) { tabTrayView.updateState(it) } } override fun onTabClosed(tab: Tab) { @@ -108,6 +116,18 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { dismissAllowingStateLoss() } + override fun onShareTabsClicked(private: Boolean) { + interactor?.onShareTabsClicked(private) + } + + override fun onSaveToCollectionClicked() { + interactor?.onSaveToCollectionClicked() + } + + override fun onCloseAllTabsClicked(private: Boolean) { + interactor?.onCloseAllTabsClicked(private) + } + companion object { private const val ELEVATION = 80f } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index 61f65b2c03..0edb22cc5e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -4,15 +4,22 @@ package org.mozilla.fenix.tabtray +import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.tabs.TabLayout import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.component_tabstray.* import kotlinx.android.synthetic.main.component_tabstray.view.* import kotlinx.android.synthetic.main.component_tabstray_fab.view.* +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +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.browser.tabstray.BrowserTabsTray import mozilla.components.concept.tabstray.Tab @@ -26,6 +33,9 @@ interface TabTrayInteractor { fun onTabSelected(tab: Tab) fun onNewTabTapped(private: Boolean) fun onTabTrayDismissed() + fun onShareTabsClicked(private: Boolean) + fun onSaveToCollectionClicked() + fun onCloseAllTabsClicked(private: Boolean) } /** * View that contains and configures the BrowserAwesomeBar @@ -41,8 +51,11 @@ class TabTrayView( val view = LayoutInflater.from(container.context) .inflate(R.layout.component_tabstray, container, true) + val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID + private val behavior = BottomSheetBehavior.from(view.tab_wrapper) private var tabsFeature: TabsFeature + private var tabTrayItemMenu: TabTrayItemMenu override val containerView: View? get() = container @@ -89,8 +102,26 @@ class TabTrayView( TabsTouchHelper(tray.tabsAdapter).attachToRecyclerView(tray) } + tabTrayItemMenu = TabTrayItemMenu(view.context, { view.tab_layout.selectedTabPosition == 0 }) { + when (it) { + is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked( + isPrivateModeSelected + ) + is TabTrayItemMenu.Item.SaveToCollection -> interactor.onSaveToCollectionClicked() + is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked( + isPrivateModeSelected + ) + } + } + + view.tab_tray_overflow.setOnClickListener { + tabTrayItemMenu.menuBuilder + .build(view.context) + .show(anchor = it) + } + fabView.new_tab_button.setOnClickListener { - interactor.onNewTabTapped(view.tab_layout.selectedTabPosition == 1) + interactor.onNewTabTapped(isPrivateModeSelected) } tabsTray.register(this) @@ -109,6 +140,17 @@ class TabTrayView( } tabsFeature.filterTabs(filter) + + updateState(view.context.components.core.store.state) + } + + fun updateState(state: BrowserState) { + val shouldHide = if (isPrivateModeSelected) { + state.privateTabs.isEmpty() + } else { + state.normalTabs.isEmpty() + } + view?.tab_tray_overflow?.isVisible = !shouldHide } override fun onTabClosed(tab: Tab) { @@ -124,3 +166,43 @@ class TabTrayView( private const val ELEVATION = 90f } } + +class TabTrayItemMenu( + private val context: Context, + private val shouldShowSaveToCollection: () -> Boolean, + private val onItemTapped: (Item) -> Unit = {} +) { + + sealed class Item { + object ShareAllTabs : Item() + object SaveToCollection : Item() + object CloseAllTabs : Item() + } + + val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } + + private val menuItems by lazy { + listOf( + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_item_save), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.SaveToCollection) + }.apply { visible = shouldShowSaveToCollection }, + + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_item_share), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.ShareAllTabs) + }, + + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_item_close), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.CloseAllTabs) + } + ) + } +} diff --git a/app/src/main/res/layout/component_tabstray.xml b/app/src/main/res/layout/component_tabstray.xml index 80fc324cec..8b360399fd 100644 --- a/app/src/main/res/layout/component_tabstray.xml +++ b/app/src/main/res/layout/component_tabstray.xml @@ -58,11 +58,11 @@ android:background="?android:attr/selectableItemBackgroundBorderless" android:contentDescription="@string/open_tabs_menu" app:srcCompat="@drawable/ic_menu" - android:layout_marginEnd="8dp" - android:visibility="gone" + android:layout_marginEnd="0dp" + android:visibility="visible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/tab_layout" - app:layout_constraintBottom_toBottomOf="@id/tab_layout"/> + app:layout_constraintBottom_toBottomOf="@id/tab_layout" /> +