diff --git a/app/build.gradle b/app/build.gradle index 76cafe440..3f7df7ce7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -455,10 +455,12 @@ dependencies { implementation Deps.mozilla_concept_storage implementation Deps.mozilla_concept_sync implementation Deps.mozilla_concept_toolbar + implementation Deps.mozilla_concept_tabstray implementation Deps.mozilla_browser_awesomebar implementation Deps.mozilla_feature_downloads implementation Deps.mozilla_browser_domains + implementation Deps.mozilla_browser_tabstray implementation Deps.mozilla_browser_icons implementation Deps.mozilla_browser_menu implementation Deps.mozilla_browser_search diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index a0538c43d..768085dd9 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -17,6 +17,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) { FromGlobal(0), FromHome(R.id.homeFragment), FromSearch(R.id.searchFragment), + FromTabTray(R.id.tabTrayFragment), FromSettings(R.id.settingsFragment), FromBookmarks(R.id.bookmarkFragment), FromHistory(R.id.historyFragment), diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 30c897e58..feebc1d93 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -22,6 +22,7 @@ import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI +import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -73,6 +74,10 @@ import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.RunWhenReadyQueue +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.browser.tabstray.TabsAdapter +import mozilla.components.browser.tabstray.BrowserTabsTray +import org.mozilla.fenix.tabtray.TabTrayFragmentDirections /** * The main activity of the application. The application is primarily a single Activity (this one) @@ -211,6 +216,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { share(it) } }.asView() + TabsTray::class.java.name -> { + val layout = LinearLayoutManager(context) + val adapter = TabsAdapter(layoutId = R.layout.tab_tray_item) + BrowserTabsTray(context, attrs, tabsAdapter = adapter, layout = layout) + } else -> super.onCreateView(parent, name, context, attrs) } @@ -333,6 +343,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { HomeFragmentDirections.actionHomeFragmentToBrowserFragment(customTabSessionId, true) BrowserDirection.FromSearch -> SearchFragmentDirections.actionGlobalBrowser(customTabSessionId) + BrowserDirection.FromTabTray -> + TabTrayFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromSettings -> SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromBookmarks -> diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragment.kt new file mode 100644 index 000000000..587ef3c41 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragment.kt @@ -0,0 +1,227 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.tabtray + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.view.isVisible +import mozilla.components.concept.engine.prompt.ShareData +import androidx.fragment.app.Fragment +import mozilla.components.feature.tabs.tabstray.TabsFeature +import kotlinx.android.synthetic.main.fragment_tab_tray.tabsTray +import kotlinx.android.synthetic.main.fragment_tab_tray.view.* +import mozilla.components.support.base.feature.UserInteractionHandler +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.requireComponents +import androidx.navigation.fragment.findNavController +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import mozilla.components.concept.tabstray.Tab +import mozilla.components.concept.tabstray.TabsTray +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.collections.SaveCollectionStep +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.ext.sessionsOfType +import org.mozilla.fenix.ext.showToolbar + +@SuppressWarnings("TooManyFunctions", "LargeClass") +class TabTrayFragment : Fragment(R.layout.fragment_tab_tray), TabsTray.Observer, UserInteractionHandler { + private var tabsFeature: TabsFeature? = null + var tabTrayMenu: Menu? = null + + private val sessionManager: SessionManager + get() = requireComponents.core.sessionManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + showToolbar(getString(R.string.tab_tray_title)) + onTabsChanged() + + sessionManager.register(observer = object : SessionManager.Observer { + override fun onSessionAdded(session: Session) { + onTabsChanged() + } + + override fun onSessionRemoved(session: Session) { + onTabsChanged() + } + + override fun onSessionsRestored() { + onTabsChanged() + } + + override fun onAllSessionsRemoved() { + onTabsChanged() + } + }, owner = viewLifecycleOwner) + + tabsFeature = TabsFeature( + tabsTray, + requireComponents.core.store, + requireComponents.useCases.tabsUseCases, + { it.content.private == (activity as HomeActivity?)?.browsingModeManager?.mode?.isPrivate }, + ::closeTabsTray) + + view.tab_tray_open_new_tab.setOnClickListener { + val directions = TabTrayFragmentDirections.actionGlobalSearch(null) + findNavController().navigate(directions) + } + + view.tab_tray_go_home.setOnClickListener { + val directions = TabTrayFragmentDirections.actionGlobalHome() + findNavController().navigate(directions) + } + + view.private_browsing_button.setOnClickListener { + val newMode = !(activity as HomeActivity).browsingModeManager.mode.isPrivate + val invertedMode = BrowsingMode.fromBoolean(newMode) + (activity as HomeActivity).browsingModeManager.mode = invertedMode + tabsFeature?.filterTabs { tabSessionState -> + tabSessionState.content.private == newMode + } + } + + view.save_to_collection_button.setOnClickListener { + saveToCollection() + } + } + + override fun onResume() { + super.onResume() + + onTabsChanged() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.tab_tray_menu, menu) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + this.tabTrayMenu = menu + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.tab_tray_select_to_save_menu_item -> { + saveToCollection() + true + } + R.id.tab_tray_share_menu_item -> { + share(getListOfSessions().toList()) + true + } + R.id.tab_tray_close_menu_item -> { + val tabs = getListOfSessions() + tabs.forEach { + sessionManager.remove(it) + } + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun saveToCollection() { + val tabs = getListOfSessions() + val tabIds = tabs.map { it.id }.toList().toTypedArray() + val tabCollectionStorage = (activity as HomeActivity).components.core.tabCollectionStorage + + val step = when { + // 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 + } + + val directions = TabTrayFragmentDirections.actionTabTrayFragmentToCreateCollectionFragment( + tabIds = tabIds, + previousFragmentId = R.id.tabTrayFragment, + saveCollectionStep = step, + selectedTabIds = tabIds, + selectedTabCollectionId = -1 + ) + + view?.let { + findNavController().navigate(directions) + } + } + + override fun onStart() { + super.onStart() + + tabsFeature?.start() + tabsTray.register(this) + } + + override fun onStop() { + super.onStop() + + tabsFeature?.stop() + tabsTray.unregister(this) + } + + override fun onBackPressed(): Boolean { + if (getListOfSessions().isEmpty()) { + findNavController().popBackStack(R.id.homeFragment, false) + return true + } + + return false + } + + private fun closeTabsTray() { + activity?.supportFragmentManager?.beginTransaction()?.apply { + commit() + } + } + + override fun onTabClosed(tab: Tab) { + // noop + } + + override fun onTabSelected(tab: Tab) { + (activity as HomeActivity).openToBrowser(BrowserDirection.FromTabTray) + } + + private fun getListOfSessions(): List { + val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate + return sessionManager.sessionsOfType(private = isPrivate) + .toList() + } + + private fun share(tabs: List) { + val data = tabs.map { + ShareData(url = it.url, title = it.title) + } + val directions = TabTrayFragmentDirections.actionGlobalShareFragment( + data = data.toTypedArray() + ) + nav(R.id.tabTrayFragment, directions) + } + + private fun onTabsChanged() { + val hasNoTabs = getListOfSessions().toList().isEmpty() + + view?.tab_tray_empty_view?.isVisible = !hasNoTabs + view?.save_to_collection_button?.isVisible = !hasNoTabs + + if (hasNoTabs) { + view?.announceForAccessibility(view?.context?.getString(R.string.no_open_tabs_description)) + } + } +} diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 000000000..57149e629 --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tab_tray.xml b/app/src/main/res/layout/fragment_tab_tray.xml new file mode 100644 index 000000000..775101b36 --- /dev/null +++ b/app/src/main/res/layout/fragment_tab_tray.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/tab_tray_item.xml b/app/src/main/res/layout/tab_tray_item.xml new file mode 100644 index 000000000..6fc480664 --- /dev/null +++ b/app/src/main/res/layout/tab_tray_item.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/tab_tray_menu.xml b/app/src/main/res/menu/tab_tray_menu.xml new file mode 100644 index 000000000..8eda6eea4 --- /dev/null +++ b/app/src/main/res/menu/tab_tray_menu.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 7d80d0e96..7384d02d9 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -48,6 +48,17 @@ + + + + + + @color/sync_disconnected_background_dark_theme @color/swipe_delete_background_dark_theme + + @color/tab_tray_item_text_dark_theme + @color/tab_tray_item_selected_text_dark_theme + @color/tab_tray_item_background_dark_theme + @color/tab_tray_item_selected_background_dark_theme + @color/collection_icon_color_violet_dark_theme @color/collection_icon_color_blue_dark_theme diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index d95a0fce3..26e85da9c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -44,6 +44,12 @@ + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 992406453..6b2dac4e5 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -38,6 +38,12 @@ #FFFDE2 #E0E0E6 + + @color/primary_text_light_theme + @color/foundation_light_theme + @color/foundation_light_theme + @color/accent_bright_light_theme + #FBFBFE #A7A2B7 @@ -71,6 +77,12 @@ #5B5846 #4A4A55 + + @color/primary_text_dark_theme + @color/primary_text_dark_theme + @color/foundation_dark_theme + @color/accent_bright_dark_theme + #FBFBFE #A7A2B7 @@ -102,6 +114,12 @@ #5B5846 #312A65 + + @color/primary_text_private_theme + @color/primary_text_private_theme + @color/foundation_private_theme + @color/accent_bright_private_theme + @color/primary_text_light_theme @color/secondary_text_light_theme @@ -132,6 +150,12 @@ @color/sync_disconnected_background_light_theme @color/swipe_delete_background_light_theme + + @color/tab_tray_item_text_light_theme + @color/tab_tray_item_selected_text_light_theme + @color/tab_tray_item_background_light_theme + @color/tab_tray_item_selected_background_light_theme + #DFDFE3 diff --git a/app/src/main/res/values/static_strings.xml b/app/src/main/res/values/static_strings.xml index e423d521d..845b84524 100644 --- a/app/src/main/res/values/static_strings.xml +++ b/app/src/main/res/values/static_strings.xml @@ -27,4 +27,20 @@ YouTube AS + + + + Open Tabs + + Save to collection + + Share all tabs + + Close all tabs + + New tab + + Go home + + Toggle tab mode diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index eb0ecb1f6..7169b0aa4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -66,6 +66,11 @@ @color/sync_disconnected_background_normal_theme @color/swipe_delete_background_normal_theme + @color/tab_tray_item_text_normal_theme + @color/tab_tray_item_selected_text_normal_theme + @color/foundation_normal_theme + @color/tab_tray_item_selected_background_normal_theme + @drawable/ic_logo_wordmark_normal @color/foundation_normal_theme @@ -170,6 +175,12 @@ @color/sync_disconnected_background_private_theme @color/swipe_delete_background_private_theme + + @color/tab_tray_item_text_private_theme + @color/tab_tray_item_selected_text_private_theme + @color/foundation_private_theme + @color/tab_tray_item_selected_background_private_theme + @drawable/ic_logo_wordmark_private @drawable/private_home_background_gradient