Close #25954: add a new delete time range confirmation dialog for history screen
This commit is contained in:
parent
bfd0eb7306
commit
7982c6b79f
|
@ -71,7 +71,7 @@ class HistoryRobot {
|
||||||
deleteButton(item).click()
|
deleteButton(item).click()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clickDeleteAllHistoryButton() = deleteAllButton().click()
|
fun clickDeleteAllHistoryButton() = deleteButton().click()
|
||||||
|
|
||||||
fun confirmDeleteAllHistory() {
|
fun confirmDeleteAllHistory() {
|
||||||
onView(withText("Delete"))
|
onView(withText("Delete"))
|
||||||
|
@ -104,7 +104,7 @@ private fun pageUrl() = onView(withId(R.id.url))
|
||||||
private fun deleteButton(title: String) =
|
private fun deleteButton(title: String) =
|
||||||
onView(allOf(withId(R.id.overflow_menu), hasSibling(withText(title))))
|
onView(allOf(withId(R.id.overflow_menu), hasSibling(withText(title))))
|
||||||
|
|
||||||
private fun deleteAllButton() = onView(withId(R.id.history_delete_all))
|
private fun deleteButton() = onView(withId(R.id.history_delete))
|
||||||
|
|
||||||
private fun snackBarText() = onView(withId(R.id.snackbar_text))
|
private fun snackBarText() = onView(withId(R.id.snackbar_text))
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ private fun assertPageUrl(expectedUrl: Uri) = pageUrl()
|
||||||
.check(matches(withText(Matchers.containsString(expectedUrl.toString()))))
|
.check(matches(withText(Matchers.containsString(expectedUrl.toString()))))
|
||||||
|
|
||||||
private fun assertDeleteConfirmationMessage() =
|
private fun assertDeleteConfirmationMessage() =
|
||||||
onView(withText("This will delete all of your browsing data."))
|
onView(withText("Removes history (including history synced from other devices), cookies and other browsing data."))
|
||||||
.inRoot(isDialog())
|
.inRoot(isDialog())
|
||||||
.check(matches(isDisplayed()))
|
.check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,11 @@ import androidx.navigation.NavOptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.state.action.EngineAction
|
||||||
import mozilla.components.browser.state.action.HistoryMetadataAction
|
import mozilla.components.browser.state.action.HistoryMetadataAction
|
||||||
|
import mozilla.components.browser.state.action.RecentlyClosedAction
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||||
import mozilla.components.service.glean.private.NoExtras
|
import mozilla.components.service.glean.private.NoExtras
|
||||||
import org.mozilla.fenix.GleanMetrics.Events
|
import org.mozilla.fenix.GleanMetrics.Events
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -19,6 +23,7 @@ import org.mozilla.fenix.components.appstate.AppAction
|
||||||
import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider
|
import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.navigateSafe
|
import org.mozilla.fenix.ext.navigateSafe
|
||||||
|
import org.mozilla.fenix.library.history.HistoryFragment.DeleteConfirmationDialogFragment
|
||||||
import org.mozilla.fenix.utils.Settings
|
import org.mozilla.fenix.utils.Settings
|
||||||
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
|
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
|
||||||
|
|
||||||
|
@ -30,10 +35,22 @@ interface HistoryController {
|
||||||
fun handleBackPressed(): Boolean
|
fun handleBackPressed(): Boolean
|
||||||
fun handleModeSwitched()
|
fun handleModeSwitched()
|
||||||
fun handleSearch()
|
fun handleSearch()
|
||||||
fun handleDeleteAll()
|
|
||||||
|
/**
|
||||||
|
* Displays a [DeleteConfirmationDialogFragment].
|
||||||
|
*/
|
||||||
|
fun handleDeleteTimeRange()
|
||||||
fun handleDeleteSome(items: Set<History>)
|
fun handleDeleteSome(items: Set<History>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes history items inside the time frame.
|
||||||
|
*
|
||||||
|
* @param timeFrame Selected time frame by the user. If `null`, removes all history.
|
||||||
|
*/
|
||||||
|
fun handleDeleteTimeRangeConfirmed(timeFrame: RemoveTimeFrame?)
|
||||||
fun handleRequestSync()
|
fun handleRequestSync()
|
||||||
fun handleEnterRecentlyClosed()
|
fun handleEnterRecentlyClosed()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to [org.mozilla.fenix.library.syncedhistory.SyncedHistoryFragment]
|
* Navigates to [org.mozilla.fenix.library.syncedhistory.SyncedHistoryFragment]
|
||||||
*/
|
*/
|
||||||
|
@ -44,11 +61,14 @@ interface HistoryController {
|
||||||
class DefaultHistoryController(
|
class DefaultHistoryController(
|
||||||
private val store: HistoryFragmentStore,
|
private val store: HistoryFragmentStore,
|
||||||
private val appStore: AppStore,
|
private val appStore: AppStore,
|
||||||
|
private val browserStore: BrowserStore,
|
||||||
|
private val historyStorage: PlacesHistoryStorage,
|
||||||
private var historyProvider: DefaultPagedHistoryProvider,
|
private var historyProvider: DefaultPagedHistoryProvider,
|
||||||
private val navController: NavController,
|
private val navController: NavController,
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val openToBrowser: (item: History.Regular) -> Unit,
|
private val openToBrowser: (item: History.Regular) -> Unit,
|
||||||
private val displayDeleteAll: () -> Unit,
|
private val displayDeleteTimeRange: () -> Unit,
|
||||||
|
private val onTimeFrameDeleted: () -> Unit,
|
||||||
private val invalidateOptionsMenu: () -> Unit,
|
private val invalidateOptionsMenu: () -> Unit,
|
||||||
private val deleteSnackbar: (
|
private val deleteSnackbar: (
|
||||||
items: Set<History>,
|
items: Set<History>,
|
||||||
|
@ -111,8 +131,8 @@ class DefaultHistoryController(
|
||||||
navController.navigateSafe(R.id.historyFragment, directions)
|
navController.navigateSafe(R.id.historyFragment, directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeleteAll() {
|
override fun handleDeleteTimeRange() {
|
||||||
displayDeleteAll.invoke()
|
displayDeleteTimeRange.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeleteSome(items: Set<History>) {
|
override fun handleDeleteSome(items: Set<History>) {
|
||||||
|
@ -121,6 +141,31 @@ class DefaultHistoryController(
|
||||||
deleteSnackbar.invoke(items, ::undo, ::delete)
|
deleteSnackbar.invoke(items, ::undo, ::delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handleDeleteTimeRangeConfirmed(timeFrame: RemoveTimeFrame?) {
|
||||||
|
scope.launch {
|
||||||
|
store.dispatch(HistoryFragmentAction.EnterDeletionMode)
|
||||||
|
if (timeFrame == null) {
|
||||||
|
GleanHistory.removedAll.record(mozilla.telemetry.glean.private.NoExtras())
|
||||||
|
historyStorage.deleteEverything()
|
||||||
|
} else {
|
||||||
|
historyStorage.deleteVisitsBetween(
|
||||||
|
startTime = timeFrame.toLongRange().first,
|
||||||
|
endTime = timeFrame.toLongRange().last,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// We introduced more deleting options, but are keeping these actions for all options.
|
||||||
|
// The approach could be improved: https://github.com/mozilla-mobile/fenix/issues/26102
|
||||||
|
browserStore.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
|
||||||
|
browserStore.dispatch(EngineAction.PurgeHistoryAction).join()
|
||||||
|
|
||||||
|
store.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
onTimeFrameDeleted.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun undo(items: Set<History>) {
|
private fun undo(items: Set<History>) {
|
||||||
val pendingDeletionItems = items.map { it.toPendingDeletionHistory() }.toSet()
|
val pendingDeletionItems = items.map { it.toPendingDeletionHistory() }.toSet()
|
||||||
appStore.dispatch(AppAction.UndoPendingDeletionSet(pendingDeletionItems))
|
appStore.dispatch(AppAction.UndoPendingDeletionSet(pendingDeletionItems))
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.library.history
|
package org.mozilla.fenix.library.history
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -14,7 +15,9 @@ import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.RadioGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -23,14 +26,9 @@ import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.state.action.EngineAction
|
|
||||||
import mozilla.components.browser.state.action.RecentlyClosedAction
|
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
|
||||||
import mozilla.components.concept.engine.prompt.ShareData
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
import mozilla.components.lib.state.ext.flowScoped
|
import mozilla.components.lib.state.ext.flowScoped
|
||||||
|
@ -48,11 +46,12 @@ import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider
|
import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider
|
||||||
import org.mozilla.fenix.databinding.FragmentHistoryBinding
|
import org.mozilla.fenix.databinding.FragmentHistoryBinding
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.getRootView
|
||||||
import org.mozilla.fenix.ext.nav
|
import org.mozilla.fenix.ext.nav
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.ext.runIfFragmentIsAttached
|
||||||
import org.mozilla.fenix.ext.setTextColor
|
import org.mozilla.fenix.ext.setTextColor
|
||||||
import org.mozilla.fenix.ext.toShortUrl
|
import org.mozilla.fenix.ext.toShortUrl
|
||||||
import org.mozilla.fenix.ext.getRootView
|
|
||||||
import org.mozilla.fenix.library.LibraryPageFragment
|
import org.mozilla.fenix.library.LibraryPageFragment
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
|
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
|
||||||
|
@ -100,13 +99,16 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
|
||||||
val historyController: HistoryController = DefaultHistoryController(
|
val historyController: HistoryController = DefaultHistoryController(
|
||||||
store = historyStore,
|
store = historyStore,
|
||||||
appStore = requireContext().components.appStore,
|
appStore = requireContext().components.appStore,
|
||||||
|
browserStore = requireComponents.core.store,
|
||||||
|
historyStorage = requireComponents.core.historyStorage,
|
||||||
historyProvider = historyProvider,
|
historyProvider = historyProvider,
|
||||||
navController = findNavController(),
|
navController = findNavController(),
|
||||||
scope = lifecycleScope,
|
scope = lifecycleScope,
|
||||||
openToBrowser = ::openItem,
|
openToBrowser = ::openItem,
|
||||||
displayDeleteAll = ::displayDeleteAllDialog,
|
displayDeleteTimeRange = ::displayDeleteTimeRange,
|
||||||
invalidateOptionsMenu = ::invalidateOptionsMenu,
|
invalidateOptionsMenu = ::invalidateOptionsMenu,
|
||||||
deleteSnackbar = :: deleteSnackbar,
|
deleteSnackbar = ::deleteSnackbar,
|
||||||
|
onTimeFrameDeleted = ::onTimeFrameDeleted,
|
||||||
syncHistory = ::syncHistory,
|
syncHistory = ::syncHistory,
|
||||||
settings = requireContext().components.settings,
|
settings = requireContext().components.settings,
|
||||||
)
|
)
|
||||||
|
@ -173,6 +175,16 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onTimeFrameDeleted() {
|
||||||
|
runIfFragmentIsAttached {
|
||||||
|
historyView.historyAdapter.refresh()
|
||||||
|
showSnackBar(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.preferences_delete_browsing_data_snackbar)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
@ -280,8 +292,8 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
|
||||||
historyInteractor.onSearch()
|
historyInteractor.onSearch()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.history_delete_all -> {
|
R.id.history_delete -> {
|
||||||
historyInteractor.onDeleteAll()
|
historyInteractor.onDeleteTimeRange()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
@ -331,40 +343,10 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayDeleteAllDialog() {
|
private fun displayDeleteTimeRange() {
|
||||||
activity?.let { activity ->
|
DeleteConfirmationDialogFragment(
|
||||||
AlertDialog.Builder(activity).apply {
|
historyInteractor = historyInteractor
|
||||||
setMessage(R.string.delete_browsing_data_prompt_message)
|
).show(childFragmentManager, null)
|
||||||
setNegativeButton(R.string.delete_browsing_data_prompt_cancel) { dialog: DialogInterface, _ ->
|
|
||||||
dialog.cancel()
|
|
||||||
}
|
|
||||||
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
|
|
||||||
historyStore.dispatch(HistoryFragmentAction.EnterDeletionMode)
|
|
||||||
// Use fragment's lifecycle; the view may be gone by the time dialog is interacted with.
|
|
||||||
lifecycleScope.launch(IO) {
|
|
||||||
GleanHistory.removedAll.record(NoExtras())
|
|
||||||
requireComponents.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
|
|
||||||
requireComponents.core.historyStorage.deleteEverything()
|
|
||||||
deleteOpenTabsEngineHistory(requireComponents.core.store)
|
|
||||||
launch(Main) {
|
|
||||||
historyView.historyAdapter.refresh()
|
|
||||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
|
||||||
showSnackBar(
|
|
||||||
requireView(),
|
|
||||||
getString(R.string.preferences_delete_browsing_data_snackbar)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
create()
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun deleteOpenTabsEngineHistory(store: BrowserStore) {
|
|
||||||
store.dispatch(EngineAction.PurgeHistoryAction).join()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun share(data: List<ShareData>) {
|
private fun share(data: List<ShareData>) {
|
||||||
|
@ -389,6 +371,33 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
|
||||||
historyView.historyAdapter.refresh()
|
historyView.historyAdapter.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class DeleteConfirmationDialogFragment(
|
||||||
|
private val historyInteractor: HistoryInteractor
|
||||||
|
) : DialogFragment() {
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||||
|
AlertDialog.Builder(requireContext()).apply {
|
||||||
|
val layout = LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.delete_history_time_range_dialog, null)
|
||||||
|
val radioGroup = layout.findViewById<RadioGroup>(R.id.radio_group)
|
||||||
|
radioGroup.check(R.id.last_hour_button)
|
||||||
|
setView(layout)
|
||||||
|
|
||||||
|
setNegativeButton(R.string.delete_browsing_data_prompt_cancel) { dialog: DialogInterface, _ ->
|
||||||
|
dialog.cancel()
|
||||||
|
}
|
||||||
|
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
|
||||||
|
val selectedTimeFrame = when (radioGroup.checkedRadioButtonId) {
|
||||||
|
R.id.last_hour_button -> RemoveTimeFrame.LastHour
|
||||||
|
R.id.today_and_yesterday_button -> RemoveTimeFrame.TodayAndYesterday
|
||||||
|
R.id.everything_button -> null
|
||||||
|
else -> throw IllegalStateException("Unexpected radioButtonId")
|
||||||
|
}
|
||||||
|
historyInteractor.onDeleteTimeRangeConfirmed(selectedTimeFrame)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}.create()
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UnusedPrivateMember")
|
@Suppress("UnusedPrivateMember")
|
||||||
companion object {
|
companion object {
|
||||||
private const val PAGE_SIZE = 25
|
private const val PAGE_SIZE = 25
|
||||||
|
|
|
@ -28,9 +28,9 @@ interface HistoryInteractor : SelectionInteractor<History> {
|
||||||
fun onSearch()
|
fun onSearch()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when delete all is tapped
|
* Called when the delete menu button is tapped.
|
||||||
*/
|
*/
|
||||||
fun onDeleteAll()
|
fun onDeleteTimeRange()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when multiple history items are deleted
|
* Called when multiple history items are deleted
|
||||||
|
@ -38,6 +38,14 @@ interface HistoryInteractor : SelectionInteractor<History> {
|
||||||
*/
|
*/
|
||||||
fun onDeleteSome(items: Set<History>)
|
fun onDeleteSome(items: Set<History>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user has confirmed deletion of a time range.
|
||||||
|
*
|
||||||
|
* @param timeFrame The selected timeframe. `null` means no specific time frame has been
|
||||||
|
* selected; should remove everything.
|
||||||
|
*/
|
||||||
|
fun onDeleteTimeRangeConfirmed(timeFrame: RemoveTimeFrame?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user requests a sync of the history
|
* Called when the user requests a sync of the history
|
||||||
*/
|
*/
|
||||||
|
@ -86,14 +94,18 @@ class DefaultHistoryInteractor(
|
||||||
historyController.handleSearch()
|
historyController.handleSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteAll() {
|
override fun onDeleteTimeRange() {
|
||||||
historyController.handleDeleteAll()
|
historyController.handleDeleteTimeRange()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteSome(items: Set<History>) {
|
override fun onDeleteSome(items: Set<History>) {
|
||||||
historyController.handleDeleteSome(items)
|
historyController.handleDeleteSome(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDeleteTimeRangeConfirmed(timeFrame: RemoveTimeFrame?) {
|
||||||
|
historyController.handleDeleteTimeRangeConfirmed(timeFrame)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onRequestSync() {
|
override fun onRequestSync() {
|
||||||
historyController.handleRequestSync()
|
historyController.handleRequestSync()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.history
|
||||||
|
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class that provides starting and ending timestamps for a set time frame. Is used by
|
||||||
|
* [HistoryFragment] to provide timestamps for options inside the delete history dialog.
|
||||||
|
*/
|
||||||
|
enum class RemoveTimeFrame {
|
||||||
|
LastHour,
|
||||||
|
TodayAndYesterday;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides starting and ending timestamps for a set time frame. Each call is calculated at the
|
||||||
|
* moment of execution, which is different from [HistoryItemTimeGroup] implementation.
|
||||||
|
*/
|
||||||
|
fun toLongRange(): LongRange {
|
||||||
|
return when (this) {
|
||||||
|
LastHour -> LongRange(getHourAgo(hoursAgo = 1).time, Long.MAX_VALUE)
|
||||||
|
TodayAndYesterday -> LongRange(getDaysAgo(daysAgo = 1).time, Long.MAX_VALUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHourAgo(hoursAgo: Int): Date {
|
||||||
|
return Calendar.getInstance().apply {
|
||||||
|
add(Calendar.HOUR_OF_DAY, -hoursAgo)
|
||||||
|
}.time
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDaysAgo(daysAgo: Int): Date {
|
||||||
|
return Calendar.getInstance().apply {
|
||||||
|
set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
set(Calendar.MINUTE, 0)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
add(Calendar.DAY_OF_YEAR, -daysAgo)
|
||||||
|
}.time
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,6 +98,7 @@ class HistoryMetadataGroupFragment :
|
||||||
store = historyMetadataGroupStore,
|
store = historyMetadataGroupStore,
|
||||||
selectOrAddUseCase = requireComponents.useCases.tabsUseCases.selectOrAddTab,
|
selectOrAddUseCase = requireComponents.useCases.tabsUseCases.selectOrAddTab,
|
||||||
navController = findNavController(),
|
navController = findNavController(),
|
||||||
|
scope = CoroutineScope(Dispatchers.IO),
|
||||||
searchTerm = args.title,
|
searchTerm = args.title,
|
||||||
deleteSnackbar = :: deleteSnackbar,
|
deleteSnackbar = :: deleteSnackbar,
|
||||||
promptDeleteAll = :: promptDeleteAll,
|
promptDeleteAll = :: promptDeleteAll,
|
||||||
|
@ -194,8 +195,8 @@ class HistoryMetadataGroupFragment :
|
||||||
showTabTray()
|
showTabTray()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.history_delete_all -> {
|
R.id.history_delete -> {
|
||||||
interactor.onDeleteAllMenuItem()
|
interactor.onDeleteAll()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
@ -218,14 +219,14 @@ class HistoryMetadataGroupFragment :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun promptDeleteAll(delete: () -> Unit) {
|
private fun promptDeleteAll() {
|
||||||
if (childFragmentManager.findFragmentByTag(DeleteAllConfirmationDialogFragment.TAG)
|
if (childFragmentManager.findFragmentByTag(DeleteAllConfirmationDialogFragment.TAG)
|
||||||
as? DeleteAllConfirmationDialogFragment != null
|
as? DeleteAllConfirmationDialogFragment != null
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteAllConfirmationDialogFragment(delete).show(
|
DeleteAllConfirmationDialogFragment(interactor, args.title).show(
|
||||||
childFragmentManager, DeleteAllConfirmationDialogFragment.TAG
|
childFragmentManager, DeleteAllConfirmationDialogFragment.TAG
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -254,15 +255,23 @@ class HistoryMetadataGroupFragment :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DeleteAllConfirmationDialogFragment(private val delete: () -> Unit) : DialogFragment() {
|
internal class DeleteAllConfirmationDialogFragment(
|
||||||
|
private val interactor: HistoryMetadataGroupInteractor,
|
||||||
|
private val groupName: String
|
||||||
|
) : DialogFragment() {
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.delete_history_group_prompt_message)
|
.setMessage(
|
||||||
|
String.format(
|
||||||
|
getString(R.string.delete_all_history_group_prompt_message),
|
||||||
|
groupName
|
||||||
|
)
|
||||||
|
)
|
||||||
.setNegativeButton(R.string.delete_history_group_prompt_cancel) { dialog: DialogInterface, _ ->
|
.setNegativeButton(R.string.delete_history_group_prompt_cancel) { dialog: DialogInterface, _ ->
|
||||||
dialog.cancel()
|
dialog.cancel()
|
||||||
}
|
}
|
||||||
.setPositiveButton(R.string.delete_history_group_prompt_allow) { dialog: DialogInterface, _ ->
|
.setPositiveButton(R.string.delete_history_group_prompt_allow) { dialog: DialogInterface, _ ->
|
||||||
delete.invoke()
|
interactor.onDeleteAllConfirmed()
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
|
|
|
@ -7,7 +7,6 @@ package org.mozilla.fenix.library.historymetadata.controller
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.state.action.HistoryMetadataAction
|
import mozilla.components.browser.state.action.HistoryMetadataAction
|
||||||
|
@ -22,6 +21,7 @@ import org.mozilla.fenix.ext.components
|
||||||
import mozilla.components.service.glean.private.NoExtras
|
import mozilla.components.service.glean.private.NoExtras
|
||||||
import org.mozilla.fenix.library.history.History
|
import org.mozilla.fenix.library.history.History
|
||||||
import org.mozilla.fenix.library.history.toPendingDeletionHistory
|
import org.mozilla.fenix.library.history.toPendingDeletionHistory
|
||||||
|
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragment.DeleteAllConfirmationDialogFragment
|
||||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentAction
|
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentAction
|
||||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
|
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
|
||||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentStore
|
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentStore
|
||||||
|
@ -74,9 +74,14 @@ interface HistoryMetadataGroupController {
|
||||||
fun handleDelete(items: Set<History.Metadata>)
|
fun handleDelete(items: Set<History.Metadata>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all the history metadata items in this group.
|
* Displays a [DeleteAllConfirmationDialogFragment] prompt.
|
||||||
*/
|
*/
|
||||||
fun handleDeleteAll()
|
fun handleDeleteAll()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes history metadata items in this group.
|
||||||
|
*/
|
||||||
|
fun handleDeleteAllConfirmed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,13 +95,14 @@ class DefaultHistoryMetadataGroupController(
|
||||||
private val store: HistoryMetadataGroupFragmentStore,
|
private val store: HistoryMetadataGroupFragmentStore,
|
||||||
private val selectOrAddUseCase: TabsUseCases.SelectOrAddUseCase,
|
private val selectOrAddUseCase: TabsUseCases.SelectOrAddUseCase,
|
||||||
private val navController: NavController,
|
private val navController: NavController,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
private val searchTerm: String,
|
private val searchTerm: String,
|
||||||
private val deleteSnackbar: (
|
private val deleteSnackbar: (
|
||||||
items: Set<History.Metadata>,
|
items: Set<History.Metadata>,
|
||||||
undo: suspend (Set<History.Metadata>) -> Unit,
|
undo: suspend (Set<History.Metadata>) -> Unit,
|
||||||
delete: (Set<History.Metadata>) -> suspend (context: Context) -> Unit
|
delete: (Set<History.Metadata>) -> suspend (context: Context) -> Unit
|
||||||
) -> Unit,
|
) -> Unit,
|
||||||
private val promptDeleteAll: (() -> Unit) -> Unit,
|
private val promptDeleteAll: () -> Unit,
|
||||||
private val allDeletedSnackbar: () -> Unit
|
private val allDeletedSnackbar: () -> Unit
|
||||||
) : HistoryMetadataGroupController {
|
) : HistoryMetadataGroupController {
|
||||||
|
|
||||||
|
@ -144,7 +150,7 @@ class DefaultHistoryMetadataGroupController(
|
||||||
|
|
||||||
private fun delete(items: Set<History.Metadata>): suspend (context: Context) -> Unit {
|
private fun delete(items: Set<History.Metadata>): suspend (context: Context) -> Unit {
|
||||||
return { context ->
|
return { context ->
|
||||||
CoroutineScope(IO).launch {
|
scope.launch {
|
||||||
val isDeletingLastItem = items.containsAll(store.state.items)
|
val isDeletingLastItem = items.containsAll(store.state.items)
|
||||||
items.forEach {
|
items.forEach {
|
||||||
store.dispatch(HistoryMetadataGroupFragmentAction.Delete(it))
|
store.dispatch(HistoryMetadataGroupFragmentAction.Delete(it))
|
||||||
|
@ -163,11 +169,11 @@ class DefaultHistoryMetadataGroupController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeleteAll() {
|
override fun handleDeleteAll() {
|
||||||
promptDeleteAll.invoke(::deleteAll)
|
promptDeleteAll.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteAll() {
|
override fun handleDeleteAllConfirmed() {
|
||||||
CoroutineScope(IO).launch {
|
scope.launch {
|
||||||
store.dispatch(HistoryMetadataGroupFragmentAction.DeleteAll)
|
store.dispatch(HistoryMetadataGroupFragmentAction.DeleteAll)
|
||||||
store.state.items.forEach {
|
store.state.items.forEach {
|
||||||
historyStorage.deleteVisitsFor(it.url)
|
historyStorage.deleteVisitsFor(it.url)
|
||||||
|
|
|
@ -29,10 +29,14 @@ interface HistoryMetadataGroupInteractor : SelectionInteractor<History.Metadata>
|
||||||
fun onDelete(items: Set<History.Metadata>)
|
fun onDelete(items: Set<History.Metadata>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all the history items in the history metadata group. Called when a user clicks
|
* Called when a user clicks on the "Delete history" menu item.
|
||||||
* on the "Delete history" menu item.
|
|
||||||
*/
|
*/
|
||||||
fun onDeleteAllMenuItem()
|
fun onDeleteAll()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a user has confirmed the deletion of the group.
|
||||||
|
*/
|
||||||
|
fun onDeleteAllConfirmed()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the share sheet for a set of history [items]. Called when a user clicks on the
|
* Opens the share sheet for a set of history [items]. Called when a user clicks on the
|
||||||
|
@ -70,10 +74,14 @@ class DefaultHistoryMetadataGroupInteractor(
|
||||||
controller.handleDelete(items)
|
controller.handleDelete(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteAllMenuItem() {
|
override fun onDeleteAll() {
|
||||||
controller.handleDeleteAll()
|
controller.handleDeleteAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDeleteAllConfirmed() {
|
||||||
|
controller.handleDeleteAllConfirmed()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShareMenuItem(items: Set<History.Metadata>) {
|
override fun onShareMenuItem(items: Set<History.Metadata>) {
|
||||||
controller.handleShare(items)
|
controller.handleShare(items)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?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"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:text="@string/delete_history_prompt_title"
|
||||||
|
android:textColor="?attr/textPrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/body"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:text="@string/delete_history_prompt_body"
|
||||||
|
android:textColor="?attr/textPrimary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/title" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/radio_group"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/body">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/last_hour_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:buttonTint="?attr/textSecondary"
|
||||||
|
android:layout_marginStart="18dp"
|
||||||
|
android:paddingStart="32dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:text="@string/delete_history_prompt_button_last_hour"
|
||||||
|
android:textColor="?attr/textPrimary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/today_and_yesterday_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:buttonTint="?attr/textSecondary"
|
||||||
|
android:layout_marginStart="18dp"
|
||||||
|
android:paddingStart="32dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:text="@string/delete_history_prompt_button_today_and_yesterday"
|
||||||
|
android:textColor="?attr/textPrimary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/everything_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:buttonTint="?attr/textSecondary"
|
||||||
|
android:layout_marginStart="18dp"
|
||||||
|
android:paddingStart="32dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:text="@string/delete_history_prompt_button_everything"
|
||||||
|
android:textColor="?attr/textPrimary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</RadioGroup>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -11,7 +11,7 @@
|
||||||
app:iconTint="?attr/textPrimary"
|
app:iconTint="?attr/textPrimary"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/history_delete_all"
|
android:id="@+id/history_delete"
|
||||||
android:icon="@drawable/ic_delete"
|
android:icon="@drawable/ic_delete"
|
||||||
android:title="@string/history_delete_all"
|
android:title="@string/history_delete_all"
|
||||||
app:iconTint="?attr/textPrimary"
|
app:iconTint="?attr/textPrimary"
|
||||||
|
|
|
@ -1118,7 +1118,18 @@
|
||||||
<string name="delete_browsing_data_on_quit_action">Quit</string>
|
<string name="delete_browsing_data_on_quit_action">Quit</string>
|
||||||
|
|
||||||
<!-- Dialog message to the user asking to delete browsing data. -->
|
<!-- Dialog message to the user asking to delete browsing data. -->
|
||||||
<string name="delete_browsing_data_prompt_message">This will delete all of your browsing data.</string>
|
<string moz:removedIn="105" name="delete_browsing_data_prompt_message" tools:ignore="UnusedResources">This will delete all of your browsing data.</string>
|
||||||
|
<!-- Title text of a delete browsing data dialog. -->
|
||||||
|
<string name="delete_history_prompt_title">Time range to delete</string>
|
||||||
|
<!-- Body text of a delete browsing data dialog. -->
|
||||||
|
<string name="delete_history_prompt_body">Removes history (including history synced from other devices), cookies and other browsing data.</string>
|
||||||
|
<!-- Radio button of a delete browsing data dialog, to delete history items for the last hour. -->
|
||||||
|
<string name="delete_history_prompt_button_last_hour">Last hour</string>
|
||||||
|
<!-- Radio button of a delete browsing data dialog, to delete history items for today and yesterday. -->
|
||||||
|
<string name="delete_history_prompt_button_today_and_yesterday">Today and yesterday</string>
|
||||||
|
<!-- Radio button of a delete browsing data dialog, to delete all history. -->
|
||||||
|
<string name="delete_history_prompt_button_everything">Everything</string>
|
||||||
|
|
||||||
<!-- Dialog message to the user asking to delete browsing data. Parameter will be replaced by app name. -->
|
<!-- Dialog message to the user asking to delete browsing data. Parameter will be replaced by app name. -->
|
||||||
<string name="delete_browsing_data_prompt_message_3">%s will delete the selected browsing data.</string>
|
<string name="delete_browsing_data_prompt_message_3">%s will delete the selected browsing data.</string>
|
||||||
<!-- Text for the cancel button for the data deletion dialog -->
|
<!-- Text for the cancel button for the data deletion dialog -->
|
||||||
|
@ -1131,7 +1142,7 @@
|
||||||
<string name="deleting_browsing_data_in_progress">Deleting browsing data…</string>
|
<string name="deleting_browsing_data_in_progress">Deleting browsing data…</string>
|
||||||
|
|
||||||
<!-- Dialog message to the user asking to delete all history items inside the opened group. -->
|
<!-- Dialog message to the user asking to delete all history items inside the opened group. -->
|
||||||
<string name="delete_history_group_prompt_message">This will delete all items.</string>
|
<string name="delete_all_history_group_prompt_message">Delete all sites in \"%s\"</string>
|
||||||
<!-- Text for the cancel button for the history group deletion dialog -->
|
<!-- Text for the cancel button for the history group deletion dialog -->
|
||||||
<string name="delete_history_group_prompt_cancel">Cancel</string>
|
<string name="delete_history_group_prompt_cancel">Cancel</string>
|
||||||
<!-- Text for the allow button for the history group dialog -->
|
<!-- Text for the allow button for the history group dialog -->
|
||||||
|
|
|
@ -9,6 +9,9 @@ import io.mockk.coVerifyOrder
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import mozilla.components.browser.state.action.RecentlyClosedAction
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
|
@ -40,6 +43,8 @@ class HistoryControllerTest {
|
||||||
|
|
||||||
private val store: HistoryFragmentStore = mockk(relaxed = true)
|
private val store: HistoryFragmentStore = mockk(relaxed = true)
|
||||||
private val appStore: AppStore = mockk(relaxed = true)
|
private val appStore: AppStore = mockk(relaxed = true)
|
||||||
|
private val browserStore: BrowserStore = mockk(relaxed = true)
|
||||||
|
private val historyStorage: PlacesHistoryStorage = mockk(relaxed = true)
|
||||||
private val state: HistoryFragmentState = mockk(relaxed = true)
|
private val state: HistoryFragmentState = mockk(relaxed = true)
|
||||||
private val navController: NavController = mockk(relaxed = true)
|
private val navController: NavController = mockk(relaxed = true)
|
||||||
private val historyProvider: DefaultPagedHistoryProvider = mockk(relaxed = true)
|
private val historyProvider: DefaultPagedHistoryProvider = mockk(relaxed = true)
|
||||||
|
@ -139,16 +144,29 @@ class HistoryControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun onDeleteAll() {
|
fun onDeleteTimeRange() {
|
||||||
var displayDeleteAllInvoked = false
|
var displayDeleteTimeRangeInvoked = false
|
||||||
val controller = createController(
|
val controller = createController(
|
||||||
displayDeleteAll = {
|
displayDeleteTimeRange = {
|
||||||
displayDeleteAllInvoked = true
|
displayDeleteTimeRangeInvoked = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
controller.handleDeleteAll()
|
controller.handleDeleteTimeRange()
|
||||||
assertTrue(displayDeleteAllInvoked)
|
assertTrue(displayDeleteTimeRangeInvoked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onDeleteTimeRangeConfirmed() {
|
||||||
|
val controller = createController()
|
||||||
|
|
||||||
|
controller.handleDeleteTimeRangeConfirmed(null)
|
||||||
|
coVerifyOrder {
|
||||||
|
store.dispatch(HistoryFragmentAction.EnterDeletionMode)
|
||||||
|
historyStorage.deleteEverything()
|
||||||
|
browserStore.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
|
||||||
|
store.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -185,7 +203,8 @@ class HistoryControllerTest {
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
private fun createController(
|
private fun createController(
|
||||||
openInBrowser: (History) -> Unit = { _ -> },
|
openInBrowser: (History) -> Unit = { _ -> },
|
||||||
displayDeleteAll: () -> Unit = {},
|
displayDeleteTimeRange: () -> Unit = {},
|
||||||
|
onTimeFrameDeleted: () -> Unit = {},
|
||||||
invalidateOptionsMenu: () -> Unit = {},
|
invalidateOptionsMenu: () -> Unit = {},
|
||||||
deleteHistoryItems: (Set<History>) -> Unit = { _ -> },
|
deleteHistoryItems: (Set<History>) -> Unit = { _ -> },
|
||||||
syncHistory: suspend () -> Unit = {}
|
syncHistory: suspend () -> Unit = {}
|
||||||
|
@ -193,11 +212,14 @@ class HistoryControllerTest {
|
||||||
return DefaultHistoryController(
|
return DefaultHistoryController(
|
||||||
store,
|
store,
|
||||||
appStore,
|
appStore,
|
||||||
|
browserStore,
|
||||||
|
historyStorage,
|
||||||
historyProvider,
|
historyProvider,
|
||||||
navController,
|
navController,
|
||||||
scope,
|
scope,
|
||||||
openInBrowser,
|
openInBrowser,
|
||||||
displayDeleteAll,
|
displayDeleteTimeRange,
|
||||||
|
onTimeFrameDeleted,
|
||||||
invalidateOptionsMenu,
|
invalidateOptionsMenu,
|
||||||
{ items, _, _ -> deleteHistoryItems.invoke(items) },
|
{ items, _, _ -> deleteHistoryItems.invoke(items) },
|
||||||
syncHistory,
|
syncHistory,
|
||||||
|
|
|
@ -75,11 +75,20 @@ class HistoryInteractorTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun onDeleteAll() {
|
fun onDeleteTimeRange() {
|
||||||
interactor.onDeleteAll()
|
interactor.onDeleteTimeRange()
|
||||||
|
|
||||||
verifyAll {
|
verifyAll {
|
||||||
controller.handleDeleteAll()
|
controller.handleDeleteTimeRange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onDeleteTimeRangeConfirmed() {
|
||||||
|
interactor.onDeleteTimeRangeConfirmed(null)
|
||||||
|
|
||||||
|
verifyAll {
|
||||||
|
controller.handleDeleteTimeRangeConfirmed(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/* 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.history
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class RemoveTimeFrameTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN LastHour is calculated THEN first timeStamp is one hour ago`() {
|
||||||
|
val lastHourRange = RemoveTimeFrame.LastHour.toLongRange()
|
||||||
|
val nowMillis = Calendar.getInstance().timeInMillis
|
||||||
|
val millisDif = nowMillis - lastHourRange.first
|
||||||
|
val hourDif = TimeUnit.HOURS.convert(millisDif, TimeUnit.MILLISECONDS)
|
||||||
|
Assert.assertEquals(1, hourDif)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN LastHour is calculated THEN second timeStamp is equal or greater than now`() {
|
||||||
|
val lastHourRange = RemoveTimeFrame.LastHour.toLongRange()
|
||||||
|
val nowMillis = Calendar.getInstance().timeInMillis
|
||||||
|
Assert.assertTrue(nowMillis <= lastHourRange.last)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN TodayAndYesterday is calculated THEN first timeStamp is one day ago`() {
|
||||||
|
val lastHourRange = RemoveTimeFrame.TodayAndYesterday.toLongRange()
|
||||||
|
val nowMillis = Calendar.getInstance().timeInMillis
|
||||||
|
val millisDif = nowMillis - lastHourRange.first
|
||||||
|
val daysDif = TimeUnit.DAYS.convert(millisDif, TimeUnit.MILLISECONDS)
|
||||||
|
Assert.assertEquals(1, daysDif)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN TodayAndYesterday is calculated THEN first timeStamp is the start of the previous day`() {
|
||||||
|
val todayAndYesterdayRange = RemoveTimeFrame.TodayAndYesterday.toLongRange()
|
||||||
|
val yesterdayStartMillis = Calendar.getInstance().apply {
|
||||||
|
add(Calendar.DAY_OF_YEAR, -1)
|
||||||
|
set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
set(Calendar.MINUTE, 0)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}.timeInMillis
|
||||||
|
Assert.assertEquals(yesterdayStartMillis, todayAndYesterdayRange.first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN TodayAndYesterday is calculated THEN second timeStamp is equal or greater than now`() {
|
||||||
|
val lastHourRange = RemoveTimeFrame.TodayAndYesterday.toLongRange()
|
||||||
|
val nowMillis = Calendar.getInstance().timeInMillis
|
||||||
|
Assert.assertTrue(nowMillis <= lastHourRange.last)
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,11 +23,10 @@ import mozilla.components.support.test.rule.MainCoroutineRule
|
||||||
import mozilla.components.support.test.rule.runTestOnMain
|
import mozilla.components.support.test.rule.runTestOnMain
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
@ -90,23 +89,7 @@ class HistoryMetadataGroupControllerTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
controller = DefaultHistoryMetadataGroupController(
|
controller = createController()
|
||||||
historyStorage = historyStorage,
|
|
||||||
browserStore = browserStore,
|
|
||||||
appStore = appStore,
|
|
||||||
store = store,
|
|
||||||
selectOrAddUseCase = selectOrAddUseCase,
|
|
||||||
navController = navController,
|
|
||||||
searchTerm = "mozilla",
|
|
||||||
deleteSnackbar = { items, _, delete ->
|
|
||||||
scope.launch {
|
|
||||||
delete(items).invoke(context)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
promptDeleteAll = { deleteAll -> deleteAll.invoke() },
|
|
||||||
allDeletedSnackbar = {}
|
|
||||||
)
|
|
||||||
|
|
||||||
every { activity.components.core.historyStorage } returns historyStorage
|
every { activity.components.core.historyStorage } returns historyStorage
|
||||||
every { context.components.core.store } returns browserStore
|
every { context.components.core.store } returns browserStore
|
||||||
every { context.components.core.historyStorage } returns historyStorage
|
every { context.components.core.historyStorage } returns historyStorage
|
||||||
|
@ -189,7 +172,6 @@ class HistoryMetadataGroupControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/25167")
|
|
||||||
fun handleDeleteSingle() = runTestOnMain {
|
fun handleDeleteSingle() = runTestOnMain {
|
||||||
assertNull(GleanHistory.searchTermGroupRemoveTab.testGetValue())
|
assertNull(GleanHistory.searchTermGroupRemoveTab.testGetValue())
|
||||||
|
|
||||||
|
@ -218,7 +200,6 @@ class HistoryMetadataGroupControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/25167")
|
|
||||||
fun handleDeleteMultiple() = runTestOnMain {
|
fun handleDeleteMultiple() = runTestOnMain {
|
||||||
assertNull(GleanHistory.searchTermGroupRemoveTab.testGetValue())
|
assertNull(GleanHistory.searchTermGroupRemoveTab.testGetValue())
|
||||||
controller.handleDelete(getMetadataItemsList().toSet())
|
controller.handleDelete(getMetadataItemsList().toSet())
|
||||||
|
@ -244,14 +225,16 @@ class HistoryMetadataGroupControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/25167")
|
|
||||||
fun handleDeleteAbnormal() = runTestOnMain {
|
fun handleDeleteAbnormal() = runTestOnMain {
|
||||||
val abnormalList = listOf(
|
val abnormalList = listOf(
|
||||||
mozillaHistoryMetadataItem,
|
mozillaHistoryMetadataItem,
|
||||||
firefoxHistoryMetadataItem,
|
firefoxHistoryMetadataItem,
|
||||||
mozillaHistoryMetadataItem.copy(title = "Pocket", url = "https://getpocket.com"),
|
mozillaHistoryMetadataItem.copy(title = "Pocket", url = "https://getpocket.com"),
|
||||||
mozillaHistoryMetadataItem.copy(title = "BBC", url = "https://www.bbc.com/"),
|
mozillaHistoryMetadataItem.copy(title = "BBC", url = "https://www.bbc.com/"),
|
||||||
mozillaHistoryMetadataItem.copy(title = "Stackoverflow", url = "https://stackoverflow.com/")
|
mozillaHistoryMetadataItem.copy(
|
||||||
|
title = "Stackoverflow",
|
||||||
|
url = "https://stackoverflow.com/"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assertNull(GleanHistory.searchTermGroupRemoveTab.testGetValue())
|
assertNull(GleanHistory.searchTermGroupRemoveTab.testGetValue())
|
||||||
|
|
||||||
|
@ -290,9 +273,21 @@ class HistoryMetadataGroupControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun handleDeleteAll() = runTestOnMain {
|
fun handleDeleteAll() = runTestOnMain {
|
||||||
|
var promptDeleteAllInvoked = false
|
||||||
|
val controller = createController(
|
||||||
|
promptDeleteAll = {
|
||||||
|
promptDeleteAllInvoked = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
controller.handleDeleteAll()
|
||||||
|
assertTrue(promptDeleteAllInvoked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleDeleteAllConfirmed() = runTestOnMain {
|
||||||
assertNull(GleanHistory.searchTermGroupRemoveAll.testGetValue())
|
assertNull(GleanHistory.searchTermGroupRemoveAll.testGetValue())
|
||||||
|
|
||||||
controller.handleDeleteAll()
|
controller.handleDeleteAllConfirmed()
|
||||||
|
|
||||||
coVerify {
|
coVerify {
|
||||||
store.dispatch(HistoryMetadataGroupFragmentAction.DeleteAll)
|
store.dispatch(HistoryMetadataGroupFragmentAction.DeleteAll)
|
||||||
|
@ -313,4 +308,33 @@ class HistoryMetadataGroupControllerTest {
|
||||||
.single().extra
|
.single().extra
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
private fun createController(
|
||||||
|
deleteSnackbar: (
|
||||||
|
items: Set<History.Metadata>,
|
||||||
|
undo: suspend (Set<History.Metadata>) -> Unit,
|
||||||
|
delete: (Set<History.Metadata>) -> suspend (context: Context) -> Unit
|
||||||
|
) -> Unit = { items, _, delete ->
|
||||||
|
scope.launch {
|
||||||
|
delete(items).invoke(context)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
promptDeleteAll: () -> Unit = {},
|
||||||
|
allDeletedSnackbar: () -> Unit = {}
|
||||||
|
): DefaultHistoryMetadataGroupController {
|
||||||
|
return DefaultHistoryMetadataGroupController(
|
||||||
|
historyStorage = historyStorage,
|
||||||
|
browserStore = browserStore,
|
||||||
|
appStore = appStore,
|
||||||
|
store = store,
|
||||||
|
selectOrAddUseCase = selectOrAddUseCase,
|
||||||
|
navController = navController,
|
||||||
|
scope = scope,
|
||||||
|
searchTerm = searchTerm,
|
||||||
|
deleteSnackbar = deleteSnackbar,
|
||||||
|
promptDeleteAll = promptDeleteAll,
|
||||||
|
allDeletedSnackbar = allDeletedSnackbar
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue