For #2486 - Adds Recently Closed Tabs

pull/101/head
ekager 4 years ago committed by ekager
parent cce58e7d51
commit 09fbb43f80

@ -385,6 +385,7 @@ dependencies {
implementation Deps.mozilla_feature_site_permissions
implementation Deps.mozilla_feature_readerview
implementation Deps.mozilla_feature_tab_collections
implementation Deps.mozilla_feature_recentlyclosed
implementation Deps.mozilla_feature_top_sites
implementation Deps.mozilla_feature_share
implementation Deps.mozilla_feature_accounts_push

@ -32,5 +32,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabTray(R.id.tabTrayDialogFragment)
FromTabTray(R.id.tabTrayDialogFragment),
FromRecentlyClosed(R.id.recentlyClosedFragment)
}

@ -83,6 +83,7 @@ import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchFragmentDirections
@ -703,6 +704,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
}
/**

@ -915,14 +915,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
*/
protected open fun removeSessionIfNeeded(): Boolean {
getSessionById()?.let { session ->
val sessionManager = requireComponents.core.sessionManager
return if (session.source == SessionState.Source.ACTION_VIEW) {
activity?.finish()
sessionManager.remove(session)
requireComponents.useCases.tabsUseCases.removeTab(session)
true
} else {
if (session.hasParentSession) {
sessionManager.remove(session, true)
requireComponents.useCases.tabsUseCases.removeTab(session)
}
// We want to return to home if this session didn't have a parent session to select.
val goToOverview = !session.hasParentSession

@ -20,6 +20,7 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
@ -40,6 +41,7 @@ import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.readerview.ReaderViewMiddleware
import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware
import mozilla.components.feature.session.HistoryDelegate
import mozilla.components.feature.top.sites.DefaultTopSitesStorage
import mozilla.components.feature.top.sites.PinnedSiteStorage
@ -140,12 +142,15 @@ class Core(private val context: Context, private val crashReporter: CrashReporti
val store by lazy {
BrowserStore(
middleware = listOf(
RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine),
MediaMiddleware(context, MediaService::class.java),
DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(),
ThumbnailsMiddleware(thumbnailStorage)
) + EngineMiddleware.create(engine, ::findSessionById)
)
).also {
it.dispatch(RecentlyClosedAction.InitializeRecentlyClosedState)
}
}
private fun findSessionById(tabId: String): Session? {
@ -344,7 +349,7 @@ class Core(private val context: Context, private val crashReporter: CrashReporti
fun getPreferredColorScheme(): PreferredColorScheme {
val inDark =
(context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
Configuration.UI_MODE_NIGHT_YES
return when {
context.settings().shouldUseDarkTheme -> PreferredColorScheme.Dark
context.settings().shouldUseLightTheme -> PreferredColorScheme.Light
@ -357,5 +362,6 @@ class Core(private val context: Context, private val crashReporter: CrashReporti
private const val KEY_STRENGTH = 256
private const val KEY_STORAGE_NAME = "core_prefs"
private const val PASSWORDS_KEY = "passwords"
private const val RECENTLY_CLOSED_MAX = 5
}
}

@ -477,7 +477,7 @@ class HomeFragment : Fragment() {
.let { SessionManager.Snapshot(it, selectedIndex) }
tabs.forEach {
sessionManager.remove(it)
requireComponents.useCases.tabsUseCases.removeTab(it)
}
val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
@ -505,7 +505,7 @@ class HomeFragment : Fragment() {
val isSelected =
session.id == requireComponents.core.store.state.selectedTabId ?: false
sessionManager.remove(session)
requireComponents.useCases.tabsUseCases.removeTab(sessionId)
val snackbarMessage = if (snapshot.session.private) {
requireContext().getString(R.string.snackbar_private_tab_closed)

@ -27,9 +27,9 @@ enum class HistoryItemTimeGroup {
}
}
class HistoryAdapter(
private val historyInteractor: HistoryInteractor
) : PagedListAdapter<HistoryItem, HistoryListItemViewHolder>(historyDiffCallback), SelectionHolder<HistoryItem> {
class HistoryAdapter(private val historyInteractor: HistoryInteractor) :
PagedListAdapter<HistoryItem, HistoryListItemViewHolder>(historyDiffCallback),
SelectionHolder<HistoryItem> {
private var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal
override val selectedItems get() = mode.selectedItems

@ -8,6 +8,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.res.Resources
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.prompt.ShareData
@ -15,6 +16,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
@Suppress("TooManyFunctions")
interface HistoryController {
fun handleOpen(item: HistoryItem, mode: BrowsingMode? = null)
fun handleSelect(item: HistoryItem)
@ -26,8 +28,10 @@ interface HistoryController {
fun handleCopyUrl(item: HistoryItem)
fun handleShare(item: HistoryItem)
fun handleRequestSync()
fun handleEnterRecentlyClosed()
}
@Suppress("TooManyFunctions")
class DefaultHistoryController(
private val store: HistoryFragmentStore,
private val navController: NavController,
@ -101,4 +105,11 @@ class DefaultHistoryController(
store.dispatch(HistoryFragmentAction.FinishSync)
}
}
override fun handleEnterRecentlyClosed() {
navController.navigate(
HistoryFragmentDirections.actionGlobalRecentlyClosed(),
NavOptions.Builder().setPopUpTo(R.id.recentlyClosedFragment, true).build()
)
}
}

@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.fxa.sync.SyncReason
@ -49,12 +50,15 @@ import org.mozilla.fenix.utils.allowUndo
@SuppressWarnings("TooManyFunctions", "LargeClass")
class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandler {
private lateinit var historyStore: HistoryFragmentStore
private lateinit var historyView: HistoryView
private lateinit var historyInteractor: HistoryInteractor
private lateinit var viewModel: HistoryViewModel
private var undoScope: CoroutineScope? = null
private var pendingHistoryDeletionJob: (suspend () -> Unit)? = null
private var _historyView: HistoryView? = null
protected val historyView: HistoryView
get() = _historyView!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -91,7 +95,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
historyInteractor = HistoryInteractor(
historyController
)
historyView = HistoryView(view.historyLayout, historyInteractor)
_historyView = HistoryView(
view.historyLayout,
historyInteractor
)
return view
}
@ -234,6 +241,11 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
return historyView.onBackPressed()
}
override fun onDestroyView() {
super.onDestroyView()
_historyView = null
}
private fun openItem(item: HistoryItem, mode: BrowsingMode? = null) {
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
@ -255,8 +267,9 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
}
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
historyStore.dispatch(HistoryFragmentAction.EnterDeletionMode)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycleScope.launch(IO) {
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
requireComponents.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
requireComponents.core.historyStorage.deleteEverything()
launch(Main) {
viewModel.invalidate()

@ -61,4 +61,8 @@ class HistoryInteractor(
override fun onRequestSync() {
historyController.handleRequestSync()
}
override fun onRecentlyClosedClicked() {
historyController.handleEnterRecentlyClosed()
}
}

@ -77,6 +77,11 @@ interface HistoryViewInteractor : SelectionInteractor<HistoryItem> {
* Called when the user requests a sync of the history
*/
fun onRequestSync()
/**
* Called when the user clicks on recently closed tab button.
*/
fun onRecentlyClosedClicked()
}
/**

@ -5,10 +5,12 @@
package org.mozilla.fenix.library.history.viewholders
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.*
import kotlinx.android.synthetic.main.library_site_item.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.SelectionHolder
@ -38,6 +40,10 @@ class HistoryListItemViewHolder(
historyInteractor.onDeleteSome(selected)
}
}
itemView.recently_closed.setOnClickListener {
historyInteractor.onRecentlyClosedClicked()
}
}
fun bind(
@ -56,7 +62,7 @@ class HistoryListItemViewHolder(
itemView.history_layout.titleView.text = item.title
itemView.history_layout.urlView.text = item.url
toggleDeleteButton(showDeleteButton, mode === HistoryFragmentState.Mode.Normal)
toggleTopContent(showDeleteButton, mode === HistoryFragmentState.Mode.Normal)
val headerText = timeGroup?.humanReadable(itemView.context)
toggleHeader(headerText)
@ -86,11 +92,11 @@ class HistoryListItemViewHolder(
}
}
private fun toggleDeleteButton(
showDeleteButton: Boolean,
private fun toggleTopContent(
showTopContent: Boolean,
isNormalMode: Boolean
) {
if (showDeleteButton) {
if (showTopContent) {
itemView.delete_button.run {
visibility = View.VISIBLE
@ -102,7 +108,16 @@ class HistoryListItemViewHolder(
alpha = DELETE_BUTTON_DISABLED_ALPHA
}
}
val numRecentTabs = itemView.context.components.core.store.state.closedTabs.size
itemView.recently_closed_tabs_description.text = String.format(
itemView.context.getString(
if (numRecentTabs == 1)
R.string.recently_closed_tab else R.string.recently_closed_tabs
), numRecentTabs
)
itemView.recently_closed.isVisible = true
} else {
itemView.recently_closed.visibility = View.GONE
itemView.delete_button.visibility = View.GONE
}
}

@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.recentlyclosed
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.state.state.ClosedTab
class RecentlyClosedAdapter(
private val interactor: RecentlyClosedFragmentInteractor
) : ListAdapter<ClosedTab, RecentlyClosedItemViewHolder>(DiffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecentlyClosedItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(RecentlyClosedItemViewHolder.LAYOUT_ID, parent, false)
return RecentlyClosedItemViewHolder(view, interactor)
}
override fun onBindViewHolder(holder: RecentlyClosedItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
private object DiffCallback : DiffUtil.ItemCallback<ClosedTab>() {
override fun areItemsTheSame(oldItem: ClosedTab, newItem: ClosedTab) =
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
override fun areContentsTheSame(oldItem: ClosedTab, newItem: ClosedTab) =
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
}
}

@ -0,0 +1,86 @@
/* 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.library.recentlyclosed
import android.content.ClipData
import android.content.ClipboardManager
import android.content.res.Resources
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.recentlyclosed.ext.restoreTab
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
interface RecentlyClosedController {
fun handleOpen(item: ClosedTab, mode: BrowsingMode? = null)
fun handleDeleteOne(tab: ClosedTab)
fun handleCopyUrl(item: ClosedTab)
fun handleShare(item: ClosedTab)
fun handleNavigateToHistory()
fun handleRestore(item: ClosedTab)
}
class DefaultRecentlyClosedController(
private val navController: NavController,
private val store: BrowserStore,
private val sessionManager: SessionManager,
private val resources: Resources,
private val snackbar: FenixSnackbar,
private val clipboardManager: ClipboardManager,
private val activity: HomeActivity,
private val openToBrowser: (item: ClosedTab, mode: BrowsingMode?) -> Unit
) : RecentlyClosedController {
override fun handleOpen(item: ClosedTab, mode: BrowsingMode?) {
openToBrowser(item, mode)
}
override fun handleDeleteOne(tab: ClosedTab) {
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tab))
}
override fun handleNavigateToHistory() {
navController.navigate(
RecentlyClosedFragmentDirections.actionGlobalHistoryFragment(),
NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build()
)
}
override fun handleCopyUrl(item: ClosedTab) {
val urlClipData = ClipData.newPlainText(item.url, item.url)
clipboardManager.setPrimaryClip(urlClipData)
with(snackbar) {
setText(resources.getString(R.string.url_copied))
show()
}
}
override fun handleShare(item: ClosedTab) {
navController.navigate(
RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title))
)
)
}
override fun handleRestore(item: ClosedTab) {
item.restoreTab(
store,
sessionManager,
onTabRestored = {
activity.openToBrowser(
from = BrowserDirection.FromRecentlyClosed
)
}
)
}
}

@ -0,0 +1,135 @@
/* 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.library.recentlyclosed
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_recently_closed_tabs.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.library.LibraryPageFragment
@Suppress("TooManyFunctions")
class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore
private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null
protected val recentlyClosedFragmentView: RecentlyClosedFragmentView
get() = _recentlyClosedFragmentView!!
private lateinit var recentlyClosedInteractor: RecentlyClosedFragmentInteractor
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.library_recently_closed_tabs))
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.close_history -> {
close()
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_recently_closed_tabs, container, false)
recentlyClosedFragmentStore = StoreProvider.get(this) {
RecentlyClosedFragmentStore(
RecentlyClosedFragmentState(
items = listOf()
)
)
}
recentlyClosedInteractor = RecentlyClosedFragmentInteractor(
recentlyClosedController = DefaultRecentlyClosedController(
navController = findNavController(),
store = requireComponents.core.store,
activity = activity as HomeActivity,
sessionManager = requireComponents.core.sessionManager,
resources = requireContext().resources,
snackbar = FenixSnackbar.make(
view = requireActivity().getRootView()!!,
isDisplayedWithBrowserToolbar = true
),
clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager,
openToBrowser = ::openItem
)
)
_recentlyClosedFragmentView = RecentlyClosedFragmentView(
view.recentlyClosedLayout,
recentlyClosedInteractor
)
return view
}
override fun onDestroyView() {
super.onDestroyView()
_recentlyClosedFragmentView = null
}
private fun openItem(tab: ClosedTab, mode: BrowsingMode? = null) {
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = tab.url,
newTab = true,
from = BrowserDirection.FromRecentlyClosed
)
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(recentlyClosedFragmentStore) {
recentlyClosedFragmentView.update(it.items)
}
requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow ->
flow.map { state -> state.closedTabs }
.ifChanged()
.collect { tabs ->
recentlyClosedFragmentStore.dispatch(
RecentlyClosedFragmentAction.Change(tabs)
)
}
}
}
override val selectedItems: Set<ClosedTab> = setOf()
}

@ -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.library.recentlyclosed
import mozilla.components.browser.state.state.ClosedTab
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
/**
* Interactor for the recently closed screen
* Provides implementations for the RecentlyClosedInteractor
*/
class RecentlyClosedFragmentInteractor(
private val recentlyClosedController: RecentlyClosedController
) : RecentlyClosedInteractor {
override fun restore(item: ClosedTab) {
recentlyClosedController.handleRestore(item)
}
override fun onCopyPressed(item: ClosedTab) {
recentlyClosedController.handleCopyUrl(item)
}
override fun onSharePressed(item: ClosedTab) {
recentlyClosedController.handleShare(item)
}
override fun onOpenInNormalTab(item: ClosedTab) {
recentlyClosedController.handleOpen(item, BrowsingMode.Normal)
}
override fun onOpenInPrivateTab(item: ClosedTab) {
recentlyClosedController.handleOpen(item, BrowsingMode.Private)
}
override fun onDeleteOne(tab: ClosedTab) {
recentlyClosedController.handleDeleteOne(tab)
}
override fun onNavigateToHistory() {
recentlyClosedController.handleNavigateToHistory()
}
}

@ -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.library.recentlyclosed
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* The [Store] for holding the [RecentlyClosedFragmentState] and applying [RecentlyClosedFragmentAction]s.
*/
class RecentlyClosedFragmentStore(initialState: RecentlyClosedFragmentState) :
Store<RecentlyClosedFragmentState, RecentlyClosedFragmentAction>(
initialState,
::recentlyClosedStateReducer
)
/**
* Actions to dispatch through the `RecentlyClosedFragmentStore` to modify
* `RecentlyClosedFragmentState` through the reducer.
*/
sealed class RecentlyClosedFragmentAction : Action {
data class Change(val list: List<ClosedTab>) : RecentlyClosedFragmentAction()
}
/**
* The state for the Recently Closed Screen
* @property items List of recently closed tabs to display
*/
data class RecentlyClosedFragmentState(val items: List<ClosedTab> = emptyList()) : State
/**
* The RecentlyClosedFragmentState Reducer.
*/
private fun recentlyClosedStateReducer(
state: RecentlyClosedFragmentState,
action: RecentlyClosedFragmentAction
): RecentlyClosedFragmentState {
return when (action) {
is RecentlyClosedFragmentAction.Change -> state.copy(items = action.list)
}
}

@ -0,0 +1,110 @@
/* 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.library.recentlyclosed
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_recently_closed.*
import mozilla.components.browser.state.state.ClosedTab
import org.mozilla.fenix.R
interface RecentlyClosedInteractor {
/**
* Called when an item is tapped to restore it.
*
* @param item the tapped item to restore.
*/
fun restore(item: ClosedTab)
/**
* Called when the view more history option is tapped.
*/
fun onNavigateToHistory()
/**
* Copies the URL of a recently closed tab item to the copy-paste buffer.
*
* @param item the recently closed tab item to copy the URL from
*/
fun onCopyPressed(item: ClosedTab)
/**
* Opens the share sheet for a recently closed tab item.
*
* @param item the recently closed tab item to share
*/
fun onSharePressed(item: ClosedTab)
/**
* Opens a recently closed tab item in a new tab.
*
* @param item the recently closed tab item to open in a new tab
*/
fun onOpenInNormalTab(item: ClosedTab)
/**
* Opens a recently closed tab item in a private tab.
*
* @param item the recently closed tab item to open in a private tab
*/
fun onOpenInPrivateTab(item: ClosedTab)
/**
* Deletes one recently closed tab item.
*
* @param item the recently closed tab item to delete.
*/
fun onDeleteOne(tab: ClosedTab)
}
/**
* View that contains and configures the Recently Closed List
*/
class RecentlyClosedFragmentView(
container: ViewGroup,
private val interactor: RecentlyClosedFragmentInteractor
) : LayoutContainer {
override val containerView: ConstraintLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_recently_closed, container, true)
.findViewById(R.id.recently_closed_wrapper)
private val recentlyClosedAdapter: RecentlyClosedAdapter = RecentlyClosedAdapter(interactor)
init {
recently_closed_list.apply {
layoutManager = LinearLayoutManager(containerView.context)
adapter = recentlyClosedAdapter
}
view_more_history.apply {
titleView.text =
containerView.context.getString(R.string.recently_closed_show_full_history)
urlView.isVisible = false
overflowView.isVisible = false
iconView.background = null
iconView.setImageDrawable(
ContextCompat.getDrawable(
containerView.context,
R.drawable.ic_history
)
)
setOnClickListener {
interactor.onNavigateToHistory()
}
}
}
fun update(items: List<ClosedTab>) {
recently_closed_empty_view.isVisible = items.isEmpty()
recently_closed_list.isVisible = items.isNotEmpty()
recentlyClosedAdapter.submitList(items)
}
}

@ -0,0 +1,68 @@
/* 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.library.recentlyclosed
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.*
import mozilla.components.browser.state.state.ClosedTab
import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.HistoryItemMenu
import org.mozilla.fenix.utils.Do
class RecentlyClosedItemViewHolder(
view: View,
private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor
) : RecyclerView.ViewHolder(view) {
private var item: ClosedTab? = null
init {
setupMenu()
}
fun bind(
item: ClosedTab
) {
itemView.history_layout.titleView.text =
if (item.title.isNotEmpty()) item.title else item.url
itemView.history_layout.urlView.text = item.url
if (this.item?.url != item.url) {
itemView.history_layout.loadFavicon(item.url)
}
itemView.setOnClickListener {
recentlyClosedFragmentInteractor.restore(item)
}
this.item = item
}
private fun setupMenu() {
val historyMenu = HistoryItemMenu(itemView.context) {
val item = this.item ?: return@HistoryItemMenu
Do exhaustive when (it) {
HistoryItemMenu.Item.Copy -> recentlyClosedFragmentInteractor.onCopyPressed(item)
HistoryItemMenu.Item.Share -> recentlyClosedFragmentInteractor.onSharePressed(item)
HistoryItemMenu.Item.OpenInNewTab -> recentlyClosedFragmentInteractor.onOpenInNormalTab(
item
)
HistoryItemMenu.Item.OpenInPrivateTab -> recentlyClosedFragmentInteractor.onOpenInPrivateTab(
item
)
HistoryItemMenu.Item.Delete -> recentlyClosedFragmentInteractor.onDeleteOne(
item
)
}
}
itemView.history_layout.attachMenu(historyMenu.menuController)
}
companion object {
const val LAYOUT_ID = R.layout.history_list_item
}
}

@ -26,6 +26,7 @@ fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar:
activity.components.useCases.tabsUseCases.removeAllTabs,
activity.components.core.historyStorage,
activity.components.core.permissionStorage,
activity.components.core.store,
activity.components.core.icons,
activity.components.core.engine,
coroutineContext

@ -7,6 +7,8 @@ package org.mozilla.fenix.settings.deletebrowsingdata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.tabs.TabsUseCases
@ -25,6 +27,7 @@ class DefaultDeleteBrowsingDataController(
private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase,
private val historyStorage: HistoryStorage,
private val permissionStorage: PermissionStorage,
private val store: BrowserStore,
private val iconsStorage: BrowserIcons,
private val engine: Engine,
private val coroutineContext: CoroutineContext = Dispatchers.Main
@ -41,6 +44,7 @@ class DefaultDeleteBrowsingDataController(
engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
historyStorage.deleteEverything()
iconsStorage.clear()
store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
}
}

@ -28,7 +28,6 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
@ -45,11 +44,12 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
super.onViewCreated(view, savedInstanceState)
controller = DefaultDeleteBrowsingDataController(
requireContext().components.useCases.tabsUseCases.removeAllTabs,
requireContext().components.core.historyStorage,
requireContext().components.core.permissionStorage,
requireContext().components.core.icons,
requireContext().components.core.engine
requireComponents.useCases.tabsUseCases.removeAllTabs,
requireComponents.core.historyStorage,
requireComponents.core.permissionStorage,
requireComponents.core.store,
requireComponents.core.icons,
requireComponents.core.engine
)
settings = requireContext().settings()

@ -42,6 +42,7 @@ interface TabTrayController {
fun handleRemoveSelectedTab(tab: Tab)
fun handleOpenTab(tab: Tab)
fun handleEnterMultiselect()
fun handleRecentlyClosedClicked()
}
/**
@ -178,4 +179,9 @@ class DefaultTabTrayController(
override fun handleEnterMultiselect() {
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
}
override fun handleRecentlyClosedClicked() {
val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed()
navController.navigate(directions)
}
}

@ -73,6 +73,11 @@ interface TabTrayInteractor {
* Called when multiselect mode should be entered with no tabs selected.
*/
fun onEnterMultiselect()
/**
* Called when user clicks the recently closed tabs menu button.
*/
fun onOpenRecentlyClosedClicked()
}
/**
@ -92,6 +97,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
controller.handleTabSettingsClicked()
}
override fun onOpenRecentlyClosedClicked() {
controller.handleRecentlyClosedClicked()
}
override fun onShareTabsClicked(private: Boolean) {
controller.onShareTabsClicked(private)
}

@ -221,6 +221,7 @@ class TabTrayView(
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
isPrivateModeSelected
)
is TabTrayItemMenu.Item.OpenRecentlyClosed -> interactor.onOpenRecentlyClosedClicked()
}
}
@ -596,6 +597,7 @@ class TabTrayItemMenu(
object OpenTabSettings : Item()
object SaveToCollection : Item()
object CloseAllTabs : Item()
object OpenRecentlyClosed : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
@ -625,6 +627,13 @@ class TabTrayItemMenu(
onItemTapped.invoke(Item.OpenTabSettings)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_recently_closed),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenRecentlyClosed)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_close),
textColorResource = R.color.primary_text_normal_theme

@ -0,0 +1,38 @@
<?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:id="@+id/recently_closed_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.mozilla.fenix.library.LibrarySiteItemView
android:id="@+id/view_more_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recently_closed_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/view_more_history"
tools:listitem="@layout/history_list_item" />
<TextView
android:id="@+id/recently_closed_empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/recently_closed_empty_message"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,9 @@
<?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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recentlyClosedLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />

@ -2,12 +2,12 @@
<!-- 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/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<LinearLayout 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="wrap_content"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_button"
@ -16,18 +16,76 @@
android:text="@string/history_delete_all"
android:visibility="gone" />
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout
android:visibility="gone"
android:id="@+id/recently_closed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="?android:attr/selectableItemBackground"
android:minHeight="@dimen/library_item_height">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="@dimen/history_favicon_width_height"
android:layout_height="@dimen/history_favicon_width_height"
android:layout_marginStart="20dp"
android:importantForAccessibility="no"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_multiple_tabs" />
<TextView
android:id="@+id/recently_closed_tabs_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/library_recently_closed_tabs"
android:textAlignment="viewStart"
android:textColor="?primaryText"
android:fontFamily="@font/metropolis_semibold"
android:paddingStart="20dp"
android:paddingEnd="0dp"
android:layout_marginBottom="8dp"
tools:text="Header"
android:visibility="gone" />
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@+id/recently_closed_tabs_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginEnd="@dimen/library_item_icon_margin_horizontal" />
<TextView
android:id="@+id/recently_closed_tabs_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toBottomOf="@id/recently_closed_tabs_header"
app:layout_goneMarginEnd="@dimen/library_item_icon_margin_horizontal"
tools:text="2 tabs" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="@font/metropolis_semibold"
android:paddingStart="20dp"
android:paddingEnd="0dp"
android:textColor="?primaryText"
android:textSize="14sp"
android:visibility="gone"
tools:text="Header" />
<org.mozilla.fenix.library.LibrarySiteItemView
android:id="@+id/history_layout"
@ -35,4 +93,3 @@
android:layout_height="wrap_content"
android:minHeight="@dimen/library_item_height" />
</LinearLayout>

@ -31,6 +31,10 @@
android:id="@+id/action_global_search_dialog"
app:destination="@id/searchDialogFragment" />
<action
android:id="@+id/action_global_recently_closed"
app:destination="@id/recentlyClosedFragment" />
<action
android:id="@+id/action_global_shareFragment"
app:destination="@id/shareFragment" />
@ -177,6 +181,11 @@
app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint" />
</fragment>
<fragment
android:id="@+id/recentlyClosedFragment"
android:name="org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragment"
android:label="@string/library_recently_closed_tabs" />
<fragment
android:id="@+id/SitePermissionsManagePhoneFeature"
android:name="org.mozilla.fenix.settings.sitepermissions.SitePermissionsManagePhoneFeatureFragment"

@ -2,6 +2,7 @@ package org.mozilla.fenix.ext
import android.content.Intent
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import io.mockk.Matcher
import io.mockk.MockKMatcherScope
import io.mockk.internalSubstitute
@ -12,6 +13,11 @@ import mozilla.components.support.ktx.android.os.contentEquals
*/
fun MockKMatcherScope.directionsEq(value: NavDirections) = match(EqNavDirectionsMatcher(value))
/**
* Verify that an equal [NavOptions] object was passed in a MockK verify call.
*/
fun MockKMatcherScope.optionsEq(value: NavOptions) = match(EqNavOptionsMatcher(value))
/**
* Verify that two intents are the same for the purposes of intent resolution (filtering).
* Checks if their action, data, type, identity, class, and categories are the same.
@ -28,6 +34,15 @@ private data class EqNavDirectionsMatcher(private val value: NavDirections) : Ma
copy(value = value.internalSubstitute(map))
}
private data class EqNavOptionsMatcher(private val value: NavOptions) : Matcher<NavOptions> {
override fun match(arg: NavOptions?): Boolean =
value.popUpTo == arg?.popUpTo && value.isPopUpToInclusive == arg.isPopUpToInclusive
override fun substitute(map: Map<Any, Any>) =
copy(value = value.internalSubstitute(map))
}
private data class EqIntentFilterMatcher(private val value: Intent) : Matcher<Intent> {
override fun match(arg: Intent?): Boolean = value.filterEquals(arg)

@ -0,0 +1,171 @@
/* 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.library.recentlyclosed
import android.content.ClipData
import android.content.ClipboardManager
import android.content.res.Resources
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.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.recentlyclosed.ext.restoreTab
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.directionsEq
import org.mozilla.fenix.ext.optionsEq
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
// Robolectric needed for `onShareItem()`
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class DefaultRecentlyClosedControllerTest {
private val dispatcher = TestCoroutineDispatcher()
private val navController: NavController = mockk(relaxed = true)
private val resources: Resources = mockk(relaxed = true)
private val snackbar: FenixSnackbar = mockk(relaxed = true)
private val clipboardManager: ClipboardManager = mockk(relaxed = true)
private val openToBrowser: (ClosedTab, BrowsingMode?) -> Unit = mockk(relaxed = true)
private val sessionManager: SessionManager = mockk(relaxed = true)
private val activity: HomeActivity = mockk(relaxed = true)
private val store: BrowserStore = mockk(relaxed = true)
val mockedTab: ClosedTab = mockk(relaxed = true)
private val controller = DefaultRecentlyClosedController(
navController,
store,
sessionManager,
resources,
snackbar,
clipboardManager,
activity,
openToBrowser
)
@Before
fun setUp() {
mockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt")
every { mockedTab.restoreTab(any(), any(), any()) } just Runs
}
@After
fun tearDown() {
dispatcher.cleanupTestCoroutines()
unmockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt")
}
@Test
fun handleOpen() {
val item: ClosedTab = mockk(relaxed = true)
controller.handleOpen(item, BrowsingMode.Private)
verify {
openToBrowser(item, BrowsingMode.Private)
}
controller.handleOpen(item, BrowsingMode.Normal)
verify {
openToBrowser(item, BrowsingMode.Normal)
}
}
@Test
fun handleDeleteOne() {
val item: ClosedTab = mockk(relaxed = true)
controller.handleDeleteOne(item)
verify {
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(item))
}
}
@Test
fun handleNavigateToHistory() {
controller.handleNavigateToHistory()
verify {
navController.navigate(
directionsEq(
RecentlyClosedFragmentDirections.actionGlobalHistoryFragment()
),
optionsEq(NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build())
)
}
}
@Test
fun handleCopyUrl() {
val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
val clipdata = slot<ClipData>()
controller.handleCopyUrl(item)
verify {
clipboardManager.setPrimaryClip(capture(clipdata))
snackbar.show()
}
assertEquals(1, clipdata.captured.itemCount)
assertEquals("mozilla.org", clipdata.captured.description.label)
assertEquals("mozilla.org", clipdata.captured.getItemAt(0).text)
}
@Test
@Suppress("UNCHECKED_CAST")
fun handleShare() {
val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
controller.handleShare(item)
verify {
navController.navigate(
directionsEq(
RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title))
)
)
)
}
}
@Test
fun handleRestore() {
controller.handleRestore(mockedTab)
dispatcher.advanceUntilIdle()
verify {
mockedTab.restoreTab(
store,
sessionManager,
onTabRestored = any()
)
}
}
}

@ -0,0 +1,96 @@
/* 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.library.recentlyclosed
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.state.state.ClosedTab
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
class RecentlyClosedFragmentInteractorTest {
lateinit var interactor: RecentlyClosedFragmentInteractor
private val defaultRecentlyClosedController: DefaultRecentlyClosedController =
mockk(relaxed = true)
@Before
fun setup() {
interactor =
RecentlyClosedFragmentInteractor(
recentlyClosedController = defaultRecentlyClosedController
)
}
@Test
fun open() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.restore(tab)
verify {
defaultRecentlyClosedController.handleRestore(tab)
}
}
@Test
fun onCopyPressed() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onCopyPressed(tab)
verify {
defaultRecentlyClosedController.handleCopyUrl(tab)
}
}
@Test
fun onSharePressed() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onSharePressed(tab)
verify {
defaultRecentlyClosedController.handleShare(tab)
}
}
@Test
fun onOpenInNormalTab() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onOpenInNormalTab(tab)
verify {
defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Normal)
}
}
@Test
fun onOpenInPrivateTab() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onOpenInPrivateTab(tab)
verify {
defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Private)
}
}
@Test
fun onDeleteOne() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onDeleteOne(tab)
verify {
defaultRecentlyClosedController.handleDeleteOne(tab)
}
}
@Test
fun onNavigateToHistory() {
interactor.onNavigateToHistory()
verify {
defaultRecentlyClosedController.handleNavigateToHistory()
}
}
}

@ -13,6 +13,8 @@ import kotlinx.coroutines.GlobalScope.coroutineContext
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.tabs.TabsUseCases
@ -31,6 +33,7 @@ class DefaultDeleteBrowsingDataControllerTest {
private var removeAllTabs: TabsUseCases.RemoveAllTabsUseCase = mockk(relaxed = true)
private var historyStorage: HistoryStorage = mockk(relaxed = true)
private var permissionStorage: PermissionStorage = mockk(relaxed = true)
private var store: BrowserStore = mockk(relaxed = true)
private var iconsStorage: BrowserIcons = mockk(relaxed = true)
private val engine: Engine = mockk(relaxed = true)
private lateinit var controller: DefaultDeleteBrowsingDataController
@ -40,6 +43,7 @@ class DefaultDeleteBrowsingDataControllerTest {
controller = DefaultDeleteBrowsingDataController(
removeAllTabs = removeAllTabs,
historyStorage = historyStorage,
store = store,
permissionStorage = permissionStorage,
iconsStorage = iconsStorage,
engine = engine,
@ -65,6 +69,7 @@ class DefaultDeleteBrowsingDataControllerTest {
coVerify {
engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
historyStorage.deleteEverything()
store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
iconsStorage.clear()
}
}

@ -111,6 +111,7 @@ object Deps {
const val mozilla_feature_site_permissions = "org.mozilla.components:feature-sitepermissions:${Versions.mozilla_android_components}"
const val mozilla_feature_readerview = "org.mozilla.components:feature-readerview:${Versions.mozilla_android_components}"
const val mozilla_feature_tab_collections = "org.mozilla.components:feature-tab-collections:${Versions.mozilla_android_components}"
const val mozilla_feature_recentlyclosed = "org.mozilla.components:feature-recentlyclosed:${Versions.mozilla_android_components}"
const val mozilla_feature_accounts_push = "org.mozilla.components:feature-accounts-push:${Versions.mozilla_android_components}"
const val mozilla_feature_top_sites = "org.mozilla.components:feature-top-sites:${Versions.mozilla_android_components}"
const val mozilla_feature_share = "org.mozilla.components:feature-share:${Versions.mozilla_android_components}"

Loading…
Cancel
Save