Unverified Commit 6f5f48c3 authored by David Walsh's avatar David Walsh Committed by GitHub
Browse files

For #10148 - Add basic tab tray implementation without exposing to rest of app (#9934)

parent e51e1c87
......@@ -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
......
......@@ -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),
......
......@@ -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 ->
......
/* 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<Session> {
val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate
return sessionManager.sessionsOfType(private = isPrivate)
.toList()
}
private fun share(tabs: List<Session>) {
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))
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21.5 11.1l-8.8-8a1 1 0 0 0-1.4 0l-8.8 8c-0.3 0.4-0.3 1 0 1.3 0.4 0.4 1 0.4 1.5 0l1-1v7c0 1.5 1.3 2.8 3 2.8h8c1.7 0 3-1.3 3-2.8v-7l1 1c0.4 0.4 1 0.4 1.4 0 0.4-0.3 0.4-1 0-1.3zM17 18.4c0 0.5-0.4 1-1 1H8c-0.6 0-1-0.5-1-1V9.6L12 5l5 4.6z"
android:fillColor="?primaryText"/>
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:mozac="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:text="@string/no_open_tabs_description"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<mozilla.components.concept.tabstray.TabsTray
android:id="@+id/tabsTray"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/save_to_collection_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
mozac:tabsTrayItemBackgroundColor="?tabTrayItemBackground"
mozac:tabsTrayItemTextColor="?tabTrayItemText"
mozac:tabsTraySelectedItemBackgroundColor="?tabTrayItemSelectedBackground"
mozac:tabsTraySelectedItemTextColor="?tabTrayItemSelectedText" />
<include
layout="@layout/save_to_collection_button"
android:id="@+id/save_to_collection_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintBottom_toTopOf="@id/bottomBarShadow"
app:layout_constraintTop_toBottomOf="@+id/tabsTray" />
<View
android:id="@+id/bottomBarShadow"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/bottom_bar_shadow"
app:layout_constraintBottom_toTopOf="@id/tab_tray_controls"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/tab_tray_controls"
android:layout_width="0dp"
android:layout_height="48dp"
android:background="?bottomBarBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageButton
android:id="@+id/private_browsing_button"
android:layout_width="@dimen/glyph_button_height"
android:layout_height="@dimen/glyph_button_height"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/content_description_private_browsing_button"
app:srcCompat="@drawable/private_browsing_button"
app:layout_constraintTop_toTopOf="@id/tab_tray_controls"
app:layout_constraintBottom_toBottomOf="@+id/tab_tray_controls"
app:layout_constraintEnd_toStartOf="@+id/tab_tray_go_home"
app:layout_constraintStart_toStartOf="@id/tab_tray_controls" />
<ImageButton
android:id="@+id/tab_tray_go_home"
android:layout_width="@dimen/glyph_button_height"
android:layout_height="@dimen/glyph_button_height"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_menu_home"
app:srcCompat="@drawable/ic_home"
app:layout_constraintTop_toTopOf="@id/tab_tray_controls"
app:layout_constraintBottom_toBottomOf="@+id/tab_tray_controls"
app:layout_constraintEnd_toStartOf="@+id/tab_tray_open_new_tab"
app:layout_constraintStart_toEndOf="@+id/private_browsing_button" />
<ImageButton
android:id="@+id/tab_tray_open_new_tab"
android:layout_width="@dimen/glyph_button_height"
android:layout_height="@dimen/glyph_button_height"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_menu_open_new_tab"
app:srcCompat="@drawable/ic_new"
app:layout_constraintTop_toTopOf="@id/tab_tray_controls"
app:layout_constraintBottom_toBottomOf="@+id/tab_tray_controls"
app:layout_constraintEnd_toEndOf="@+id/tab_tray_controls"
app:layout_constraintStart_toEndOf="@+id/tab_tray_go_home" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="72dp">
<androidx.cardview.widget.CardView
android:id="@+id/mozac_browser_tabstray_card"
android:layout_width="100dp"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:cardBackgroundColor="@color/photonWhite">
<ImageView
android:id="@+id/mozac_browser_tabstray_icon"
android:layout_width="50dp"
android:layout_height="28dp"
android:layout_marginTop="14dp"
android:layout_marginStart="25dp"
android:importantForAccessibility="no" />
<mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
android:id="@+id/mozac_browser_tabstray_thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/mozac_browser_tabstray_open_tab" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/mozac_browser_tabstray_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:ellipsize="end"
android:lines="1"
android:textSize="16sp"
android:paddingTop="16dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toStartOf="@id/mozac_browser_tabstray_close"
app:layout_constraintStart_toEndOf="@id/mozac_browser_tabstray_card"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/mozac_browser_tabstray_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_alignParentTop="true"
android:ellipsize="end"
android:lines="1"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textColor="@color/tab_tray_item_selected_text_normal_theme"
android:alpha="0.50"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toStartOf="@id/mozac_browser_tabstray_close"
app:layout_constraintStart_toEndOf="@id/mozac_browser_tabstray_card"
app:layout_constraintTop_toBottomOf="@id/mozac_browser_tabstray_title" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/mozac_browser_tabstray_close"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/close_tab"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:srcCompat="@drawable/mozac_ic_close" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Save to collection -->
<item
android:id="@+id/tab_tray_select_to_save_menu_item"
android:title="@string/tab_tray_menu_item_save"
app:showAsAction="never" />
<!-- Share-->
<item
android:id="@+id/tab_tray_share_menu_item"
android:title="@string/tab_tray_menu_item_share"
app:showAsAction="never" />
<!-- Close tab-->
<item
android:id="@+id/tab_tray_close_menu_item"
android:title="@string/tab_tray_menu_item_close"
app:showAsAction="never" />
</menu>
\ No newline at end of file
......@@ -48,6 +48,17 @@
<action android:id="@+id/action_global_accountSettingsFragment" app:destination="@id/accountSettingsFragment" />
<action android:id="@+id/action_global_trackingProtectionPanelDialogFragment" app:destination="@id/trackingProtectionPanelDialogFragment" />
<action android:id="@+id/action_global_quickSettingsSheetDialogFragment" app:destination="@id/quickSettingsSheetDialogFragment"/>
<action android:id="@+id/action_global_tabTrayFragment" app:destination="@id/tabTrayFragment"/>
<fragment
android:id="@+id/tabTrayFragment"
android:name="org.mozilla.fenix.tabtray.TabTrayFragment">
<action
android:id="@+id/action_tabTrayFragment_to_createCollectionFragment"
app:destination="@+id/collectionCreationFragment"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in" />
</fragment>
<fragment
android:id="@+id/homeFragment"
......@@ -163,6 +174,9 @@
<action
android:id="@+id/action_browserFragment_to_trackingProtectionPanelDialogFragment"
app:destination="@id/trackingProtectionPanelDialogFragment" />
<action
android:id="@+id/action_browserFragment_to_tabsTrayFragment"
app:destination="@+id/tabTrayFragment" />
</fragment>
<fragment
......
......@@ -36,6 +36,12 @@
<color name="sync_disconnected_background_normal_theme">@color/sync_disconnected_background_dark_theme</color>
<color name="swipe_delete_background_normal_theme">@color/swipe_delete_background_dark_theme</color>
<!-- Tab tray -->
<color name="tab_tray_item_text_normal_theme">@color/tab_tray_item_text_dark_theme</color>
<color name="tab_tray_item_selected_text_normal_theme">@color/tab_tray_item_selected_text_dark_theme</color>
<color name="tab_tray_item_background_normal_theme">@color/tab_tray_item_background_dark_theme</color>
<color name="tab_tray_item_selected_background_normal_theme">@color/tab_tray_item_selected_background_dark_theme</color>
<!-- Collection icons-->
<color name="collection_icon_color_violet">@color/collection_icon_color_violet_dark_theme</color>
<color name="collection_icon_color_blue">@color/collection_icon_color_blue_dark_theme</color>
......
......@@ -44,6 +44,12 @@
<attr name="syncDisconnectedBackground" format="reference" />
<attr name="swipeDeleteBackground" format="reference" />
<!-- Tab tray -->
<attr name="tabTrayItemText" format="reference"/>
<attr name="tabTrayItemSelectedText" format="reference" />
<attr name="tabTrayItemBackground" format="reference" />
<attr name="tabTrayItemSelectedBackground" format="reference" />
<declare-styleable name="LibraryListItem">
<attr name="listItemTitle" format="reference" />
<attr name="listItemIcon" format="reference" />
......
......@@ -38,6 +38,12 @@
<color name="sync_disconnected_background_light_theme">#FFFDE2</color>
<color name="swipe_delete_background_light_theme">#E0E0E6</color>
<!-- Tab Tray -->
<color name="tab_tray_item_text_light_theme">@color/primary_text_light_theme</color>
<color name="tab_tray_item_selected_text_light_theme">@color/foundation_light_theme</color>
<color name="tab_tray_item_background_light_theme">@color/foundation_light_theme</color>
<color name="tab_tray_item_selected_background_light_theme">@color/accent_bright_light_theme</color>
<!-- Dark theme color palette -->
<color name="primary_text_dark_theme">#FBFBFE</color>
<color name="secondary_text_dark_theme">#A7A2B7</color>
......@@ -71,6 +77,12 @@
<color name="sync_disconnected_background_dark_theme">#5B5846</color>
<color name="swipe_delete_background_dark_theme">#4A4A55</color>
<!-- Tab Tray -->
<color name="tab_tray_item_text_dark_theme">@color/primary_text_dark_theme</color>
<color name="tab_tray_item_selected_text_dark_theme">@color/primary_text_dark_theme</color>
<color name="tab_tray_item_background_dark_theme">@color/foundation_dark_theme</color>
<color name="tab_tray_item_selected_background_dark_theme">@color/accent_bright_dark_theme</color>
<!-- Private theme color palette -->
<color name="primary_text_private_theme">#FBFBFE</color>
<color name="secondary_text_private_theme">#A7A2B7</color>
......@@ -102,6 +114,12 @@
<color name="sync_disconnected_background_private_theme">#5B5846</color>
<color name="swipe_delete_background_private_theme">#312A65</color>