For FNX-22339: Recently saved bookmarks (#19835)

* Title and button for home screen recently saved bookmarks section

Create bookmark item view with favicon and title

* View holders and interactors for recently saved bookmarks

Recent bookmark item view holder binding

Create adapter for recent bookmarks. Implement controller methods. Implement view holder bindings for items

Top level adapter for recent bookmarks section

Retrieve list of recent bookmarks on home

View holders and interactors for recently saved bookmarks

Recent bookmark item view holder binding

Create adapter for recent bookmarks. Implement controller methods. Implement view holder bindings for items

Top level adapter for recent bookmarks section

Retrieve list of recent bookmarks on home

Update list on app start and when bookmarks are added

View holders and interactors for recently saved bookmarks

Recent bookmark item view holder binding

Create adapter for recent bookmarks. Implement controller methods. Implement view holder bindings for items

Top level adapter for recent bookmarks section

Retrieve list of recent bookmarks on home

Update list on app start and when bookmarks are added

Make a use case for retrieving and updating the list of recently saved bookmarks

Add adapter items and define header viewholder binding

Use session interactor for header button clicks. Bind in the adapter

* Retrieve list of bookmarks asynchronously on home

Interactor and controller tests

Address review comments

Split up tests for recent bookmarks

Update to new interactors

Dark mode and light mode styles

Refactor bookmarks home stuff

* Add RecentBookmarksFeature to home

Move interactor to SessionControlInteractor

Clean up lint, styles, and dimens.

* Bookmarks use case tests for retrieving recently saved bookmarks. Linting.

* View holder tests

* Match ux to designs for colors, margins, and scrolling

* Clean up clean up

* Tests for the view bound feature

* Controller test

* Clean up: check state of store in feature tests; ellipsize textviews for bookmark item; remove unused attr; format

Co-authored-by: Jonathan Almeida <jalmeida@mozilla.com>
upstream-sync
Elise Richards 3 years ago committed by GitHub
parent ff9aa36885
commit 9bfe9b0787
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -43,4 +43,9 @@ object FeatureFlags {
* Enables recording of history metadata. * Enables recording of history metadata.
*/ */
val historyMetadataFeature = Config.channel.isDebug val historyMetadataFeature = Config.channel.isDebug
/**
* Enables the recently saved bookmarks feature in the home screen.
*/
val recentBookmarksFeature = Config.channel.isNightlyOrDebug
} }

@ -6,10 +6,11 @@ package org.mozilla.fenix.components.bookmarks
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.concept.storage.BookmarksStorage
/** /**
* Use cases that allow for modifying bookmarks. * Use cases that allow for modifying and retrieving bookmarks.
*/ */
class BookmarksUseCase(storage: BookmarksStorage) { class BookmarksUseCase(storage: BookmarksStorage) {
@ -38,5 +39,22 @@ class BookmarksUseCase(storage: BookmarksStorage) {
} }
} }
class RetrieveRecentBookmarksUseCase internal constructor(
private val storage: BookmarksStorage
) {
/**
* Retrieves a list of recently added bookmarks, if any, up to maximum.
*/
@WorkerThread
suspend operator fun invoke(count: Int = DEFAULT_BOOKMARKS_TO_RETRIEVE): List<BookmarkNode> {
return storage.getRecentBookmarks(count)
}
}
val addBookmark by lazy { AddBookmarksUseCase(storage) } val addBookmark by lazy { AddBookmarksUseCase(storage) }
val retrieveRecentBookmarks by lazy { RetrieveRecentBookmarksUseCase(storage) }
companion object {
const val DEFAULT_BOOKMARKS_TO_RETRIEVE = 4
}
} }

@ -113,6 +113,8 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
import org.mozilla.fenix.home.recentbookmarks.RecentBookmarksFeature
import org.mozilla.fenix.home.recentbookmarks.controller.DefaultRecentBookmarksController
import org.mozilla.fenix.home.recenttabs.controller.DefaultRecentTabsController import org.mozilla.fenix.home.recenttabs.controller.DefaultRecentTabsController
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.recenttabs.RecentTabsListFeature import org.mozilla.fenix.home.recenttabs.RecentTabsListFeature
@ -176,6 +178,7 @@ class HomeFragment : Fragment() {
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>() private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>() private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()
private val recentBookmarksFeature = ViewBoundFeatureWrapper<RecentBookmarksFeature>()
@VisibleForTesting @VisibleForTesting
internal var getMenuButton: () -> MenuButton? = { menuButton } internal var getMenuButton: () -> MenuButton? = { menuButton }
@ -232,6 +235,7 @@ class HomeFragment : Fragment() {
) )
).getTip() ).getTip()
}, },
recentBookmarks = emptyList(),
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome, showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome,
showSetAsDefaultBrowserCard = components.settings.shouldShowSetAsDefaultBrowserCard(), showSetAsDefaultBrowserCard = components.settings.shouldShowSetAsDefaultBrowserCard(),
recentTabs = components.core.store.state.asRecentTabs() recentTabs = components.core.store.state.asRecentTabs()
@ -260,6 +264,20 @@ class HomeFragment : Fragment() {
) )
} }
if (FeatureFlags.recentBookmarksFeature) {
recentBookmarksFeature.set(
feature = RecentBookmarksFeature(
homeStore = homeFragmentStore,
bookmarksUseCase = run {
requireContext().components.useCases.bookmarksUseCases
},
scope = viewLifecycleOwner.lifecycleScope
),
owner = viewLifecycleOwner,
view = view
)
}
_sessionControlInteractor = SessionControlInteractor( _sessionControlInteractor = SessionControlInteractor(
controller = DefaultSessionControlController( controller = DefaultSessionControlController(
activity = activity, activity = activity,
@ -284,6 +302,10 @@ class HomeFragment : Fragment() {
recentTabController = DefaultRecentTabsController( recentTabController = DefaultRecentTabsController(
selectTabUseCase = components.useCases.tabsUseCases.selectTab, selectTabUseCase = components.useCases.tabsUseCases.selectTab,
navController = findNavController() navController = findNavController()
),
recentBookmarksController = DefaultRecentBookmarksController(
activity = activity,
navController = findNavController()
) )
) )
@ -600,7 +622,8 @@ class HomeFragment : Fragment() {
).getTip() ).getTip()
}, },
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome, showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome,
recentTabs = components.core.store.state.asRecentTabs() recentTabs = components.core.store.state.asRecentTabs(),
recentBookmarks = emptyList()
) )
) )

@ -5,6 +5,7 @@
package org.mozilla.fenix.home package org.mozilla.fenix.home
import android.graphics.Bitmap import android.graphics.Bitmap
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
@ -45,6 +46,7 @@ data class Tab(
* @property tip The current [Tip] to show on 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 showCollectionPlaceholder If true, shows a placeholder when there are no collections.
* @property recentTabs The list of recent [TabSessionState] in the [HomeFragment]. * @property recentTabs The list of recent [TabSessionState] in the [HomeFragment].
* @property recentBookmarks The list of recently saved [BookmarkNode]s to show on the [HomeFragment].
*/ */
data class HomeFragmentState( data class HomeFragmentState(
val collections: List<TabCollection> = emptyList(), val collections: List<TabCollection> = emptyList(),
@ -54,7 +56,8 @@ data class HomeFragmentState(
val tip: Tip? = null, val tip: Tip? = null,
val showCollectionPlaceholder: Boolean = false, val showCollectionPlaceholder: Boolean = false,
val showSetAsDefaultBrowserCard: Boolean = false, val showSetAsDefaultBrowserCard: Boolean = false,
val recentTabs: List<TabSessionState> = emptyList() val recentTabs: List<TabSessionState> = emptyList(),
val recentBookmarks: List<BookmarkNode> = emptyList()
) : State ) : State
sealed class HomeFragmentAction : Action { sealed class HomeFragmentAction : Action {
@ -64,7 +67,8 @@ sealed class HomeFragmentAction : Action {
val collections: List<TabCollection>, val collections: List<TabCollection>,
val tip: Tip? = null, val tip: Tip? = null,
val showCollectionPlaceholder: Boolean, val showCollectionPlaceholder: Boolean,
val recentTabs: List<TabSessionState> val recentTabs: List<TabSessionState>,
val recentBookmarks: List<BookmarkNode>
) : ) :
HomeFragmentAction() HomeFragmentAction()
@ -76,6 +80,7 @@ sealed class HomeFragmentAction : Action {
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction() data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction() data class RemoveTip(val tip: Tip) : HomeFragmentAction()
data class RecentTabsChange(val recentTabs: List<TabSessionState>) : HomeFragmentAction() data class RecentTabsChange(val recentTabs: List<TabSessionState>) : HomeFragmentAction()
data class RecentBookmarksChange(val recentBookmarks: List<BookmarkNode>) : HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction() object RemoveCollectionsPlaceholder : HomeFragmentAction()
object RemoveSetDefaultBrowserCard : HomeFragmentAction() object RemoveSetDefaultBrowserCard : HomeFragmentAction()
} }
@ -114,5 +119,6 @@ private fun homeFragmentStateReducer(
} }
is HomeFragmentAction.RemoveSetDefaultBrowserCard -> state.copy(showSetAsDefaultBrowserCard = false) is HomeFragmentAction.RemoveSetDefaultBrowserCard -> state.copy(showSetAsDefaultBrowserCard = false)
is HomeFragmentAction.RecentTabsChange -> state.copy(recentTabs = action.recentTabs) is HomeFragmentAction.RecentTabsChange -> state.copy(recentTabs = action.recentTabs)
is HomeFragmentAction.RecentBookmarksChange -> state.copy(recentBookmarks = action.recentBookmarks)
} }
} }

@ -0,0 +1,44 @@
/* 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.recentbookmarks
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentStore
/**
* View-bound feature that retrieves a list of recently added [BookmarkNode]s and dispatches
* updates to the [HomeFragmentStore].
*
* @param homeStore the [HomeFragmentStore]
* @param bookmarksUseCase the [BookmarksUseCase] for retrieving the list of recently saved
* bookmarks from storage.
* @param scope the [CoroutineScope] used to fetch the bookmarks list
*/
class RecentBookmarksFeature(
private val homeStore: HomeFragmentStore,
private val bookmarksUseCase: BookmarksUseCase,
private val scope: CoroutineScope
) : LifecycleAwareFeature {
internal var job: Job? = null
override fun start() {
job = scope.launch(Dispatchers.IO) {
val bookmarks = bookmarksUseCase.retrieveRecentBookmarks()
homeStore.dispatch(HomeFragmentAction.RecentBookmarksChange(bookmarks))
}
}
override fun stop() {
job?.cancel()
}
}

@ -0,0 +1,44 @@
/* 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.recentbookmarks
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
import org.mozilla.fenix.home.recentbookmarks.view.RecentBookmarkItemViewHolder
/**
* Adapter for binding individual bookmark items for the homescreen.
*
* @param interactor The [RecentBookmarksInteractor] to be passed into the view.
*/
class RecentBookmarksItemAdapter(
private val interactor: RecentBookmarksInteractor
) : ListAdapter<BookmarkNode, RecentBookmarkItemViewHolder>(RecentBookmarkItemDiffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecentBookmarkItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(RecentBookmarkItemViewHolder.LAYOUT_ID, parent, false)
return RecentBookmarkItemViewHolder(view, interactor)
}
override fun onBindViewHolder(holder: RecentBookmarkItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
internal object RecentBookmarkItemDiffCallback : DiffUtil.ItemCallback<BookmarkNode>() {
override fun areItemsTheSame(oldItem: BookmarkNode, newItem: BookmarkNode) =
oldItem.guid == newItem.guid
override fun areContentsTheSame(oldItem: BookmarkNode, newItem: BookmarkNode) =
oldItem == newItem
}
}

@ -0,0 +1,54 @@
/* 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.recentbookmarks.controller
import androidx.navigation.NavController
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
/**
* An interface that handles the view manipulation of the recently saved bookmarks on the
* Home screen.
*/
interface RecentBookmarksController {
/**
* @see [RecentBookmarksInteractor.onRecentBookmarkClicked]
*/
fun handleBookmarkClicked(bookmark: BookmarkNode)
/**
* @see [RecentBookmarksInteractor.onShowAllBookmarksClicked]
*/
fun handleShowAllBookmarksClicked()
}
/**
* The default implementation of [RecentBookmarksController].
*/
class DefaultRecentBookmarksController(
private val activity: HomeActivity,
private val navController: NavController
) : RecentBookmarksController {
override fun handleBookmarkClicked(bookmark: BookmarkNode) {
activity.openToBrowserAndLoad(
searchTermOrURL = bookmark.url!!,
newTab = true,
from = BrowserDirection.FromHome
)
}
override fun handleShowAllBookmarksClicked() {
val directions = HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
navController.nav(R.id.homeFragment, directions)
}
}

@ -0,0 +1,28 @@
/* 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.recentbookmarks.interactor
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
/**
* Interface for recently saved bookmark related actions in the [SessionControlInteractor].
*/
interface RecentBookmarksInteractor {
/**
* Opens the given bookmark in a new tab. Called when an user clicks on a recently saved
* bookmark on the home screen.
*
* @param bookmark The bookmark that will be opened.
*/
fun onRecentBookmarkClicked(bookmark: BookmarkNode)
/**
* Navigates to bookmark list. Called when an user clicks on the "Show all" button for
* recently saved bookmarks on the home screen.
*/
fun onShowAllBookmarksClicked()
}

@ -0,0 +1,41 @@
/* 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.recentbookmarks.view
import android.view.View
import kotlinx.android.synthetic.main.recent_bookmark_item.bookmark_title
import kotlinx.android.synthetic.main.recent_bookmark_item.bookmark_subtitle
import kotlinx.android.synthetic.main.recent_bookmark_item.bookmark_item
import kotlinx.android.synthetic.main.recent_bookmark_item.favicon_image
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
import org.mozilla.fenix.utils.view.ViewHolder
class RecentBookmarkItemViewHolder(
private val view: View,
private val interactor: RecentBookmarksInteractor
) : ViewHolder(view) {
fun bind(bookmark: BookmarkNode) {
bookmark_title.text = bookmark.title ?: bookmark.url
bookmark_subtitle.text = bookmark.url?.tryGetHostFromUrl() ?: bookmark.title ?: ""
bookmark_item.setOnClickListener {
interactor.onRecentBookmarkClicked(bookmark)
}
bookmark.url?.let {
view.context.components.core.icons.loadIntoView(favicon_image, it)
}
}
companion object {
const val LAYOUT_ID = R.layout.recent_bookmark_item
}
}

@ -0,0 +1,45 @@
/* 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.recentbookmarks.view
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
import kotlinx.android.synthetic.main.component_recent_bookmarks.view.*
import kotlinx.android.synthetic.main.recent_bookmarks_header.*
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.R
import org.mozilla.fenix.home.recentbookmarks.RecentBookmarksItemAdapter
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
import org.mozilla.fenix.utils.view.ViewHolder
class RecentBookmarksViewHolder(
view: View,
val interactor: RecentBookmarksInteractor
) : ViewHolder(view) {
private val recentBookmarksAdapter = RecentBookmarksItemAdapter(interactor)
init {
val linearLayoutManager = LinearLayoutManager(view.context, HORIZONTAL, false)
view.recent_bookmarks_list.apply {
adapter = recentBookmarksAdapter
layoutManager = linearLayoutManager
}
showAllBookmarksButton.setOnClickListener {
interactor.onShowAllBookmarksClicked()
}
}
fun bind(bookmarks: List<BookmarkNode>) {
recentBookmarksAdapter.submitList(bookmarks)
}
companion object {
const val LAYOUT_ID = R.layout.component_recent_bookmarks
}
}

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import mozilla.components.ui.widgets.WidgetSiteItemView import mozilla.components.ui.widgets.WidgetSiteItemView
@ -39,6 +40,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTr
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingWhatsNewViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingWhatsNewViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabViewHolder import org.mozilla.fenix.home.recenttabs.view.RecentTabViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabsHeaderViewHolder import org.mozilla.fenix.home.recenttabs.view.RecentTabsHeaderViewHolder
import org.mozilla.fenix.home.recentbookmarks.view.RecentBookmarksViewHolder
import org.mozilla.fenix.home.tips.ButtonTipViewHolder import org.mozilla.fenix.home.tips.ButtonTipViewHolder
import mozilla.components.feature.tab.collections.Tab as ComponentTab import mozilla.components.feature.tab.collections.Tab as ComponentTab
@ -151,6 +153,31 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
} }
} }
data class RecentBookmarks(val recentBookmarks: List<BookmarkNode>) :
AdapterItem(RecentBookmarksViewHolder.LAYOUT_ID) {
override fun sameAs(other: AdapterItem): Boolean {
val newBookmarks = (other as? RecentBookmarks) ?: return false
if (newBookmarks.recentBookmarks.size != this.recentBookmarks.size) {
return false
}
return recentBookmarks.zip(newBookmarks.recentBookmarks).all { (new, old) ->
new.guid == old.guid
}
}
override fun contentsSameAs(other: AdapterItem): Boolean {
val newBookmarks = (other as? RecentBookmarks) ?: return false
val newBookmarksSequence = newBookmarks.recentBookmarks.asSequence()
val oldBookmarksList = this.recentBookmarks.asSequence()
return newBookmarksSequence.zip(oldBookmarksList).all { (new, old) ->
new == old
}
}
}
/** /**
* True if this item represents the same value as other. Used by [AdapterItemDiffCallback]. * True if this item represents the same value as other. Used by [AdapterItemDiffCallback].
*/ */
@ -233,6 +260,10 @@ class SessionControlAdapter(
ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor) ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor)
RecentTabsHeaderViewHolder.LAYOUT_ID -> RecentTabsHeaderViewHolder(view, interactor) RecentTabsHeaderViewHolder.LAYOUT_ID -> RecentTabsHeaderViewHolder(view, interactor)
RecentTabViewHolder.LAYOUT_ID -> RecentTabViewHolder(view, interactor) RecentTabViewHolder.LAYOUT_ID -> RecentTabViewHolder(view, interactor)
RecentBookmarksViewHolder.LAYOUT_ID -> {
RecentBookmarksViewHolder(view, interactor)
}
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
@ -287,6 +318,11 @@ class SessionControlAdapter(
is RecentTabViewHolder -> { is RecentTabViewHolder -> {
holder.bindTab((item as AdapterItem.RecentTabItem).tab) holder.bindTab((item as AdapterItem.RecentTabItem).tab)
} }
is RecentBookmarksViewHolder -> {
holder.bind(
(item as AdapterItem.RecentBookmarks).recentBookmarks
)
}
} }
} }
} }

@ -4,10 +4,13 @@
package org.mozilla.fenix.home.sessioncontrol package org.mozilla.fenix.home.sessioncontrol
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.feature.tab.collections.Tab import mozilla.components.feature.tab.collections.Tab
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController
import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
@ -205,17 +208,19 @@ interface ExperimentCardInteractor {
} }
/** /**
* Interactor for the Home screen. * Interactor for the Home screen. Provides implementations for the CollectionInteractor,
* Provides implementations for the CollectionInteractor, OnboardingInteractor, TopSiteInteractor, * OnboardingInteractor, TopSiteInteractor, TipInteractor, TabSessionInteractor,
* TipInteractor, TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor, and * ToolbarInteractor, ExperimentCardInteractor, RecentTabInteractor, and RecentBookmarksInteractor.
* RecentTabInteractor.
*/ */
@SuppressWarnings("TooManyFunctions") @SuppressWarnings("TooManyFunctions")
class SessionControlInteractor( class SessionControlInteractor(
private val controller: SessionControlController, private val controller: SessionControlController,
private val recentTabController: RecentTabController private val recentTabController: RecentTabController,
private val recentBookmarksController: RecentBookmarksController
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor, ) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor,
TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor, RecentTabInteractor { TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor, RecentTabInteractor,
RecentBookmarksInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) { override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection) controller.handleCollectionAddTabTapped(collection)
} }
@ -327,4 +332,18 @@ class SessionControlInteractor(
override fun onRecentTabShowAllClicked() { override fun onRecentTabShowAllClicked() {
recentTabController.handleRecentTabShowAllClicked() recentTabController.handleRecentTabShowAllClicked()
} }
/**
* See [RecentBookmarksInteractor.onRecentBookmarkClicked].
*/
override fun onRecentBookmarkClicked(bookmark: BookmarkNode) {
recentBookmarksController.handleBookmarkClicked(bookmark)
}
/**
* See [RecentBookmarksInteractor.onShowAllBookmarksClicked].
*/
override fun onShowAllBookmarksClicked() {
recentBookmarksController.handleShowAllBookmarksClicked()
}
} }

@ -10,6 +10,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
@ -28,6 +29,7 @@ private fun normalModeAdapterItems(
collections: List<TabCollection>, collections: List<TabCollection>,
expandedCollections: Set<Long>, expandedCollections: Set<Long>,
tip: Tip?, tip: Tip?,
recentBookmarks: List<BookmarkNode>,
showCollectionsPlaceholder: Boolean, showCollectionsPlaceholder: Boolean,
showSetAsDefaultBrowserCard: Boolean, showSetAsDefaultBrowserCard: Boolean,
recentTabs: List<TabSessionState> recentTabs: List<TabSessionState>
@ -48,6 +50,10 @@ private fun normalModeAdapterItems(
showRecentTabs(recentTabs, items) showRecentTabs(recentTabs, items)
} }
if (recentBookmarks.isNotEmpty()) {
items.add(AdapterItem.RecentBookmarks(recentBookmarks))
}
if (collections.isEmpty()) { if (collections.isEmpty()) {
if (showCollectionsPlaceholder) { if (showCollectionsPlaceholder) {
items.add(AdapterItem.NoCollectionsMessage) items.add(AdapterItem.NoCollectionsMessage)
@ -131,6 +137,7 @@ private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
collections, collections,
expandedCollections, expandedCollections,
tip, tip,
recentBookmarks,
showCollectionPlaceholder, showCollectionPlaceholder,
showSetAsDefaultBrowserCard, showSetAsDefaultBrowserCard,
recentTabs recentTabs
@ -174,7 +181,6 @@ class SessionControlView(
} }
fun update(state: HomeFragmentState) { fun update(state: HomeFragmentState) {
val stateAdapterList = state.toAdapterList() val stateAdapterList = state.toAdapterList()
if (homeScreenViewModel.shouldScrollToTopSites) { if (homeScreenViewModel.shouldScrollToTopSites) {
sessionControlAdapter.submitList(stateAdapterList) { sessionControlAdapter.submitList(stateAdapterList) {

@ -44,7 +44,7 @@ class TabInCollectionViewHolder(
} }
// This needs to match the elevation of the CollectionViewHolder for the shadow // This needs to match the elevation of the CollectionViewHolder for the shadow
view.elevation = view.resources.getDimension(R.dimen.home_collection_elevation) view.elevation = view.resources.getDimension(R.dimen.home_item_elevation)
view.setOnClickListener { view.setOnClickListener {
interactor.onCollectionOpenTabClicked(tab) interactor.onCollectionOpenTabClicked(tab)

@ -1,8 +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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/photonDarkGrey05" />
<item android:state_checked="false" android:color="@color/photonLightGrey50" />
</selector>

@ -1,8 +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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/photonDarkGrey05" />
<item android:state_checked="false" android:color="@color/photonLightGrey05" />
</selector>

@ -1,8 +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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/photonDarkGrey90" />
<item android:state_checked="false" android:color="@color/photonLightGrey30" />
</selector>

@ -11,7 +11,7 @@
android:background="@drawable/home_list_row_background" android:background="@drawable/home_list_row_background"
android:clickable="true" android:clickable="true"
android:clipToPadding="false" android:clipToPadding="false"
android:elevation="@dimen/home_collection_elevation" android:elevation="@dimen/home_item_elevation"
android:focusable="true" android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"> android:foreground="?android:attr/selectableItemBackground">

@ -3,28 +3,19 @@
- License, v. 2.0. If a copy of the MPL was not distributed with this - 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, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:isScrollContainer="true" android:orientation="vertical">
android:gravity="start">
<include layout="@layout/recent_bookmarks_header" /> <include layout="@layout/recent_bookmarks_header" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recent_bookmarks_list" android:id="@+id/recent_bookmarks_list"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:minWidth="448dp" android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:clipChildren="false"
android:clipToPadding="false"
android:overScrollMode="never"
android:nestedScrollingEnabled="false"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:spanCount="4" tools:listitem="@layout/recent_bookmark_item"
tools:listitem="@layout/recent_bookmark_item" /> tools:spanCount="4" />
</LinearLayout> </LinearLayout>

@ -2,56 +2,64 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public <!-- 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 - 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, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bookmark_item" android:id="@+id/bookmark_item"
android:layout_width="@dimen/recent_bookmark_item_width" android:layout_width="@dimen/recent_bookmark_item_width"
android:layout_height="@dimen/recent_bookmark_item_height" android:layout_height="@dimen/recent_bookmark_item_height"
android:orientation="vertical"> android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:scrollbars="none"
android:nestedScrollingEnabled="false"
android:importantForAccessibility="no"
style="@style/RecentBookmarks.FaviconCard">
<com.google.android.material.card.MaterialCardView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/favicon_card" android:id="@+id/favicon_image"
style="@style/RecentBookmarks.FaviconCard" style="@style/recentBookmarkFavicon"
android:importantForAccessibility="noHideDescendants" android:layout_width="24dp"
app:layout_constraintStart_toStartOf="parent" android:layout_height="24dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="24dp"
app:layout_constraintTop_toTopOf="parent" android:layout_gravity="center_horizontal" />
app:layout_constraintBottom_toBottomOf="parent" >
<com.google.android.material.imageview.ShapeableImageView <View
android:id="@+id/favicon_image" android:layout_width="match_parent"
style="@style/recentBookmarkFavicon" android:layout_height="1dp"
app:layout_constraintTop_toTopOf="parent" android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintStart_toStartOf="parent" android:importantForAccessibility="no"
app:layout_constraintEnd_toEndOf="parent" /> android:layout_marginTop="72dp"
android:clickable="false"/>
<TextView <TextView
android:id="@+id/bookmark_title" android:id="@+id/bookmark_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/recent_bookmark_item_title_height" android:layout_height="30dp"
android:maxHeight="@dimen/recent_bookmark_item_title_height" android:layout_marginTop="72dp"
android:layout_marginTop="@dimen/recent_bookmark_item_favicon_height" android:maxHeight="30dp"
android:paddingTop="@dimen/recent_bookmark_item_title_padding_top" android:paddingStart="16dp"
android:paddingStart="@dimen/recent_bookmark_item_padding" android:paddingTop="7dp"
android:paddingEnd="@dimen/recent_bookmark_item_padding" android:paddingEnd="16dp"
android:textAppearance="@style/recentBookmarkItemTitleText" android:scrollbars="none"
app:layout_constraintStart_toStartOf="parent" android:ellipsize="end"
app:layout_constraintEnd_toEndOf="parent" android:maxLines="1"
app:layout_constraintTop_toBottomOf="@id/favicon_image" android:nestedScrollingEnabled="false"
tools:text="Recently Saved bookmark item" /> android:importantForAccessibility="yes"
android:textAppearance="@style/recentBookmarkItemTitleText"
tools:text="Recently Saved bookmark item" />
<TextView <TextView
android:id="@+id/bookmark_subtitle" android:id="@+id/bookmark_subtitle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/recent_bookmark_item_subtitle_height" android:layout_height="16dp"
android:layout_marginTop="@dimen/recent_bookmark_item_subtitle_margin_top" android:layout_marginTop="105dp"
android:paddingStart="@dimen/recent_bookmark_item_padding" android:paddingStart="16dp"
android:paddingEnd="@dimen/recent_bookmark_item_padding" android:paddingEnd="16dp"
android:textAppearance="@style/recentBookmarkItemSubTitleText" android:ellipsize="end"
app:layout_constraintBottom_toBottomOf="@id/bookmark_title" android:maxLines="1"
app:layout_constraintStart_toStartOf="parent" android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent" android:nestedScrollingEnabled="false"
tools:text="Subtitle text" /> android:textAppearance="@style/recentBookmarkItemSubTitleText"
</com.google.android.material.card.MaterialCardView> tools:text="Subtitle text"
</androidx.constraintlayout.widget.ConstraintLayout> android:textColor="@color/home_recent_bookmarks_item_url" />
</com.google.android.material.card.MaterialCardView>

@ -1,39 +1,42 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public <!-- 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 - 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, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" > android:layout_height="wrap_content"
android:layout_marginTop="40dp">
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
style="@style/Header20TextStyle"
android:id="@+id/recentlySavedBookmarksHeader" android:id="@+id/recentlySavedBookmarksHeader"
style="@style/Header20TextStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/recently_saved_bookmarks_content_description" android:contentDescription="@string/recently_saved_bookmarks_content_description"
android:maxLines="1"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@string/recently_saved_bookmarks" android:text="@string/recently_saved_bookmarks"
android:layout_marginTop="@dimen/home_recently_saved_padding_top"
android:paddingStart="@dimen/home_recently_saved_padding_start"
android:paddingBottom="@dimen/home_recently_saved_padding_bottom"
android:maxLines="2"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/showAllBookmarksButton" android:id="@+id/showAllBookmarksButton"
style="@style/Button12TextStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/Button12TextStyle"
android:contentDescription="@string/recently_saved_show_all_content_description"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/recently_saved_show_all_content_description"
android:padding="16dp"
android:text="@string/recently_saved_show_all" android:text="@string/recently_saved_show_all"
android:paddingStart="@dimen/home_recently_saved_padding_start" android:textColor="@color/home_show_all_button_text"
android:paddingEnd="@dimen/home_recently_saved_padding_end" android:maxLines="1"
android:paddingTop="@dimen/home_show_all_padding_top" android:scrollbars="none"
android:paddingBottom="@dimen/home_show_all_padding_bottom" android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="@id/recentlySavedBookmarksHeader"
app:layout_constraintBottom_toBottomOf="@id/recentlySavedBookmarksHeader" /> app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -10,7 +10,7 @@
android:layout_height="48dp" android:layout_height="48dp"
android:background="@drawable/home_list_row_background" android:background="@drawable/home_list_row_background"
android:clipToPadding="false" android:clipToPadding="false"
android:elevation="@dimen/home_collection_elevation" android:elevation="@dimen/home_item_elevation"
android:foreground="?android:attr/selectableItemBackground"> android:foreground="?android:attr/selectableItemBackground">
<ImageView <ImageView

@ -97,6 +97,7 @@
<!-- Home screen --> <!-- Home screen -->
<color name="home_show_all_button_text">@color/photonLightGrey50</color> <color name="home_show_all_button_text">@color/photonLightGrey50</color>
<color name="home_recent_bookmarks_item_url">@color/photonLightGrey50</color>
<!-- Search Widget --> <!-- Search Widget -->
<color name="search_widget_background">@color/inset_dark_theme</color> <color name="search_widget_background">@color/inset_dark_theme</color>

@ -302,6 +302,7 @@
<!-- Home screen --> <!-- Home screen -->
<color name="home_show_all_button_text">@color/photonDarkGrey05</color> <color name="home_show_all_button_text">@color/photonDarkGrey05</color>
<color name="home_recent_bookmarks_item_url">@color/photonDarkGrey05</color>
<!-- Library buttons --> <!-- Library buttons -->
<color name="library_sessions_icon_background">#B9F0FD</color> <color name="library_sessions_icon_background">#B9F0FD</color>

@ -95,26 +95,12 @@
<!-- Home Fragment --> <!-- Home Fragment -->
<dimen name="home_fragment_top_toolbar_header_margin">60dp</dimen> <dimen name="home_fragment_top_toolbar_header_margin">60dp</dimen>
<dimen name="home_collection_elevation">5dp</dimen> <dimen name="home_item_elevation">5dp</dimen>
<dimen name="home_recently_saved_padding_top">40dp</dimen>
<dimen name="home_recently_saved_padding_bottom">16dp</dimen>
<dimen name="home_recently_saved_padding_start">16dp</dimen>
<dimen name="home_recently_saved_padding_end">16dp</dimen>
<dimen name="home_show_all_padding_top">16dp</dimen>
<dimen name="home_show_all_padding_bottom">16dp</dimen>
<!-- Home - Recently saved bookmarks --> <!-- Home - Recently saved bookmarks -->
<dimen name="recent_bookmark_item_height">128dp</dimen> <dimen name="recent_bookmark_item_height">128dp</dimen>
<dimen name="recent_bookmark_item_width">152dp</dimen> <dimen name="recent_bookmark_item_width">152dp</dimen>
<dimen name="recent_bookmark_item_padding">16dp</dimen>
<dimen name="recent_bookmark_item_card_radius">8dp</dimen>
<dimen name="recent_bookmark_item_card_elevation">1dp</dimen>
<dimen name="recent_bookmark_item_favicon_height">72dp</dimen> <dimen name="recent_bookmark_item_favicon_height">72dp</dimen>
<dimen name="recent_bookmark_item_favicon_corner_size">4dp</dimen>
<dimen name="recent_bookmark_item_title_height">30dp</dimen>
<dimen name="recent_bookmark_item_title_padding_top">7dp</dimen>
<dimen name="recent_bookmark_item_subtitle_height">16dp</dimen>
<dimen name="recent_bookmark_item_subtitle_margin_top">105dp</dimen>
<!-- Browser Fragment --> <!-- Browser Fragment -->
<!--The size of the gap between the tab preview and content layout.--> <!--The size of the gap between the tab preview and content layout.-->

@ -404,13 +404,12 @@
</style> </style>
<style name="Body12TextStyle" parent="TextAppearance.MaterialComponents.Body1"> <style name="Body12TextStyle" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textColor">@color/primary_state_title_text_color</item> <item name="android:textColor">?primaryText</item>
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
<item name="android:textAllCaps">false</item> <item name="android:textAllCaps">false</item>
</style> </style>
<style name="Button12TextStyle" parent="TextAppearance.MaterialComponents.Button"> <style name="Button12TextStyle" parent="TextAppearance.MaterialComponents.Button">
<item name="android:textColor">@color/primary_state_button_text_color</item>
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
<item name="fontFamily">@font/metropolis</item> <item name="fontFamily">@font/metropolis</item>
<item name="android:textAllCaps">false</item> <item name="android:textAllCaps">false</item>
@ -656,10 +655,10 @@
<style name="RecentBookmarks.FaviconCard" parent="Mozac.Widgets.Favicon"> <style name="RecentBookmarks.FaviconCard" parent="Mozac.Widgets.Favicon">
<item name="android:layout_width">@dimen/recent_bookmark_item_width</item> <item name="android:layout_width">@dimen/recent_bookmark_item_width</item>
<item name="android:layout_height">@dimen/recent_bookmark_item_height</item> <item name="android:layout_height">@dimen/recent_bookmark_item_height</item>
<item name="android:padding">@dimen/recent_bookmark_item_padding</item> <item name="android:padding">16dp</item>
<item name="cardBackgroundColor">?mozac_widget_favicon_background_color</item> <item name="cardBackgroundColor">?mozac_widget_favicon_background_color</item>
<item name="cardCornerRadius">@dimen/recent_bookmark_item_card_radius</item> <item name="cardCornerRadius">8dp</item>
<item name="cardElevation">@dimen/recent_bookmark_item_card_elevation</item> <item name="cardElevation">@dimen/home_item_elevation</item>
</style> </style>
<style name="recentBookmarkFavicon"> <style name="recentBookmarkFavicon">
@ -672,21 +671,22 @@
<style name="recentBookmarkFaviconShape"> <style name="recentBookmarkFaviconShape">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="elevation">@dimen/recent_bookmark_item_card_elevation</item> <item name="elevation">1dp</item>
<item name="cornerSize">@dimen/recent_bookmark_item_favicon_corner_size</item> <item name="cornerSize">4dp</item>
</style> </style>
<style name="recentBookmarkItemTitleText" parent="Body16TextStyle"> <style name="recentBookmarkItemTitleText" parent="Body16TextStyle">
<item name="android:gravity">start</item> <item name="android:gravity">start</item>
<item name="android:textAlignment">gravity</item> <item name="android:textAlignment">gravity</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
<item name="android:textColor">@color/recent_bookmark_item_text_color</item> <item name="android:textColor">?primaryText</item>
<item name="android:ellipsize">end</item> <item name="android:ellipsize">end</item>
<item name="android:maxLines">1</item> <item name="android:maxLines">1</item>
</style> </style>
<style name="recentBookmarkItemSubTitleText" parent="Body12TextStyle"> <style name="recentBookmarkItemSubTitleText" parent="Body12TextStyle">
<item name="android:gravity">start</item> <item name="android:gravity">start</item>
<item name="android:textColor">?primaryText</item>
<item name="android:textAlignment">gravity</item> <item name="android:textAlignment">gravity</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
<item name="android:ellipsize">end</item> <item name="android:ellipsize">end</item>

@ -13,6 +13,7 @@ import kotlinx.coroutines.test.runBlockingTest
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.concept.storage.BookmarksStorage
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
@ -47,4 +48,33 @@ class BookmarksUseCaseTest {
coVerify { storage.addItem(BookmarkRoot.Mobile.id, "https://mozilla.org", "Mozilla", null) } coVerify { storage.addItem(BookmarkRoot.Mobile.id, "https://mozilla.org", "Mozilla", null) }
} }
@Test
fun `WHEN recently saved bookmarks exist THEN retrieve the list from storage`() = runBlockingTest {
val storage = mockk<BookmarksStorage>(relaxed = true)
val useCase = BookmarksUseCase(storage)
val bookmarkNode = mockk<BookmarkNode>()
coEvery { storage.getRecentBookmarks(any()) }.coAnswers { listOf(bookmarkNode) }
val result = useCase.retrieveRecentBookmarks()
assertEquals(listOf(bookmarkNode), result)
coVerify { storage.getRecentBookmarks(BookmarksUseCase.DEFAULT_BOOKMARKS_TO_RETRIEVE) }
}
@Test
fun `WHEN there are no recently saved bookmarks THEN retrieve the empty list from storage`() = runBlockingTest {
val storage = mockk<BookmarksStorage>(relaxed = true)
val useCase = BookmarksUseCase(storage)
coEvery { storage.getRecentBookmarks(any()) }.coAnswers { listOf() }
val result = useCase.retrieveRecentBookmarks()
assertEquals(listOf<BookmarkNode>(), result)
coVerify { storage.getRecentBookmarks(BookmarksUseCase.DEFAULT_BOOKMARKS_TO_RETRIEVE) }
}
} }

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

@ -153,7 +153,8 @@ class HomeFragmentStoreTest {
mode = Mode.Private, mode = Mode.Private,
topSites = topSites, topSites = topSites,
showCollectionPlaceholder = true, showCollectionPlaceholder = true,
recentTabs = recentTabs recentTabs = recentTabs,
recentBookmarks = emptyList()
) )
).join() ).join()

@ -6,10 +6,13 @@ package org.mozilla.fenix.home
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.feature.tab.collections.Tab import mozilla.components.feature.tab.collections.Tab
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController
import org.mozilla.fenix.home.recenttabs.controller.RecentTabController import org.mozilla.fenix.home.recenttabs.controller.RecentTabController
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
@ -18,12 +21,17 @@ class SessionControlInteractorTest {
private val controller: DefaultSessionControlController = mockk(relaxed = true) private val controller: DefaultSessionControlController = mockk(relaxed = true)
private val recentTabController: RecentTabController = mockk(relaxed = true) private val recentTabController: RecentTabController = mockk(relaxed = true)
private val recentBookmarksController: RecentBookmarksController = mockk(relaxed = true)
private lateinit var interactor: SessionControlInteractor private lateinit var interactor: SessionControlInteractor
@Before @Before
fun setup() { fun setup() {
interactor = SessionControlInteractor(controller, recentTabController) interactor = SessionControlInteractor(
controller,
recentTabController,
recentBookmarksController
)
} }
@Test @Test
@ -143,4 +151,26 @@ class SessionControlInteractorTest {
interactor.onRecentTabShowAllClicked() interactor.onRecentTabShowAllClicked()
verify { recentTabController.handleRecentTabShowAllClicked() } verify { recentTabController.handleRecentTabShowAllClicked() }
} }
@Test
fun `WHEN a recently saved bookmark is clicked THEN the selected bookmark is handled`() {
val bookmark = BookmarkNode(
type = BookmarkNodeType.ITEM,
guid = "guid#${Math.random() * 1000}",
parentGuid = null,
position = null,
title = null,
url = null,
children = null
)
interactor.onRecentBookmarkClicked(bookmark)
verify { recentBookmarksController.handleBookmarkClicked(bookmark) }
}
@Test
fun `WHEN Show All recently saved bookmarks button is clicked THEN the click is handled`() {
interactor.onShowAllBookmarksClicked()
verify { recentBookmarksController.handleShowAllBookmarksClicked() }
}
} }

@ -0,0 +1,91 @@
/* 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.recentbookmarks
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.recentbookmarks.controller.DefaultRecentBookmarksController
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultRecentBookmarksControllerTest {
private val testDispatcher = TestCoroutineDispatcher()
@get:Rule
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
private val activity: HomeActivity = mockk(relaxed = true)
private val navController: NavController = mockk(relaxUnitFun = true)
private lateinit var controller: DefaultRecentBookmarksController
@Before
fun setup() {
every { activity.openToBrowserAndLoad(any(), any(), any()) } just Runs
every { navController.currentDestination } returns mockk {
every { id } returns R.id.homeFragment
}
controller = spyk(DefaultRecentBookmarksController(
activity = activity,
navController = navController
))
}
@After
fun cleanUp() {
testDispatcher.cleanupTestCoroutines()
}
@Test
fun `WHEN a recently saved bookmark is clicked THEN the selected bookmark is opened`() {
val bookmark = BookmarkNode(
type = BookmarkNodeType.ITEM,
guid = "guid#${Math.random() * 1000}",
parentGuid = null,
position = null,
title = null,
url = "https://www.example.com",
children = null
)
controller.handleBookmarkClicked(bookmark)
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = bookmark.url!!,
newTab = true,
from = BrowserDirection.FromHome
)
}
}
@Test
fun `WHEN show all recently saved bookmark is clicked THEN the bookmarks root is opened`() {
controller.handleShowAllBookmarksClicked()
val directions = HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
verify { navController.navigate(directions, any<NavOptions>()) }
}
}

@ -0,0 +1,108 @@
/* 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.recentbookmarks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.middleware.CaptureActionsMiddleware
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.HomeFragmentStore
@OptIn(ExperimentalCoroutinesApi::class)
class RecentBookmarksFeatureTest {
private val middleware = CaptureActionsMiddleware<HomeFragmentState, HomeFragmentAction>()
private val homeStore = HomeFragmentStore(middlewares = listOf(middleware))
private val bookmarksUseCases: BookmarksUseCase = mockk(relaxed = true)
private val scope = TestCoroutineScope()
private val testDispatcher = TestCoroutineDispatcher()
private val bookmark = BookmarkNode(
type = BookmarkNodeType.ITEM,
guid = "guid#${Math.random() * 1000}",
parentGuid = null,
position = null,
title = null,
url = "https://www.example.com",
children = null
)
@get:Rule
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
@Before
fun setup() {
coEvery { bookmarksUseCases.retrieveRecentBookmarks() }.coAnswers { listOf(bookmark) }
}
@After
fun cleanUp() {
scope.cleanupTestCoroutines()
testDispatcher.cleanupTestCoroutines()
}
@Test
fun `GIVEN no recent bookmarks WHEN feature starts THEN fetch bookmarks and notify store`() =
testDispatcher.runBlockingTest {
val feature = RecentBookmarksFeature(
homeStore,
bookmarksUseCases,
scope
)
feature.start()
assertEquals(emptyList<BookmarkNode>(), homeStore.state.recentBookmarks)
testDispatcher.advanceUntilIdle()
homeStore.waitUntilIdle()
coVerify {
bookmarksUseCases.retrieveRecentBookmarks()
}
middleware.assertLastAction(HomeFragmentAction.RecentBookmarksChange::class) {
assertEquals(listOf(bookmark), it.recentBookmarks)
}
}
@Test
fun `WHEN the feature is destroyed THEN the job is cancelled`() {
val feature = spyk(RecentBookmarksFeature(
homeStore,
bookmarksUseCases,
scope
))
assertNull(feature.job)
feature.start()
assertNotNull(feature.job)
feature.stop()
verify(exactly = 1) { feature.stop() }
}
}

@ -0,0 +1,93 @@
/* 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.recentbookmarks.view
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import kotlinx.android.synthetic.main.recent_bookmark_item.view.*
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert
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 RecentBookmarkItemViewHolderTest {
private lateinit var view: View
private lateinit var interactor: SessionControlInteractor
private val bookmarkNoUrl = BookmarkNode(
type = BookmarkNodeType.ITEM,
guid = "guid#${Math.random() * 1000}",
parentGuid = null,
position = null,
title = "Bookmark Title",
url = null,
children = null
)
private val bookmarkWithUrl = BookmarkNode(
type = BookmarkNodeType.ITEM,
guid = "guid#${Math.random() * 1000}",
parentGuid = null,
position = null,
title = "Other Bookmark Title",
url = "https://www.example.com",
children = null
)
private val bookmarkNoTitle = BookmarkNode(
type = BookmarkNodeType.ITEM,
guid = "guid#${Math.random() * 1000}",
parentGuid = null,
position = null,
title = null,
url = "https://www.github.com",
children = null
)
@Before
fun setup() {
view = LayoutInflater.from(testContext)
.inflate(RecentBookmarkItemViewHolder.LAYOUT_ID, null)
interactor = mockk(relaxed = true)
}
@Test
fun `GIVEN a bookmark exists in the list THEN set the title text and subtitle from item`() {
RecentBookmarkItemViewHolder(view, interactor).bind(bookmarkWithUrl)
val hostFromUrl = bookmarkWithUrl.url?.tryGetHostFromUrl()
Assert.assertEquals(bookmarkWithUrl.title, view.bookmark_title.text)
Assert.assertEquals(hostFromUrl, view.bookmark_subtitle.text)
}
@Test
fun `WHEN there is no url for the bookmark THEN do not load an icon `() {
val viewHolder = RecentBookmarkItemViewHolder(view, interactor)
Assert.assertNull(view.favicon_image.drawable)
viewHolder.bind(bookmarkNoUrl)
Assert.assertNull(view.favicon_image.drawable)
}
@Test
fun `WHEN a bookmark does not have a title THEN show the url`() {
RecentBookmarkItemViewHolder(view, interactor).bind(bookmarkNoTitle)
Assert.assertEquals(bookmarkNoTitle.url, view.bookmark_title.text)
}
}

@ -0,0 +1,52 @@
/* 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.recentbookmarks.view
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.recent_bookmarks_header.view.*
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
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 RecentBookmarksViewHolderTest {
private lateinit var view: View
private lateinit var interactor: SessionControlInteractor
private val bookmark = BookmarkNode(
type = BookmarkNodeType.ITEM,
guid = "guid#${Math.random() * 1000}",
parentGuid = null,
position = null,
title = null,
url = null,
children = null
)
@Before
fun setup() {
view = LayoutInflater.from(testContext)
.inflate(RecentBookmarksViewHolder.LAYOUT_ID, null)
interactor = mockk(relaxed = true)
}
@Test
fun `WHEN show all bookmarks button is clicked THEN interactor is called`() {
RecentBookmarksViewHolder(view, interactor).bind(listOf(bookmark))
view.showAllBookmarksButton.performClick()
verify { interactor.onShowAllBookmarksClicked() }
}
}
Loading…
Cancel
Save