For #2486 - Adds Recently Closed Tabs
parent
cce58e7d51
commit
09fbb43f80
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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" />
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue