diff --git a/app/build.gradle b/app/build.gradle index 3cf6ad855..dfcc663d9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index 841305b36..84da2f843 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index aaa99af95..d18c56462 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -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) } /** diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index 5b208af83..315f20704 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 12a556bfc..fbec09a5d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 5ddd53818..bc392ac14 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt index 234082b74..7a0b33b1a 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt @@ -27,9 +27,9 @@ enum class HistoryItemTimeGroup { } } -class HistoryAdapter( - private val historyInteractor: HistoryInteractor -) : PagedListAdapter(historyDiffCallback), SelectionHolder { +class HistoryAdapter(private val historyInteractor: HistoryInteractor) : + PagedListAdapter(historyDiffCallback), + SelectionHolder { private var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal override val selectedItems get() = mode.selectedItems diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt index 46a99952e..069afb1df 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt @@ -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() + ) + } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index 3b5f6d7d8..b34f22bb4 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -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(), 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(), UserInteractionHandl historyInteractor = HistoryInteractor( historyController ) - historyView = HistoryView(view.historyLayout, historyInteractor) + _historyView = HistoryView( + view.historyLayout, + historyInteractor + ) return view } @@ -234,6 +241,11 @@ class HistoryFragment : LibraryPageFragment(), 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(), 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() diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt index 4ddf147a7..2c8b9056e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt @@ -61,4 +61,8 @@ class HistoryInteractor( override fun onRequestSync() { historyController.handleRequestSync() } + + override fun onRecentlyClosedClicked() { + historyController.handleEnterRecentlyClosed() + } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt index 09be8a3d7..c309a4fb9 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt @@ -77,6 +77,11 @@ interface HistoryViewInteractor : SelectionInteractor { * Called when the user requests a sync of the history */ fun onRequestSync() + + /** + * Called when the user clicks on recently closed tab button. + */ + fun onRecentlyClosedClicked() } /** diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt index f833507e9..03016716e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt new file mode 100644 index 000000000..db6121c1e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt @@ -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(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() { + 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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt new file mode 100644 index 000000000..0f71bf023 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt @@ -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 + ) + } + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt new file mode 100644 index 000000000..743e33c18 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt @@ -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() { + 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 = setOf() +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt new file mode 100644 index 000000000..b62b430b2 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt @@ -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() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt new file mode 100644 index 000000000..cb75dabca --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt @@ -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( + initialState, + ::recentlyClosedStateReducer + ) + +/** + * Actions to dispatch through the `RecentlyClosedFragmentStore` to modify + * `RecentlyClosedFragmentState` through the reducer. + */ +sealed class RecentlyClosedFragmentAction : Action { + data class Change(val list: List) : RecentlyClosedFragmentAction() +} + +/** + * The state for the Recently Closed Screen + * @property items List of recently closed tabs to display + */ +data class RecentlyClosedFragmentState(val items: List = emptyList()) : State + +/** + * The RecentlyClosedFragmentState Reducer. + */ +private fun recentlyClosedStateReducer( + state: RecentlyClosedFragmentState, + action: RecentlyClosedFragmentAction +): RecentlyClosedFragmentState { + return when (action) { + is RecentlyClosedFragmentAction.Change -> state.copy(items = action.list) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt new file mode 100644 index 000000000..0ec06e48e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt @@ -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) { + recently_closed_empty_view.isVisible = items.isEmpty() + recently_closed_list.isVisible = items.isNotEmpty() + recentlyClosedAdapter.submitList(items) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt new file mode 100644 index 000000000..e60cc34ea --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt @@ -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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt index 6f87de29d..8550cd650 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt index 575e8e969..7b02cd526 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt @@ -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) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt index 733ae3ba2..77f867494 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt index ea1a4abf0..431bfe898 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -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) + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt index 2ec18d928..3cf89a386 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index 3a237e22a..92e809bd7 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -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 diff --git a/app/src/main/res/layout/component_recently_closed.xml b/app/src/main/res/layout/component_recently_closed.xml new file mode 100644 index 000000000..e0b70db2c --- /dev/null +++ b/app/src/main/res/layout/component_recently_closed.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_recently_closed_tabs.xml b/app/src/main/res/layout/fragment_recently_closed_tabs.xml new file mode 100644 index 000000000..bfeca1ab4 --- /dev/null +++ b/app/src/main/res/layout/fragment_recently_closed_tabs.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/history_list_item.xml b/app/src/main/res/layout/history_list_item.xml index a40877e72..147d44820 100644 --- a/app/src/main/res/layout/history_list_item.xml +++ b/app/src/main/res/layout/history_list_item.xml @@ -2,12 +2,12 @@ - + - + + + + + 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" /> + + + + + - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 866cdf46b..a91a57421 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -31,6 +31,10 @@ android:id="@+id/action_global_search_dialog" app:destination="@id/searchDialogFragment" /> + + @@ -177,6 +181,11 @@ app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint" /> + + { + + override fun match(arg: NavOptions?): Boolean = + value.popUpTo == arg?.popUpTo && value.isPopUpToInclusive == arg.isPopUpToInclusive + + override fun substitute(map: Map) = + copy(value = value.internalSubstitute(map)) +} + private data class EqIntentFilterMatcher(private val value: Intent) : Matcher { override fun match(arg: Intent?): Boolean = value.filterEquals(arg) diff --git a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt new file mode 100644 index 000000000..1d2a767a1 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt @@ -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() + + 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() + ) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt new file mode 100644 index 000000000..c4242bc03 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt @@ -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() + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt index 0d300350b..605a81492 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt @@ -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() } } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index c10b9775e..ff7344e09 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -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}"