[fenix] Issue https://github.com/mozilla-mobile/fenix/issues/21099 - Update items under "Jump back in" section to the latest designs

pull/600/head
Gabriel Luong 3 years ago committed by mergify[bot]
parent d4a454442b
commit b057d6a76f

@ -152,8 +152,10 @@ class CollectionTest {
}.openTabDrawer {
createCollection(webPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
closeTab()
}
homeScreen {
}.expandCollection(firstCollectionName) {
removeTabFromCollection(webPage.title)
verifyTabSavedInCollection(webPage.title, false)

@ -1,132 +0,0 @@
/* 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 android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
private const val TOP_MARGIN_DP = 1
/**
* All possible positions of a recent tab in relation to others when shown in the "Jump back in" section.
*/
enum class RecentTabsItemPosition {
/**
* This is the only tab to be shown in this section.
*/
SINGLE,
/**
* This item is to be shown at the top of the section with others below it.
*/
TOP,
/**
* This item is to be shown between others in this section.
*/
MIDDLE,
/**
* This item is to be shown at the bottom of the section with others above it.
*/
BOTTOM
}
/**
* Helpers for setting various layout properties for the view from a [RecentTabViewHolder].
*
* Depending on the provided [RecentTabsItemPosition]:
* - sets a different background so that the entire section possibly containing
* more such items would have rounded corners but sibling items not.
* - sets small margins for the items so that there's a clear separation between siblings
*/
sealed class RecentTabViewDecorator {
/**
* Apply the decoration to [itemView].
*/
abstract operator fun invoke(itemView: View): View
companion object {
/**
* Get the appropriate decorator to set view background / margins depending on the position
* of that view in the recent tabs section.
*/
fun forPosition(position: RecentTabsItemPosition) = when (position) {
RecentTabsItemPosition.SINGLE -> SingleTabDecoration
RecentTabsItemPosition.TOP -> TopTabDecoration
RecentTabsItemPosition.MIDDLE -> MiddleTabDecoration
RecentTabsItemPosition.BOTTOM -> BottomTabDecoration
}
}
/**
* Decorator for a view shown in the recent tabs section that will update it to express
* that that item is the single one shown in this section.
*/
object SingleTabDecoration : RecentTabViewDecorator() {
override fun invoke(itemView: View): View {
val context = itemView.context
itemView.background =
AppCompatResources.getDrawable(context, R.drawable.card_list_row_background)
return itemView
}
}
/**
* Decorator for a view shown in the recent tabs section that will update it to express
* that this is an item shown at the top of the section and there are others below it.
*/
object TopTabDecoration : RecentTabViewDecorator() {
override fun invoke(itemView: View): View {
val context = itemView.context
itemView.background =
AppCompatResources.getDrawable(context, R.drawable.rounded_top_corners)
return itemView
}
}
/**
* Decorator for a view shown in the recent tabs section that will update it to express
* that this is an item shown has other recents tabs to be shown on top or below it.
*/
object MiddleTabDecoration : RecentTabViewDecorator() {
override fun invoke(itemView: View): View {
val context = itemView.context
itemView.setBackgroundColor(context.getColorFromAttr(R.attr.above))
(itemView.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin =
TOP_MARGIN_DP.dpToPx(context.resources.displayMetrics)
return itemView
}
}
/**
* Decorator for a view shown in the recent tabs section that will update it to express
* that this is an item shown at the bottom of the section and there are others above it.
*/
object BottomTabDecoration : RecentTabViewDecorator() {
override fun invoke(itemView: View): View {
val context = itemView.context
itemView.background =
AppCompatResources.getDrawable(context, R.drawable.rounded_bottom_corners)
(itemView.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin =
TOP_MARGIN_DP.dpToPx(context.resources.displayMetrics)
return itemView
}
}
}

@ -5,55 +5,44 @@
package org.mozilla.fenix.home.recenttabs.view
import android.view.View
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.RecentTabsListRowBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.utils.view.ViewHolder
/**
* View holder for a recent tab item.
*
* @param composeView [ComposeView] which will be populated with Jetpack Compose UI content.
* @param store [HomeFragmentStore] containing the list of recent tabs to be displayed.
* @param interactor [RecentTabInteractor] which will have delegated to all user interactions.
* @param icons an instance of [BrowserIcons] for rendering the sites icon if one isn't found
* in [ContentState.icon].
*/
class RecentTabViewHolder(
private val view: View,
private val interactor: RecentTabInteractor,
private val icons: BrowserIcons = view.context.components.core.icons
) : ViewHolder(view) {
fun bindTab(tab: TabSessionState): View {
// A page may take a while to retrieve a title, so let's show the url until we get one.
val biding = RecentTabsListRowBinding.bind(view)
biding.recentTabTitle.text = if (tab.content.title.isNotEmpty()) {
tab.content.title
} else {
tab.content.url
}
if (tab.content.icon != null) {
biding.recentTabIcon.setImageBitmap(tab.content.icon)
} else {
icons.loadIntoView(biding.recentTabIcon, tab.content.url)
}
biding.recentTabIcon.setImageBitmap(tab.content.icon)
itemView.setOnClickListener {
interactor.onRecentTabClicked(tab.id)
val composeView: ComposeView,
private val store: HomeFragmentStore,
private val interactor: RecentTabInteractor
) : ViewHolder(composeView) {
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
composeView.setContent {
val recentTabs = store.observeAsComposableState { state -> state.recentTabs }
FirefoxTheme {
RecentTabs(
recentTabs = recentTabs.value ?: emptyList(),
onRecentTabClick = { interactor.onRecentTabClicked(it) }
)
}
}
return itemView
}
companion object {
const val LAYOUT_ID = R.layout.recent_tabs_list_row
val LAYOUT_ID = View.generateViewId()
}
}

@ -0,0 +1,197 @@
/* 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/. */
@file:Suppress("MagicNumber")
package org.mozilla.fenix.home.recenttabs.view
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.support.ktx.kotlin.getRepresentativeSnippet
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components
import org.mozilla.fenix.theme.FirefoxTheme
/**
* A list of recent tabs to jump back to.
*
* @param recentTabs List of [TabSessionState] to display.
* @param onRecentTabClick Invoked when the user clicks on a recent tab.
*/
@Composable
fun RecentTabs(
recentTabs: List<TabSessionState>,
onRecentTabClick: (String) -> Unit = {}
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
recentTabs.forEach { tab ->
RecentTabItem(
tabId = tab.id,
url = tab.content.url,
title = tab.content.title,
icon = tab.content.icon,
onRecentTabClick = onRecentTabClick
)
}
}
}
/**
* A recent tab item.
*
* @param tabId Tbe id of the tab.
* @param url The loaded URL of the tab.
* @param title The title of the tab.
* @param icon The icon of the tab.
* @param onRecentTabClick Invoked when the user clicks on a recent tab.
*/
@Composable
private fun RecentTabItem(
tabId: String,
url: String,
title: String,
icon: Bitmap? = null,
onRecentTabClick: (String) -> Unit = {}
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(116.dp)
.clickable { onRecentTabClick(tabId) },
shape = RoundedCornerShape(8.dp),
backgroundColor = FirefoxTheme.colors.surface,
elevation = 6.dp
) {
Row(
modifier = Modifier.padding(16.dp)
) {
RecentTabImage(
url = url,
modifier = Modifier.size(116.dp, 84.dp),
icon = icon
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
RecentTabTitle(title = title)
RecentTabSubtitle(url = url)
}
}
}
}
/**
* A recent tab image.
*
* @param url The loaded URL of the tab.
* @param modifier modifier Modifier used to draw the image content.
* @param icon The icon of the tab. Fallback to loading the icon from the [url] if the [icon]
* is null.
*/
@Composable
private fun RecentTabImage(
url: String,
modifier: Modifier = Modifier,
icon: Bitmap? = null
) {
if (icon != null) {
Image(
painter = BitmapPainter(icon.asImageBitmap()),
contentDescription = null,
modifier = modifier,
)
} else {
components.core.icons.Loader(
url = url
) {
Placeholder {
Box(
modifier = Modifier.background(
color = when (isSystemInDarkTheme()) {
true -> Color(0xFF42414D) // DarkGrey30
false -> PhotonColors.LightGrey30
}
)
)
}
WithIcon { icon ->
Image(
painter = icon.painter,
contentDescription = null,
modifier = modifier,
)
}
}
}
}
/**
* A recent tab title.
*
* @param title The title of the tab.
*/
@Composable
private fun RecentTabTitle(title: String) {
Text(
text = title,
color = FirefoxTheme.colors.textPrimary,
fontSize = 14.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 2
)
}
/**
* A recent tab subtitle.
*
* @param url The loaded URL of the tab.
*/
@Composable
private fun RecentTabSubtitle(url: String) {
Text(
text = url.getRepresentativeSnippet(),
color = FirefoxTheme.colors.textSecondary,
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}

@ -22,7 +22,6 @@ class RecentTabsHeaderViewHolder(
) : ViewHolder(view) {
init {
val binding = RecentTabsHeaderBinding.bind(view)
binding.showAllButton.setOnClickListener {
dismissSearchDialogIfDisplayed()

@ -13,7 +13,6 @@ 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.concept.storage.BookmarkNode
import mozilla.components.concept.storage.HistoryMetadata
import mozilla.components.feature.tab.collections.TabCollection
@ -28,14 +27,11 @@ import org.mozilla.fenix.historymetadata.view.HistoryMetadataViewHolder
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.recentbookmarks.view.RecentBookmarksViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabViewDecorator
import org.mozilla.fenix.home.recenttabs.view.RecentTabViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabsHeaderViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabsItemPosition
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketStoriesViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSitePagerViewHolder
@ -51,6 +47,7 @@ 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.sessioncontrol.viewholders.pocket.PocketStoriesViewHolder
import org.mozilla.fenix.home.tips.ButtonTipViewHolder
import mozilla.components.feature.tab.collections.Tab as ComponentTab
@ -167,21 +164,7 @@ 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,
val position: RecentTabsItemPosition
) : AdapterItem(RecentTabViewHolder.LAYOUT_ID) {
override fun sameAs(other: AdapterItem) = other is RecentTabItem && tab.id == other.tab.id &&
position == other.position
override fun contentsSameAs(other: AdapterItem): Boolean {
val otherItem = other as RecentTabItem
// We only care about updating if the title and icon have changed because that is
// all we show today. This should be updated if we want to show updates for more.
return tab.content.title == otherItem.tab.content.title &&
tab.content.icon == otherItem.tab.content.icon
}
}
object RecentTabItem : AdapterItem(RecentTabViewHolder.LAYOUT_ID)
object HistoryMetadataHeader : AdapterItem(HistoryMetadataHeaderViewHolder.LAYOUT_ID)
@ -275,6 +258,11 @@ class SessionControlAdapter(
store,
components.core.client
)
RecentTabViewHolder.LAYOUT_ID -> return RecentTabViewHolder(
composeView = ComposeView(parent.context),
store = store,
interactor = interactor
)
}
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
@ -323,7 +311,6 @@ class SessionControlAdapter(
)
ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor)
RecentTabsHeaderViewHolder.LAYOUT_ID -> RecentTabsHeaderViewHolder(view, interactor)
RecentTabViewHolder.LAYOUT_ID -> RecentTabViewHolder(view, interactor)
RecentBookmarksViewHolder.LAYOUT_ID -> {
RecentBookmarksViewHolder(view, interactor)
}
@ -342,7 +329,14 @@ class SessionControlAdapter(
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
when (holder) {
is PocketStoriesViewHolder -> holder.composeView.disposeComposition()
is RecentTabViewHolder,
is PocketStoriesViewHolder -> {
// no op
// This previously called "composeView.disposeComposition" which would have the
// entire Composable destroyed and recreated when this View is scrolled off or on screen again.
// This View already listens and maps store updates. Avoid creating and binding new Views.
// The composition will live until the ViewTreeLifecycleOwner to which it's attached to is destroyed.
}
else -> super.onViewRecycled(holder)
}
}
@ -394,12 +388,6 @@ class SessionControlAdapter(
is OnboardingAutomaticSignInViewHolder -> holder.bind(
(item as AdapterItem.OnboardingAutomaticSignIn).state.withAccount
)
is RecentTabViewHolder -> {
val (tab, tabPosition) = item as AdapterItem.RecentTabItem
holder.bindTab(tab).apply {
RecentTabViewDecorator.forPosition(tabPosition).invoke(this)
}
}
is RecentBookmarksViewHolder -> {
holder.bind(
(item as AdapterItem.RecentBookmarks).recentBookmarks
@ -411,6 +399,7 @@ class SessionControlAdapter(
is HistoryMetadataGroupViewHolder -> {
holder.bind((item as AdapterItem.HistoryMetadataGroup).historyMetadataGroup)
}
is RecentTabViewHolder,
is PocketStoriesViewHolder -> {
// no-op. This ViewHolder receives the HomeStore as argument and will observe that
// without the need for us to manually update from here the data to be displayed.

@ -6,13 +6,12 @@ package org.mozilla.fenix.home.sessioncontrol
import android.content.Context
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.service.pocket.PocketRecommendedStory
@ -25,7 +24,6 @@ import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.recenttabs.view.RecentTabsItemPosition
// This method got a little complex with the addition of the tab tray feature flag
// When we remove the tabs from the home screen this will get much simpler again.
@ -56,7 +54,8 @@ private fun normalModeAdapterItems(
}
if (recentTabs.isNotEmpty()) {
showRecentTabs(recentTabs, items)
items.add(AdapterItem.RecentTabsHeader)
items.add(AdapterItem.RecentTabItem)
}
if (recentBookmarks.isNotEmpty()) {
@ -82,45 +81,6 @@ private fun normalModeAdapterItems(
return items
}
/**
* Constructs the list of items to be shown in the recent tabs section.
*
* This section's structure is:
* - section header
* - one or more normal tabs
* - zero or one media tab (if there is a tab opened on which media started playing.
* This may be a duplicate of one of the normal tabs shown above).
*/
@VisibleForTesting
internal fun showRecentTabs(
recentTabs: List<TabSessionState>,
items: MutableList<AdapterItem>
) {
items.add(AdapterItem.RecentTabsHeader)
recentTabs.forEachIndexed { index, recentTab ->
// If this is the first tab to be shown but more will follow.
if (index == 0 && recentTabs.size > 1) {
items.add(AdapterItem.RecentTabItem(recentTab, RecentTabsItemPosition.TOP))
}
// if this is the only tab to be shown.
else if (index == 0 && recentTabs.size == 1) {
items.add(AdapterItem.RecentTabItem(recentTab, RecentTabsItemPosition.SINGLE))
}
// If there are items above and below.
else if (index < recentTabs.size - 1) {
items.add(AdapterItem.RecentTabItem(recentTab, RecentTabsItemPosition.MIDDLE))
}
// If this is the last recent tab to be shown.
else if (index < recentTabs.size) {
items.add(AdapterItem.RecentTabItem(recentTab, RecentTabsItemPosition.BOTTOM))
}
}
}
private fun showHistoryMetadata(
historyMetadata: List<HistoryMetadataGroup>,
items: MutableList<AdapterItem>

@ -6,10 +6,16 @@ package org.mozilla.fenix.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Colors
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import mozilla.components.ui.colors.PhotonColors
/**
* The theme for Mozilla Firefox for Android (Fenix).
@ -19,12 +25,76 @@ fun FirefoxTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
content = content,
colors = if (darkTheme) darkColorPalette else lightColorPalette
val colors = if (darkTheme) darkColorPalette else lightColorPalette
ProvideFirefoxColors(colors) {
MaterialTheme(
content = content
)
}
}
object FirefoxTheme {
val colors: FirefoxColors
@Composable
get() = localFirefoxColors.current
}
private val darkColorPalette = FirefoxColors(
surface = PhotonColors.DarkGrey50,
textPrimary = PhotonColors.LightGrey05,
textSecondary = PhotonColors.LightGrey05
)
private val lightColorPalette = FirefoxColors(
surface = PhotonColors.White,
textPrimary = PhotonColors.DarkGrey90,
textSecondary = PhotonColors.DarkGrey05
)
/**
* A custom Color Palette for Mozilla Firefox for Android (Fenix).
*/
@Stable
class FirefoxColors(
surface: Color,
textPrimary: Color,
textSecondary: Color
) {
var surface by mutableStateOf(surface)
private set
var textPrimary by mutableStateOf(textPrimary)
private set
var textSecondary by mutableStateOf(textSecondary)
private set
fun update(other: FirefoxColors) {
surface = other.surface
textPrimary = other.textPrimary
textSecondary = other.textSecondary
}
fun copy(): FirefoxColors = FirefoxColors(
surface = surface,
textPrimary = textPrimary,
textSecondary = textSecondary
)
}
private val darkColorPalette: Colors = darkColors()
@Composable
fun ProvideFirefoxColors(
colors: FirefoxColors,
content: @Composable () -> Unit
) {
val colorPalette = remember {
// Explicitly creating a new object here so we don't mutate the initial [colors]
// provided, and overwrite the values set in it.
colors.copy()
}
colorPalette.update(colors)
CompositionLocalProvider(localFirefoxColors provides colorPalette, content = content)
}
private val lightColorPalette: Colors = lightColors()
private val localFirefoxColors = staticCompositionLocalOf<FirefoxColors> {
error("No FirefoxColors provided")
}

@ -1,49 +0,0 @@
<?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:clipToPadding="false"
android:elevation="@dimen/home_item_elevation"
android:foreground="?android:attr/selectableItemBackground">
<com.google.android.material.imageview.ShapeableImageView
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"
style="@style/recentTabIcon"
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>

@ -226,10 +226,6 @@
<dimen name="top_sites_card_radius">8dp</dimen>
<dimen name="top_sites_card_stroke_width">1dp</dimen>
<!-- Recent tabs -->
<dimen name="recent_tabs_icon_elevation">0dp</dimen>
<dimen name="recent_tabs_icon_corner_size">4dp</dimen>
<!-- Credit Cards Fragment -->
<dimen name="credit_cards_saved_cards_item_margin_start">16dp</dimen>
<dimen name="credit_cards_saved_cards_item_margin_end">48dp</dimen>

@ -680,18 +680,6 @@
<item name="cornerSize">@dimen/top_sites_favicon_corner_size</item>
</style>
<style name="recentTabIcon">
<item name="android:scaleType">fitCenter</item>
<item name="android:layout_gravity">center</item>
<item name="shapeAppearanceOverlay">@style/recentTabIconShape</item>
</style>
<style name="recentTabIconShape">
<item name="cornerFamily">rounded</item>
<item name="elevation">@dimen/recent_tabs_icon_elevation</item>
<item name="cornerSize">@dimen/recent_tabs_icon_corner_size</item>
</style>
<style name="RecentBookmarks.FaviconCard" parent="Mozac.Widgets.Favicon">
<item name="android:layout_width">@dimen/recent_bookmark_item_width</item>
<item name="android:layout_height">@dimen/recent_bookmark_item_height</item>

@ -1,142 +0,0 @@
/* 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.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import io.mockk.verify
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.util.dpToPx
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.R
class RecentTabViewDecoratorTest {
@Test
fun `WHEN forPosition is called with RecentTabsItemPosition#SINGLE THEN return SingleTabDecoration`() {
val result = RecentTabViewDecorator.forPosition(RecentTabsItemPosition.SINGLE)
assertTrue(result is RecentTabViewDecorator.SingleTabDecoration)
}
@Test
fun `WHEN forPosition is called with RecentTabsItemPosition#TOP THEN return TopTabDecoration`() {
val result = RecentTabViewDecorator.forPosition(RecentTabsItemPosition.TOP)
assertTrue(result is RecentTabViewDecorator.TopTabDecoration)
}
@Test
fun `WHEN forPosition is called with RecentTabsItemPosition#MIDDLE THEN return MiddleTabDecoration`() {
val result = RecentTabViewDecorator.forPosition(RecentTabsItemPosition.MIDDLE)
assertTrue(result is RecentTabViewDecorator.MiddleTabDecoration)
}
@Test
fun `WHEN forPosition is called with RecentTabsItemPosition#BOTTOM THEN return SingleTabDecoration`() {
val result = RecentTabViewDecorator.forPosition(RecentTabsItemPosition.BOTTOM)
assertTrue(result is RecentTabViewDecorator.BottomTabDecoration)
}
@Test
fun `WHEN SingleTabDecoration is invoked for a View THEN set the appropriate background`() {
val view: View = mockk(relaxed = true)
val drawable: Drawable = mockk()
val drawableResCaptor = slot<Int>()
try {
mockkStatic(AppCompatResources::class)
every { AppCompatResources.getDrawable(any(), capture(drawableResCaptor)) } returns drawable
RecentTabViewDecorator.SingleTabDecoration(view)
verify { view.background = drawable }
assertEquals(R.drawable.card_list_row_background, drawableResCaptor.captured)
} finally {
unmockkStatic(AppCompatResources::class)
}
}
@Test
fun `WHEN TopTabDecoration is invoked for a View THEN set the appropriate background`() {
val view: View = mockk(relaxed = true)
val drawable: Drawable = mockk()
val drawableResCaptor = slot<Int>()
try {
mockkStatic(AppCompatResources::class)
every { AppCompatResources.getDrawable(any(), capture(drawableResCaptor)) } returns drawable
RecentTabViewDecorator.TopTabDecoration(view)
verify { view.background = drawable }
assertEquals(R.drawable.rounded_top_corners, drawableResCaptor.captured)
} finally {
unmockkStatic(AppCompatResources::class)
}
}
@Test
fun `WHEN MiddleTabDecoration is invoked for a View THEN set the appropriate background and layout params`() {
val colorAttrCaptor = slot<Int>()
val viewLayoutParams: ViewGroup.MarginLayoutParams = mockk(relaxed = true)
try {
mockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
mockkStatic("mozilla.components.support.ktx.android.content.ContextKt")
val view: View = mockk(relaxed = true) {
every { layoutParams } returns viewLayoutParams
every { context.getColorFromAttr(capture(colorAttrCaptor)) } returns 42
every { context.resources.displayMetrics } returns mockk(relaxed = true)
}
every { any<Int>().dpToPx(any()) } returns 43
RecentTabViewDecorator.MiddleTabDecoration(view)
verify { view.setBackgroundColor(42) }
assertEquals(R.attr.above, colorAttrCaptor.captured)
assertEquals(43, viewLayoutParams.topMargin)
} finally {
unmockkStatic("mozilla.components.support.ktx.android.content.ContextKt")
unmockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
}
}
@Test
fun `WHEN BottomTabDecoration is invoked for a View THEN set the appropriate background and layout params`() {
val viewLayoutParams: ViewGroup.MarginLayoutParams = mockk(relaxed = true)
val drawable: Drawable = mockk()
val drawableResCaptor = slot<Int>()
try {
mockkStatic(AppCompatResources::class)
every { AppCompatResources.getDrawable(any(), capture(drawableResCaptor)) } returns drawable
mockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
val view: View = mockk(relaxed = true) {
every { layoutParams } returns viewLayoutParams
every { context.resources.displayMetrics } returns mockk(relaxed = true)
}
every { any<Int>().dpToPx(any()) } returns 43
RecentTabViewDecorator.BottomTabDecoration(view)
verify { view.background = drawable }
assertEquals(R.drawable.rounded_bottom_corners, drawableResCaptor.captured)
assertEquals(43, viewLayoutParams.topMargin)
} finally {
unmockkStatic("mozilla.components.support.ktx.android.util.DisplayMetricsKt")
}
}
}

@ -1,87 +0,0 @@
/* 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 androidx.core.graphics.drawable.toBitmap
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.RecentTabsListRowBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
@RunWith(FenixRobolectricTestRunner::class)
class RecentTabViewHolderTest {
private lateinit var binding: RecentTabsListRowBinding
private lateinit var interactor: SessionControlInteractor
private lateinit var icons: BrowserIcons
private val tab = createTab(
url = "https://mozilla.org",
title = "Mozilla"
)
@Before
fun setup() {
binding = RecentTabsListRowBinding.inflate(LayoutInflater.from(testContext))
interactor = mockk(relaxed = true)
icons = mockk(relaxed = true)
every { icons.loadIntoView(binding.recentTabIcon, any()) } returns mockk()
}
@Test
fun `GIVEN a new recent tab on bind THEN set the title text and load the tab icon`() {
RecentTabViewHolder(binding.root, interactor, icons).bindTab(tab)
assertEquals(tab.content.title, binding.recentTabTitle.text)
verify { icons.loadIntoView(binding.recentTabIcon, IconRequest(tab.content.url)) }
}
@Test
fun `WHEN a recent tab item is clicked THEN interactor is called`() {
RecentTabViewHolder(binding.root, interactor, icons).bindTab(tab)
binding.root.performClick()
verify { interactor.onRecentTabClicked(tab.id) }
}
@Test
fun `WHEN a recent tab icon exists THEN load it`() {
val bitmap = testContext.getDrawable(R.drawable.ic_search)!!.toBitmap()
val tabWithIcon = tab.copy(content = tab.content.copy(icon = bitmap))
val viewHolder = RecentTabViewHolder(binding.root, interactor, icons)
assertNull(binding.recentTabIcon.drawable)
viewHolder.bindTab(tabWithIcon)
assertNotNull(binding.recentTabIcon.drawable)
}
@Test
fun `WHEN a recent tab does not have a title THEN show the url`() {
val tabWithoutTitle = createTab(url = "https://mozilla.org")
RecentTabViewHolder(binding.root, interactor, icons).bindTab(tabWithoutTitle)
assertEquals(tabWithoutTitle.content.url, binding.recentTabTitle.text)
}
}

@ -1,83 +0,0 @@
/* 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 io.mockk.mockk
import mozilla.components.browser.state.state.TabSessionState
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.home.recenttabs.view.RecentTabsItemPosition
class SessionControlViewTest {
@Test
fun `GIVEN two recent tabs WHEN showRecentTabs is called THEN add the header, and two recent items to be shown`() {
val recentTab: TabSessionState = mockk()
val mediaTab: TabSessionState = mockk()
val items = mutableListOf<AdapterItem>()
showRecentTabs(listOf(recentTab, mediaTab), items)
assertEquals(3, items.size)
assertTrue(items[0] is AdapterItem.RecentTabsHeader)
assertEquals(recentTab, (items[1] as AdapterItem.RecentTabItem).tab)
assertEquals(mediaTab, (items[2] as AdapterItem.RecentTabItem).tab)
}
@Test
fun `GIVEN one recent tab WHEN showRecentTabs is called THEN add the header and the recent tab to items shown`() {
val recentTab: TabSessionState = mockk()
val items = mutableListOf<AdapterItem>()
showRecentTabs(listOf(recentTab), items)
assertEquals(2, items.size)
assertTrue(items[0] is AdapterItem.RecentTabsHeader)
assertEquals(recentTab, (items[1] as AdapterItem.RecentTabItem).tab)
}
@Test
fun `GIVEN only one recent tab and no media tab WHEN showRecentTabs is called THEN add the recent item as a single one to be shown`() {
val recentTab: TabSessionState = mockk()
val items = mutableListOf<AdapterItem>()
showRecentTabs(listOf(recentTab), items)
assertEquals(recentTab, (items[1] as AdapterItem.RecentTabItem).tab)
assertSame(RecentTabsItemPosition.SINGLE, (items[1] as AdapterItem.RecentTabItem).position)
}
@Test
fun `GIVEN two recent tabs WHEN showRecentTabs is called THEN add one item as top and one as bottom to be shown`() {
val recentTab: TabSessionState = mockk()
val mediaTab: TabSessionState = mockk()
val items = mutableListOf<AdapterItem>()
showRecentTabs(listOf(recentTab, mediaTab), items)
assertEquals(recentTab, (items[1] as AdapterItem.RecentTabItem).tab)
assertSame(RecentTabsItemPosition.TOP, (items[1] as AdapterItem.RecentTabItem).position)
assertEquals(mediaTab, (items[2] as AdapterItem.RecentTabItem).tab)
assertSame(RecentTabsItemPosition.BOTTOM, (items[2] as AdapterItem.RecentTabItem).position)
}
@Test
fun `GIVEN three recent tabs WHEN showRecentTabs is called THEN add one recent item as top, one as middle and one as bottom to be shown`() {
val recentTab1: TabSessionState = mockk()
val recentTab2: TabSessionState = mockk()
val mediaTab: TabSessionState = mockk()
val items = mutableListOf<AdapterItem>()
showRecentTabs(listOf(recentTab1, recentTab2, mediaTab), items)
assertEquals(recentTab1, (items[1] as AdapterItem.RecentTabItem).tab)
assertSame(RecentTabsItemPosition.TOP, (items[1] as AdapterItem.RecentTabItem).position)
assertEquals(recentTab2, (items[2] as AdapterItem.RecentTabItem).tab)
assertSame(RecentTabsItemPosition.MIDDLE, (items[2] as AdapterItem.RecentTabItem).position)
assertEquals(mediaTab, (items[3] as AdapterItem.RecentTabItem).tab)
assertSame(RecentTabsItemPosition.BOTTOM, (items[3] as AdapterItem.RecentTabItem).position)
}
}
Loading…
Cancel
Save