For #19916 - Add last viewed tab to home screen

Co-authored-by: Jonathan Almeida <jalmeida@mozilla.com>
upstream-sync
Gabriel Luong 3 years ago
parent 42d87d1a7a
commit 9d3cf79051

@ -33,4 +33,9 @@ object FeatureFlags {
* Enables the Home button in the browser toolbar to navigate back to the home screen.
*/
val showHomeButtonFeature = Config.channel.isNightlyOrDebug
/**
* Enables the "recent" tabs feature in the home screen.
*/
val showRecentTabsFeature = Config.channel.isNightlyOrDebug
}

@ -0,0 +1,24 @@
/* 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.ext
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
/**
* Returns the currently selected tab if there's one as a list.
*
* @return A list of the currently selected tab or an empty list.
*/
fun BrowserState.asRecentTabs(): List<TabSessionState> {
val tab = selectedTab
return if (tab != null && !tab.content.private) {
listOfNotNull(tab)
} else {
emptyList()
}
}

@ -85,6 +85,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -101,6 +102,7 @@ import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider
import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
@ -111,7 +113,9 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
import org.mozilla.fenix.home.recenttabs.controller.DefaultRecentTabsController
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.RecentTabsListFeature
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
@ -171,6 +175,7 @@ class HomeFragment : Fragment() {
private lateinit var currentMode: CurrentMode
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()
@VisibleForTesting
internal var getMenuButton: () -> MenuButton? = { menuButton }
@ -228,7 +233,8 @@ class HomeFragment : Fragment() {
).getTip()
},
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome,
showSetAsDefaultBrowserCard = components.settings.shouldShowSetAsDefaultBrowserCard()
showSetAsDefaultBrowserCard = components.settings.shouldShowSetAsDefaultBrowserCard(),
recentTabs = components.core.store.state.asRecentTabs()
)
)
}
@ -243,8 +249,19 @@ class HomeFragment : Fragment() {
view = view
)
if (FeatureFlags.showRecentTabsFeature) {
recentTabsListFeature.set(
feature = RecentTabsListFeature(
browserStore = components.core.store,
homeStore = homeFragmentStore
),
owner = viewLifecycleOwner,
view = view
)
}
_sessionControlInteractor = SessionControlInteractor(
DefaultSessionControlController(
controller = DefaultSessionControlController(
activity = activity,
settings = components.settings,
engine = components.core.engine,
@ -263,6 +280,10 @@ class HomeFragment : Fragment() {
showDeleteCollectionPrompt = ::showDeleteCollectionPrompt,
showTabTray = ::openTabsTray,
handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel
),
recentTabController = DefaultRecentTabsController(
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
navController = findNavController()
)
)
@ -577,7 +598,8 @@ class HomeFragment : Fragment() {
)
).getTip()
},
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome,
recentTabs = components.core.store.state.asRecentTabs()
)
)

@ -5,6 +5,7 @@
package org.mozilla.fenix.home
import android.graphics.Bitmap
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.lib.state.Action
@ -16,7 +17,7 @@ import org.mozilla.fenix.components.tips.Tip
* The [Store] for holding the [HomeFragmentState] and applying [HomeFragmentAction]s.
*/
class HomeFragmentStore(
initialState: HomeFragmentState
initialState: HomeFragmentState = HomeFragmentState()
) : Store<HomeFragmentState, HomeFragmentAction>(
initialState, ::homeFragmentStateReducer
)
@ -41,15 +42,17 @@ data class Tab(
* @property topSites The list of [TopSite] in the [HomeFragment].
* @property tip The current [Tip] to show on the [HomeFragment].
* @property showCollectionPlaceholder If true, shows a placeholder when there are no collections.
* @property recentTabs The list of recent [TabSessionState] in the [HomeFragment].
*/
data class HomeFragmentState(
val collections: List<TabCollection>,
val expandedCollections: Set<Long>,
val mode: Mode,
val topSites: List<TopSite>,
val collections: List<TabCollection> = emptyList(),
val expandedCollections: Set<Long> = emptySet(),
val mode: Mode = Mode.Normal,
val topSites: List<TopSite> = emptyList(),
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean,
val showSetAsDefaultBrowserCard: Boolean
val showCollectionPlaceholder: Boolean = false,
val showSetAsDefaultBrowserCard: Boolean = false,
val recentTabs: List<TabSessionState> = emptyList()
) : State
sealed class HomeFragmentAction : Action {
@ -58,7 +61,8 @@ sealed class HomeFragmentAction : Action {
val mode: Mode,
val collections: List<TabCollection>,
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean
val showCollectionPlaceholder: Boolean,
val recentTabs: List<TabSessionState>
) :
HomeFragmentAction()
@ -69,6 +73,7 @@ sealed class HomeFragmentAction : Action {
data class ModeChange(val mode: Mode) : HomeFragmentAction()
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction()
data class RecentTabsChange(val recentTabs: List<TabSessionState>) : HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction()
object RemoveSetDefaultBrowserCard : HomeFragmentAction()
}
@ -82,7 +87,8 @@ private fun homeFragmentStateReducer(
collections = action.collections,
mode = action.mode,
topSites = action.topSites,
tip = action.tip
tip = action.tip,
recentTabs = action.recentTabs
)
is HomeFragmentAction.CollectionExpanded -> {
val newExpandedCollection = state.expandedCollections.toMutableSet()
@ -105,5 +111,6 @@ private fun homeFragmentStateReducer(
state.copy(showCollectionPlaceholder = false)
}
is HomeFragmentAction.RemoveSetDefaultBrowserCard -> state.copy(showSetAsDefaultBrowserCard = false)
is HomeFragmentAction.RecentTabsChange -> state.copy(recentTabs = action.recentTabs)
}
}

@ -0,0 +1,53 @@
/* 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.home.recenttabs.controller
import androidx.navigation.NavController
import mozilla.components.feature.tabs.TabsUseCases.SelectTabUseCase
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
/**
* An interface that handles the view manipulation of the recent tabs in the Home screen.
*/
interface RecentTabController {
/**
* @see [RecentTabInteractor.onRecentTabClicked]
*/
fun handleRecentTabClicked(tabId: String)
/**
* @see [RecentTabInteractor.onRecentTabShowAllClicked]
*/
fun handleRecentTabShowAllClicked()
}
/**
* The default implementation of [RecentTabController].
*
* @param selectTabUseCase [SelectTabUseCase] used selecting a tab.
* @param navController [NavController] used for navigation.
*/
class DefaultRecentTabsController(
private val selectTabUseCase: SelectTabUseCase,
private val navController: NavController
) : RecentTabController {
override fun handleRecentTabClicked(tabId: String) {
selectTabUseCase.invoke(tabId)
navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment)
}
override fun handleRecentTabShowAllClicked() {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalTabsTrayFragment()
)
}
}

@ -0,0 +1,23 @@
/* 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.home.recenttabs.interactor
/**
* Interface for recent tab related actions in the Home screen.
*/
interface RecentTabInteractor {
/**
* Opens the given tab. Called when a user clicks on a recent tab.
*
* @param tabId The ID of the tab to open.
*/
fun onRecentTabClicked(tabId: String)
/**
* Show the tabs tray. Called when a user clicks on the "Show all" button besides the recent
* tabs.
*/
fun onRecentTabShowAllClicked()
}

@ -0,0 +1,36 @@
/* 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.home.recenttabs.view
import android.view.View
import kotlinx.android.synthetic.main.recent_tabs_list_row.*
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.fenix.R
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
import org.mozilla.fenix.utils.view.ViewHolder
/**
* View holder for a recent tab item.
*
* @param interactor [RecentTabInteractor] which will have delegated to all user interactions.
*/
class RecentTabViewHolder(
view: View,
private val interactor: RecentTabInteractor
) : ViewHolder(view) {
fun bindTab(tab: TabSessionState) {
recent_tab_title.text = tab.content.title
recent_tab_icon.setImageBitmap(tab.content.icon)
itemView.setOnClickListener {
interactor.onRecentTabClicked(tab.id)
}
}
companion object {
const val LAYOUT_ID = R.layout.recent_tabs_list_row
}
}

@ -0,0 +1,32 @@
/* 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.home.recenttabs.view
import android.view.View
import kotlinx.android.synthetic.main.recent_tabs_header.*
import org.mozilla.fenix.R
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
import org.mozilla.fenix.utils.view.ViewHolder
/**
* View holder for the recent tabs header and "Show all" button.
*
* @param interactor [RecentTabInteractor] which will have delegated to all user interactions.
*/
class RecentTabsHeaderViewHolder(
view: View,
private val interactor: RecentTabInteractor
) : ViewHolder(view) {
init {
show_all_button.setOnClickListener {
interactor.onRecentTabShowAllClicked()
}
}
companion object {
const val LAYOUT_ID = R.layout.recent_tabs_header
}
}

@ -0,0 +1,47 @@
/* 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.home.sessioncontrol
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentStore
/**
* View-bound feature that dispatches recent tab changes to the [HomeFragmentStore] when the
* [BrowserStore] is updated.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class RecentTabsListFeature(
private val browserStore: BrowserStore,
private val homeStore: HomeFragmentStore
) : AbstractBinding<BrowserState>(browserStore) {
override suspend fun onState(flow: Flow<BrowserState>) {
flow.map { it.selectedTabId }
.ifChanged()
.collect { selectedTabId ->
// Attempt to get the selected normal tab since here may not be a selected tab or
// the selected tab may be a private tab.
val selectedTab = browserStore.state.normalTabs.firstOrNull {
it.id == selectedTabId
}
val recentTabsList = if (selectedTab != null) {
listOf(selectedTab)
} else {
emptyList()
}
homeStore.dispatch(HomeFragmentAction.RecentTabsChange(recentTabsList))
}
}
}

@ -12,6 +12,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.ui.widgets.WidgetSiteItemView
@ -36,6 +37,8 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTh
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingToolbarPositionPickerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTrackingProtectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingWhatsNewViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabsHeaderViewHolder
import org.mozilla.fenix.home.tips.ButtonTipViewHolder
import mozilla.components.feature.tab.collections.Tab as ComponentTab
@ -131,6 +134,11 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
object OnboardingWhatsNew : AdapterItem(OnboardingWhatsNewViewHolder.LAYOUT_ID)
object RecentTabsHeader : AdapterItem(RecentTabsHeaderViewHolder.LAYOUT_ID)
data class RecentTabItem(val tab: TabSessionState) : AdapterItem(RecentTabViewHolder.LAYOUT_ID) {
override fun sameAs(other: AdapterItem) = other is RecentTabItem && tab.id == other.tab.id
}
/**
* True if this item represents the same value as other. Used by [AdapterItemDiffCallback].
*/
@ -211,7 +219,8 @@ class SessionControlAdapter(
view
)
ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor)
RecentTabsHeaderViewHolder.LAYOUT_ID -> RecentTabsHeaderViewHolder(view, interactor)
RecentTabViewHolder.LAYOUT_ID -> RecentTabViewHolder(view, interactor)
else -> throw IllegalStateException()
}
}
@ -263,6 +272,9 @@ class SessionControlAdapter(
is OnboardingAutomaticSignInViewHolder -> holder.bind(
(item as AdapterItem.OnboardingAutomaticSignIn).state.withAccount
)
is RecentTabViewHolder -> {
holder.bindTab((item as AdapterItem.RecentTabItem).tab)
}
}
}
}

@ -8,6 +8,8 @@ import mozilla.components.feature.tab.collections.Tab
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
/**
* Interface for tab related actions in the [SessionControlInteractor].
@ -204,14 +206,16 @@ interface ExperimentCardInteractor {
/**
* Interactor for the Home screen.
* Provides implementations for the CollectionInteractor, OnboardingInteractor,
* TabSessionInteractor and TopSiteInteractor.
* Provides implementations for the CollectionInteractor, OnboardingInteractor, TopSiteInteractor,
* TipInteractor, TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor, and
* RecentTabInteractor.
*/
@SuppressWarnings("TooManyFunctions")
class SessionControlInteractor(
private val controller: SessionControlController
private val controller: SessionControlController,
private val recentTabController: RecentTabController
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor,
TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor {
TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor, RecentTabInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection)
}
@ -315,4 +319,12 @@ class SessionControlInteractor(
override fun onCloseExperimentCardClicked() {
controller.handleCloseExperimentCard()
}
override fun onRecentTabClicked(tabId: String) {
recentTabController.handleRecentTabClicked(tabId)
}
override fun onRecentTabShowAllClicked() {
recentTabController.handleRecentTabShowAllClicked()
}
}

@ -10,6 +10,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.tips.Tip
@ -28,7 +29,8 @@ private fun normalModeAdapterItems(
expandedCollections: Set<Long>,
tip: Tip?,
showCollectionsPlaceholder: Boolean,
showSetAsDefaultBrowserCard: Boolean
showSetAsDefaultBrowserCard: Boolean,
recentTabs: List<TabSessionState>
): List<AdapterItem> {
val items = mutableListOf<AdapterItem>()
@ -42,6 +44,10 @@ private fun normalModeAdapterItems(
items.add(AdapterItem.TopSitePager(topSites))
}
if (recentTabs.isNotEmpty()) {
showRecentTabs(recentTabs, items)
}
if (collections.isEmpty()) {
if (showCollectionsPlaceholder) {
items.add(AdapterItem.NoCollectionsMessage)
@ -53,6 +59,16 @@ private fun normalModeAdapterItems(
return items
}
private fun showRecentTabs(
recentTabs: List<TabSessionState>,
items: MutableList<AdapterItem>
) {
items.add(AdapterItem.RecentTabsHeader)
recentTabs.forEach {
items.add(AdapterItem.RecentTabItem(it))
}
}
private fun showCollections(
collections: List<TabCollection>,
expandedCollections: Set<Long>,
@ -116,7 +132,8 @@ private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
expandedCollections,
tip,
showCollectionPlaceholder,
showSetAsDefaultBrowserCard
showSetAsDefaultBrowserCard,
recentTabs
)
is Mode.Private -> privateModeAdapterItems()
is Mode.Onboarding -> onboardingAdapterItems(mode.state)

@ -5,7 +5,7 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/collections_header_text"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_height="56dp"
android:gravity="center_vertical"
android:text="@string/collections_header"
android:textAppearance="@style/HeaderTextStyle" />

@ -8,7 +8,7 @@
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="12dp"
android:background="@drawable/collection_home_list_row_background"
android:background="@drawable/home_list_row_background"
android:clickable="true"
android:clipToPadding="false"
android:elevation="@dimen/home_collection_elevation"

@ -0,0 +1,34 @@
<?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="56dp"
android:layout_marginTop="40dp">
<androidx.appcompat.widget.AppCompatTextView
style="@style/Header20TextStyle"
android:id="@+id/header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recent_tabs_header"
android:layout_marginVertical="16dp"
android:maxLines="2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/show_all_button"
style="@style/Button12TextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:text="@string/recent_tabs_show_all"
android:textColor="@color/home_show_all_button_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,50 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/home_list_row_background"
android:clipToPadding="false"
android:elevation="@dimen/home_collection_elevation"
android:foreground="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/recent_tab_icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="12dp"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/recent_tab_title"
style="@style/Body16TextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:minLines="1"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@id/recent_tab_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/recent_tab_icon"
app:layout_constraintTop_toTopOf="@id/recent_tab_icon"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -95,6 +95,9 @@
<color name="collection_icon_color_green">@color/collection_icon_color_green_dark_theme</color>
<color name="collection_icon_color_yellow">@color/collection_icon_color_yellow_dark_theme</color>
<!-- Home screen -->
<color name="home_show_all_button_text">@color/photonLightGrey50</color>
<!-- Search Widget -->
<color name="search_widget_background">@color/inset_dark_theme</color>
<color name="search_widget_mic_fill_color">@color/primary_text_dark_theme</color>

@ -300,6 +300,9 @@
<item>@color/collection_icon_color_yellow</item>
</array>
<!-- Home screen -->
<color name="home_show_all_button_text">@color/photonDarkGrey05</color>
<!-- Library buttons -->
<color name="library_sessions_icon_background">#B9F0FD</color>
<color name="library_sessions_icon">#0E214A</color>

@ -100,6 +100,12 @@
<!-- Heading for the Top Sites block -->
<string name="home_screen_top_sites_heading">Top sites</string>
<!-- Recent Tabs -->
<!-- Header text for jumping back into the recent tab in the home screen -->
<string name="recent_tabs_header">Jump back in</string>
<!-- Button text for showing all the tabs in the tabs tray -->
<string name="recent_tabs_show_all">Show all</string>
<!-- Browser Fragment -->
<!-- Content description (not visible, for screen readers etc.): Navigate to open tabs -->
<string name="browser_tabs_button">Open Tabs</string>

@ -369,6 +369,12 @@
<item name="fontFamily">@font/metropolis_semibold</item>
</style>
<style name="Header20TextStyle" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textColor">?primaryText</item>
<item name="android:textSize">20sp</item>
<item name="fontFamily">@font/metropolis_semibold</item>
</style>
<style name="Header16TextStyle" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textColor">?primaryText</item>
<item name="android:textSize">16sp</item>
@ -397,6 +403,12 @@
<item name="android:textColor">?primaryText</item>
</style>
<style name="Button12TextStyle" parent="TextAppearance.MaterialComponents.Button">
<item name="android:textSize">12sp</item>
<item name="fontFamily">@font/metropolis</item>
<item name="android:textAllCaps">false</item>
</style>
<style name="SubtitleTextStyle" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textColor">?secondaryText</item>
<item name="android:textSize">14sp</item>

@ -0,0 +1,64 @@
/* 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.ext
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class BrowserStateTest {
@Test
fun `WHEN there is a selected tab THEN asRecentTabs returns the selected tab as a list`() {
val tab = createTab(
url = "https://www.mozilla.org",
id = "1"
)
val tabs = listOf(tab)
val state = BrowserState(
tabs = tabs,
selectedTabId = tab.id
)
val recentTabs = state.asRecentTabs()
assertEquals(tabs, recentTabs)
}
@Test
fun `WHEN there is no selected tab THEN asRecentTabs returns an empty list`() {
val state = BrowserState(
tabs = listOf(
createTab(
url = "https://www.mozilla.org",
id = "1"
)
)
)
val recentTabs = state.asRecentTabs()
assertEquals(0, recentTabs.size)
}
@Test
fun `WHEN the selected tab is private THEN asRecentTabs returns an empty list`() {
val tab = createTab(
url = "https://www.mozilla.org",
id = "1",
private = true
)
val tabs = listOf(tab)
val state = BrowserState(
tabs = tabs,
selectedTabId = tab.id
)
val recentTabs = state.asRecentTabs()
assertEquals(0, recentTabs.size)
}
}

@ -133,7 +133,8 @@ class DefaultSessionControlControllerTest {
mode = Mode.Normal,
topSites = emptyList(),
showCollectionPlaceholder = true,
showSetAsDefaultBrowserCard = true
showSetAsDefaultBrowserCard = true,
recentTabs = emptyList()
)
every { navController.currentDestination } returns mockk {

@ -8,6 +8,7 @@ import android.content.Context
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.service.fxa.manager.FxaAccountManager
@ -56,7 +57,8 @@ class HomeFragmentStoreTest {
mode = currentMode.getCurrentMode(),
topSites = emptyList(),
showCollectionPlaceholder = true,
showSetAsDefaultBrowserCard = true
showSetAsDefaultBrowserCard = true,
recentTabs = emptyList()
)
homeFragmentStore = HomeFragmentStore(homeFragmentState)
@ -98,6 +100,17 @@ class HomeFragmentStoreTest {
assertEquals(topSites, homeFragmentStore.state.topSites)
}
@Test
fun `Test changing the recent tabs in HomeFragmentStore`() = runBlocking {
assertEquals(0, homeFragmentStore.state.recentTabs.size)
// Add 2 TabSessionState to the HomeFragmentStore.
val recentTabs: List<TabSessionState> = listOf(mockk(), mockk())
homeFragmentStore.dispatch(HomeFragmentAction.RecentTabsChange(recentTabs)).join()
assertEquals(recentTabs, homeFragmentStore.state.recentTabs)
}
@Test
fun `Test changing hiding collections placeholder`() = runBlocking {
assertTrue(homeFragmentStore.state.showCollectionPlaceholder)
@ -127,22 +140,26 @@ class HomeFragmentStoreTest {
// Verify that the default state of the HomeFragment is correct.
assertEquals(0, homeFragmentStore.state.collections.size)
assertEquals(0, homeFragmentStore.state.topSites.size)
assertEquals(0, homeFragmentStore.state.recentTabs.size)
assertEquals(Mode.Normal, homeFragmentStore.state.mode)
val collections: List<TabCollection> = listOf(mockk())
val topSites: List<TopSite> = listOf(mockk(), mockk())
val recentTabs: List<TabSessionState> = listOf(mockk(), mockk())
homeFragmentStore.dispatch(
HomeFragmentAction.Change(
collections = collections,
mode = Mode.Private,
topSites = topSites,
showCollectionPlaceholder = true
showCollectionPlaceholder = true,
recentTabs = recentTabs
)
).join()
assertEquals(1, homeFragmentStore.state.collections.size)
assertEquals(Mode.Private, homeFragmentStore.state.mode)
assertEquals(2, homeFragmentStore.state.topSites.size)
assertEquals(2, homeFragmentStore.state.recentTabs.size)
assertEquals(Mode.Private, homeFragmentStore.state.mode)
}
}

@ -0,0 +1,144 @@
/* 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.home
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.home.sessioncontrol.RecentTabsListFeature
class RecentTabsListFeatureTest {
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher())
@Test
fun `GIVEN no selected tab WHEN the feature starts THEN dispatch an empty list`() {
val browserStore = BrowserStore()
val homeStore = HomeFragmentStore()
val feature = RecentTabsListFeature(
browserStore = browserStore,
homeStore = homeStore
)
feature.start()
homeStore.waitUntilIdle()
assertEquals(0, homeStore.state.recentTabs.size)
}
@Test
fun `GIVEN a selected tab WHEN the feature starts THEN dispatch the selected tab as a recent tab list`() {
val tab = createTab(
url = "https://www.mozilla.org",
id = "1"
)
val tabs = listOf(tab)
val browserStore = BrowserStore(
BrowserState(
tabs = tabs,
selectedTabId = "1"
)
)
val homeStore = HomeFragmentStore()
val feature = RecentTabsListFeature(
browserStore = browserStore,
homeStore = homeStore
)
feature.start()
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
}
@Test
fun `WHEN the browser state has an updated select tab THEN dispatch the new recent tab list`() {
val tab1 = createTab(
url = "https://www.mozilla.org",
id = "1"
)
val tab2 = createTab(
url = "https://www.firefox.com",
id = "2"
)
val tabs = listOf(tab1, tab2)
val browserStore = BrowserStore(
BrowserState(
tabs = tabs,
selectedTabId = "1"
)
)
val homeStore = HomeFragmentStore()
val feature = RecentTabsListFeature(
browserStore = browserStore,
homeStore = homeStore
)
feature.start()
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(tab1, homeStore.state.recentTabs[0])
browserStore.dispatch(TabListAction.SelectTabAction(tab2.id)).joinBlocking()
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(tab2, homeStore.state.recentTabs[0])
}
@Test
fun `WHEN the browser state selects a private tab THEN dispatch an empty list`() {
val normalTab = createTab(
url = "https://www.mozilla.org",
id = "1"
)
val privateTab = createTab(
url = "https://www.firefox.com",
id = "2",
private = true
)
val tabs = listOf(normalTab, privateTab)
val browserStore = BrowserStore(
BrowserState(
tabs = tabs,
selectedTabId = "1"
)
)
val homeStore = HomeFragmentStore()
val feature = RecentTabsListFeature(
browserStore = browserStore,
homeStore = homeStore
)
feature.start()
homeStore.waitUntilIdle()
assertEquals(1, homeStore.state.recentTabs.size)
assertEquals(normalTab, homeStore.state.recentTabs[0])
browserStore.dispatch(TabListAction.SelectTabAction(privateTab.id)).joinBlocking()
homeStore.waitUntilIdle()
assertEquals(0, homeStore.state.recentTabs.size)
}
}

@ -10,18 +10,20 @@ import mozilla.components.feature.tab.collections.Tab
import mozilla.components.feature.tab.collections.TabCollection
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
class SessionControlInteractorTest {
private val controller: DefaultSessionControlController = mockk(relaxed = true)
private val recentTabController: RecentTabController = mockk(relaxed = true)
private lateinit var interactor: SessionControlInteractor
@Before
fun setup() {
interactor = SessionControlInteractor(controller)
interactor = SessionControlInteractor(controller, recentTabController)
}
@Test
@ -128,4 +130,17 @@ class SessionControlInteractorTest {
interactor.onTopSiteMenuOpened()
verify { controller.handleMenuOpened() }
}
@Test
fun onRecentTabClicked() {
val tabId = "tabId"
interactor.onRecentTabClicked(tabId)
verify { recentTabController.handleRecentTabClicked(tabId) }
}
@Test
fun onRecentTabShowAllClicked() {
interactor.onRecentTabShowAllClicked()
verify { recentTabController.handleRecentTabShowAllClicked() }
}
}

@ -0,0 +1,89 @@
/* 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.home.recenttabs.controller
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
@OptIn(ExperimentalCoroutinesApi::class)
class RecentTabControllerTest {
private val testDispatcher = TestCoroutineDispatcher()
@get:Rule
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
private val navController: NavController = mockk(relaxed = true)
private val selectTabUseCase: TabsUseCases = mockk(relaxed = true)
private lateinit var store: BrowserStore
private lateinit var controller: RecentTabController
@Before
fun setup() {
store = BrowserStore(
BrowserState()
)
controller = spyk(DefaultRecentTabsController(
selectTabUseCase = selectTabUseCase.selectTab,
navController = navController
))
every { navController.currentDestination } returns mockk {
every { id } returns R.id.homeFragment
}
}
@Test
fun handleRecentTabClicked() {
val tab = createTab(
url = "https://mozilla.org",
title = "Mozilla"
)
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
controller.handleRecentTabClicked(tab.id)
verify {
selectTabUseCase.selectTab.invoke(tab.id)
navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment)
}
}
@Test
fun handleRecentTabShowAllClicked() {
controller.handleRecentTabShowAllClicked()
verify {
navController.navigate(
match<NavDirections> { it.actionId == R.id.action_global_tabsTrayFragment },
null
)
}
}
}

@ -0,0 +1,53 @@
/* 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.home.recenttabs.view
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.recent_tabs_list_row.view.*
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
@RunWith(FenixRobolectricTestRunner::class)
class RecentTabViewHolderTest {
private lateinit var view: View
private lateinit var interactor: SessionControlInteractor
private val tab = createTab(
url = "https://mozilla.org",
title = "Mozilla"
)
@Before
fun setup() {
view = LayoutInflater.from(testContext).inflate(RecentTabViewHolder.LAYOUT_ID, null)
interactor = mockk(relaxed = true)
}
@Test
fun `GIVEN a new recent tab on bind THEN set the title text`() {
RecentTabViewHolder(view, interactor).bindTab(tab)
assertEquals(tab.content.title, view.recent_tab_title.text)
}
@Test
fun `WHEN a recent tab item is clicked THEN interactor iis called`() {
RecentTabViewHolder(view, interactor).bindTab(tab)
view.performClick()
verify { interactor.onRecentTabClicked(tab.id) }
}
}

@ -0,0 +1,39 @@
/* 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.home.recenttabs.view
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.recent_tabs_header.view.*
import mozilla.components.support.test.robolectric.testContext
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
@RunWith(FenixRobolectricTestRunner::class)
class RecentTabsHeaderViewHolderTest {
private lateinit var view: View
private lateinit var interactor: SessionControlInteractor
@Before
fun setup() {
view = LayoutInflater.from(testContext).inflate(RecentTabsHeaderViewHolder.LAYOUT_ID, null)
interactor = mockk(relaxed = true)
}
@Test
fun `WHEN show all button is clicked THEN interactor iis called`() {
RecentTabsHeaderViewHolder(view, interactor)
view.show_all_button.performClick()
verify { interactor.onRecentTabShowAllClicked() }
}
}
Loading…
Cancel
Save