For #2486 - Adds Recently Closed Tabs
This commit is contained in:
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…
Reference in New Issue