Close #25954: add a new delete time range confirmation dialog for history screen

This commit is contained in:
mike a 2022-07-20 07:54:36 -07:00 committed by mergify[bot]
parent bfd0eb7306
commit 7982c6b79f
15 changed files with 460 additions and 112 deletions

View File

@ -71,7 +71,7 @@ class HistoryRobot {
deleteButton(item).click()
}
fun clickDeleteAllHistoryButton() = deleteAllButton().click()
fun clickDeleteAllHistoryButton() = deleteButton().click()
fun confirmDeleteAllHistory() {
onView(withText("Delete"))
@ -104,7 +104,7 @@ private fun pageUrl() = onView(withId(R.id.url))
private fun deleteButton(title: String) =
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))
@ -147,7 +147,7 @@ private fun assertPageUrl(expectedUrl: Uri) = pageUrl()
.check(matches(withText(Matchers.containsString(expectedUrl.toString()))))
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())
.check(matches(isDisplayed()))

View File

@ -10,7 +10,11 @@ import androidx.navigation.NavOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.EngineAction
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 org.mozilla.fenix.GleanMetrics.Events
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.ext.components
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.library.history.HistoryFragment.DeleteConfirmationDialogFragment
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
@ -30,10 +35,22 @@ interface HistoryController {
fun handleBackPressed(): Boolean
fun handleModeSwitched()
fun handleSearch()
fun handleDeleteAll()
/**
* Displays a [DeleteConfirmationDialogFragment].
*/
fun handleDeleteTimeRange()
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 handleEnterRecentlyClosed()
/**
* Navigates to [org.mozilla.fenix.library.syncedhistory.SyncedHistoryFragment]
*/
@ -44,11 +61,14 @@ interface HistoryController {
class DefaultHistoryController(
private val store: HistoryFragmentStore,
private val appStore: AppStore,
private val browserStore: BrowserStore,
private val historyStorage: PlacesHistoryStorage,
private var historyProvider: DefaultPagedHistoryProvider,
private val navController: NavController,
private val scope: CoroutineScope,
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 deleteSnackbar: (
items: Set<History>,
@ -111,8 +131,8 @@ class DefaultHistoryController(
navController.navigateSafe(R.id.historyFragment, directions)
}
override fun handleDeleteAll() {
displayDeleteAll.invoke()
override fun handleDeleteTimeRange() {
displayDeleteTimeRange.invoke()
}
override fun handleDeleteSome(items: Set<History>) {
@ -121,6 +141,31 @@ class DefaultHistoryController(
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>) {
val pendingDeletionItems = items.map { it.toPendingDeletionHistory() }.toSet()
appStore.dispatch(AppAction.UndoPendingDeletionSet(pendingDeletionItems))

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.library.history
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
@ -14,7 +15,9 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
@ -23,14 +26,9 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.CoroutineScope
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.mapNotNull
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.lib.state.ext.consumeFrom
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.databinding.FragmentHistoryBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
@ -100,13 +99,16 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
val historyController: HistoryController = DefaultHistoryController(
store = historyStore,
appStore = requireContext().components.appStore,
browserStore = requireComponents.core.store,
historyStorage = requireComponents.core.historyStorage,
historyProvider = historyProvider,
navController = findNavController(),
scope = lifecycleScope,
openToBrowser = ::openItem,
displayDeleteAll = ::displayDeleteAllDialog,
displayDeleteTimeRange = ::displayDeleteTimeRange,
invalidateOptionsMenu = ::invalidateOptionsMenu,
deleteSnackbar = :: deleteSnackbar,
deleteSnackbar = ::deleteSnackbar,
onTimeFrameDeleted = ::onTimeFrameDeleted,
syncHistory = ::syncHistory,
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?) {
super.onViewCreated(view, savedInstanceState)
@ -280,8 +292,8 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
historyInteractor.onSearch()
true
}
R.id.history_delete_all -> {
historyInteractor.onDeleteAll()
R.id.history_delete -> {
historyInteractor.onDeleteTimeRange()
true
}
else -> super.onOptionsItemSelected(item)
@ -331,40 +343,10 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
)
}
private fun displayDeleteAllDialog() {
activity?.let { activity ->
AlertDialog.Builder(activity).apply {
setMessage(R.string.delete_browsing_data_prompt_message)
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 displayDeleteTimeRange() {
DeleteConfirmationDialogFragment(
historyInteractor = historyInteractor
).show(childFragmentManager, null)
}
private fun share(data: List<ShareData>) {
@ -389,6 +371,33 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler {
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")
companion object {
private const val PAGE_SIZE = 25

View File

@ -28,9 +28,9 @@ interface HistoryInteractor : SelectionInteractor<History> {
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
@ -38,6 +38,14 @@ interface HistoryInteractor : SelectionInteractor<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
*/
@ -86,14 +94,18 @@ class DefaultHistoryInteractor(
historyController.handleSearch()
}
override fun onDeleteAll() {
historyController.handleDeleteAll()
override fun onDeleteTimeRange() {
historyController.handleDeleteTimeRange()
}
override fun onDeleteSome(items: Set<History>) {
historyController.handleDeleteSome(items)
}
override fun onDeleteTimeRangeConfirmed(timeFrame: RemoveTimeFrame?) {
historyController.handleDeleteTimeRangeConfirmed(timeFrame)
}
override fun onRequestSync() {
historyController.handleRequestSync()
}

View File

@ -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
}
}

View File

@ -98,6 +98,7 @@ class HistoryMetadataGroupFragment :
store = historyMetadataGroupStore,
selectOrAddUseCase = requireComponents.useCases.tabsUseCases.selectOrAddTab,
navController = findNavController(),
scope = CoroutineScope(Dispatchers.IO),
searchTerm = args.title,
deleteSnackbar = :: deleteSnackbar,
promptDeleteAll = :: promptDeleteAll,
@ -194,8 +195,8 @@ class HistoryMetadataGroupFragment :
showTabTray()
true
}
R.id.history_delete_all -> {
interactor.onDeleteAllMenuItem()
R.id.history_delete -> {
interactor.onDeleteAll()
true
}
else -> super.onOptionsItemSelected(item)
@ -218,14 +219,14 @@ class HistoryMetadataGroupFragment :
)
}
private fun promptDeleteAll(delete: () -> Unit) {
private fun promptDeleteAll() {
if (childFragmentManager.findFragmentByTag(DeleteAllConfirmationDialogFragment.TAG)
as? DeleteAllConfirmationDialogFragment != null
) {
return
}
DeleteAllConfirmationDialogFragment(delete).show(
DeleteAllConfirmationDialogFragment(interactor, args.title).show(
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 =
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, _ ->
dialog.cancel()
}
.setPositiveButton(R.string.delete_history_group_prompt_allow) { dialog: DialogInterface, _ ->
delete.invoke()
interactor.onDeleteAllConfirmed()
dialog.dismiss()
}
.create()

View File

@ -7,7 +7,6 @@ package org.mozilla.fenix.library.historymetadata.controller
import android.content.Context
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
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 org.mozilla.fenix.library.history.History
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.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentStore
@ -74,9 +74,14 @@ interface HistoryMetadataGroupController {
fun handleDelete(items: Set<History.Metadata>)
/**
* Deletes all the history metadata items in this group.
* Displays a [DeleteAllConfirmationDialogFragment] prompt.
*/
fun handleDeleteAll()
/**
* Deletes history metadata items in this group.
*/
fun handleDeleteAllConfirmed()
}
/**
@ -90,13 +95,14 @@ class DefaultHistoryMetadataGroupController(
private val store: HistoryMetadataGroupFragmentStore,
private val selectOrAddUseCase: TabsUseCases.SelectOrAddUseCase,
private val navController: NavController,
private val scope: CoroutineScope,
private val searchTerm: String,
private val deleteSnackbar: (
items: Set<History.Metadata>,
undo: suspend (Set<History.Metadata>) -> Unit,
delete: (Set<History.Metadata>) -> suspend (context: Context) -> Unit
) -> Unit,
private val promptDeleteAll: (() -> Unit) -> Unit,
private val promptDeleteAll: () -> Unit,
private val allDeletedSnackbar: () -> Unit
) : HistoryMetadataGroupController {
@ -144,7 +150,7 @@ class DefaultHistoryMetadataGroupController(
private fun delete(items: Set<History.Metadata>): suspend (context: Context) -> Unit {
return { context ->
CoroutineScope(IO).launch {
scope.launch {
val isDeletingLastItem = items.containsAll(store.state.items)
items.forEach {
store.dispatch(HistoryMetadataGroupFragmentAction.Delete(it))
@ -163,11 +169,11 @@ class DefaultHistoryMetadataGroupController(
}
override fun handleDeleteAll() {
promptDeleteAll.invoke(::deleteAll)
promptDeleteAll.invoke()
}
private fun deleteAll() {
CoroutineScope(IO).launch {
override fun handleDeleteAllConfirmed() {
scope.launch {
store.dispatch(HistoryMetadataGroupFragmentAction.DeleteAll)
store.state.items.forEach {
historyStorage.deleteVisitsFor(it.url)

View File

@ -29,10 +29,14 @@ interface HistoryMetadataGroupInteractor : SelectionInteractor<History.Metadata>
fun onDelete(items: Set<History.Metadata>)
/**
* Deletes all the history items in the history metadata group. Called when a user clicks
* on the "Delete history" menu item.
* Called when a user clicks 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
@ -70,10 +74,14 @@ class DefaultHistoryMetadataGroupInteractor(
controller.handleDelete(items)
}
override fun onDeleteAllMenuItem() {
override fun onDeleteAll() {
controller.handleDeleteAll()
}
override fun onDeleteAllConfirmed() {
controller.handleDeleteAllConfirmed()
}
override fun onShareMenuItem(items: Set<History.Metadata>) {
controller.handleShare(items)
}

View File

@ -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>

View File

@ -11,7 +11,7 @@
app:iconTint="?attr/textPrimary"
app:showAsAction="ifRoom" />
<item
android:id="@+id/history_delete_all"
android:id="@+id/history_delete"
android:icon="@drawable/ic_delete"
android:title="@string/history_delete_all"
app:iconTint="?attr/textPrimary"

View File

@ -1118,7 +1118,18 @@
<string name="delete_browsing_data_on_quit_action">Quit</string>
<!-- 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. -->
<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 -->
@ -1131,7 +1142,7 @@
<string name="deleting_browsing_data_in_progress">Deleting browsing data&#8230;</string>
<!-- 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 -->
<string name="delete_history_group_prompt_cancel">Cancel</string>
<!-- Text for the allow button for the history group dialog -->

View File

@ -9,6 +9,9 @@ import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.mockk
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 org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@ -40,6 +43,8 @@ class HistoryControllerTest {
private val store: HistoryFragmentStore = 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 navController: NavController = mockk(relaxed = true)
private val historyProvider: DefaultPagedHistoryProvider = mockk(relaxed = true)
@ -139,16 +144,29 @@ class HistoryControllerTest {
}
@Test
fun onDeleteAll() {
var displayDeleteAllInvoked = false
fun onDeleteTimeRange() {
var displayDeleteTimeRangeInvoked = false
val controller = createController(
displayDeleteAll = {
displayDeleteAllInvoked = true
displayDeleteTimeRange = {
displayDeleteTimeRangeInvoked = true
}
)
controller.handleDeleteAll()
assertTrue(displayDeleteAllInvoked)
controller.handleDeleteTimeRange()
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
@ -185,7 +203,8 @@ class HistoryControllerTest {
@Suppress("LongParameterList")
private fun createController(
openInBrowser: (History) -> Unit = { _ -> },
displayDeleteAll: () -> Unit = {},
displayDeleteTimeRange: () -> Unit = {},
onTimeFrameDeleted: () -> Unit = {},
invalidateOptionsMenu: () -> Unit = {},
deleteHistoryItems: (Set<History>) -> Unit = { _ -> },
syncHistory: suspend () -> Unit = {}
@ -193,11 +212,14 @@ class HistoryControllerTest {
return DefaultHistoryController(
store,
appStore,
browserStore,
historyStorage,
historyProvider,
navController,
scope,
openInBrowser,
displayDeleteAll,
displayDeleteTimeRange,
onTimeFrameDeleted,
invalidateOptionsMenu,
{ items, _, _ -> deleteHistoryItems.invoke(items) },
syncHistory,

View File

@ -75,11 +75,20 @@ class HistoryInteractorTest {
}
@Test
fun onDeleteAll() {
interactor.onDeleteAll()
fun onDeleteTimeRange() {
interactor.onDeleteTimeRange()
verifyAll {
controller.handleDeleteAll()
controller.handleDeleteTimeRange()
}
}
@Test
fun onDeleteTimeRangeConfirmed() {
interactor.onDeleteTimeRangeConfirmed(null)
verifyAll {
controller.handleDeleteTimeRangeConfirmed(null)
}
}

View File

@ -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)
}
}

View File

@ -23,11 +23,10 @@ import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -90,23 +89,7 @@ class HistoryMetadataGroupControllerTest {
@Before
fun setUp() {
controller = DefaultHistoryMetadataGroupController(
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 = {}
)
controller = createController()
every { activity.components.core.historyStorage } returns historyStorage
every { context.components.core.store } returns browserStore
every { context.components.core.historyStorage } returns historyStorage
@ -189,7 +172,6 @@ class HistoryMetadataGroupControllerTest {
}
@Test
@Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/25167")
fun handleDeleteSingle() = runTestOnMain {
assertNull(GleanHistory.searchTermGroupRemoveTab.testGetValue())
@ -218,7 +200,6 @@ class HistoryMetadataGroupControllerTest {
}
@Test
@Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/25167")
fun handleDeleteMultiple() = runTestOnMain {
assertNull(GleanHistory.searchTermGroupRemoveTab.testGetValue())
controller.handleDelete(getMetadataItemsList().toSet())
@ -244,14 +225,16 @@ class HistoryMetadataGroupControllerTest {
}
@Test
@Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/25167")
fun handleDeleteAbnormal() = runTestOnMain {
val abnormalList = listOf(
mozillaHistoryMetadataItem,
firefoxHistoryMetadataItem,
mozillaHistoryMetadataItem.copy(title = "Pocket", url = "https://getpocket.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())
@ -290,9 +273,21 @@ class HistoryMetadataGroupControllerTest {
@Test
fun handleDeleteAll() = runTestOnMain {
var promptDeleteAllInvoked = false
val controller = createController(
promptDeleteAll = {
promptDeleteAllInvoked = true
}
)
controller.handleDeleteAll()
assertTrue(promptDeleteAllInvoked)
}
@Test
fun handleDeleteAllConfirmed() = runTestOnMain {
assertNull(GleanHistory.searchTermGroupRemoveAll.testGetValue())
controller.handleDeleteAll()
controller.handleDeleteAllConfirmed()
coVerify {
store.dispatch(HistoryMetadataGroupFragmentAction.DeleteAll)
@ -313,4 +308,33 @@ class HistoryMetadataGroupControllerTest {
.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
)
}
}