mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-19 09:25:34 +00:00
[fenix] For https://github.com/mozilla-mobile/fenix/issues/352: Delete a download
This commit is contained in:
parent
5344dceead
commit
88375a2294
@ -6,6 +6,7 @@ package org.mozilla.fenix.library.downloads
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.mozilla.fenix.library.SelectionHolder
|
||||
import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder
|
||||
@ -16,6 +17,7 @@ class DownloadAdapter(
|
||||
private var downloads: List<DownloadItem> = listOf()
|
||||
private var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
|
||||
override val selectedItems get() = mode.selectedItems
|
||||
var pendingDeletionIds = emptySet<String>()
|
||||
|
||||
override fun getItemCount(): Int = downloads.size
|
||||
override fun getItemViewType(position: Int): Int = DownloadsListItemViewHolder.LAYOUT_ID
|
||||
@ -27,14 +29,38 @@ class DownloadAdapter(
|
||||
|
||||
fun updateMode(mode: DownloadFragmentState.Mode) {
|
||||
this.mode = mode
|
||||
// Update the delete button alpha that the first item holds
|
||||
if (itemCount > 0) notifyItemChanged(0)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) {
|
||||
holder.bind(downloads[position])
|
||||
val current = downloads[position]
|
||||
val isPendingDeletion = pendingDeletionIds.contains(current.id)
|
||||
holder.bind(downloads[position], position == 0, mode, isPendingDeletion)
|
||||
}
|
||||
|
||||
fun updateDownloads(downloads: List<DownloadItem>) {
|
||||
this.downloads = downloads
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updatePendingDeletionIds(pendingDeletionIds: Set<String>) {
|
||||
this.pendingDeletionIds = pendingDeletionIds
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val downloadDiffCallback = object : DiffUtil.ItemCallback<DownloadItem>() {
|
||||
override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: DownloadItem, newItem: DownloadItem): Any? {
|
||||
return newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,33 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
|
||||
interface DownloadController {
|
||||
fun handleOpen(item: DownloadItem, mode: BrowsingMode? = null)
|
||||
fun handleSelect(item: DownloadItem)
|
||||
fun handleDeselect(item: DownloadItem)
|
||||
fun handleBackPressed(): Boolean
|
||||
fun handleModeSwitched()
|
||||
fun handleDeleteSome(items: Set<DownloadItem>)
|
||||
fun handleDeleteAll()
|
||||
}
|
||||
|
||||
class DefaultDownloadController(
|
||||
private val store: DownloadFragmentStore,
|
||||
private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit
|
||||
private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit,
|
||||
private val displayDeleteAll: () -> Unit,
|
||||
private val invalidateOptionsMenu: () -> Unit,
|
||||
private val deleteDownloadItems: (Set<DownloadItem>) -> Unit
|
||||
) : DownloadController {
|
||||
override fun handleOpen(item: DownloadItem, mode: BrowsingMode?) {
|
||||
openToFileManager(item, mode)
|
||||
}
|
||||
|
||||
override fun handleSelect(item: DownloadItem) {
|
||||
store.dispatch(DownloadFragmentAction.AddItemForRemoval(item))
|
||||
}
|
||||
|
||||
override fun handleDeselect(item: DownloadItem) {
|
||||
store.dispatch(DownloadFragmentAction.RemoveItemForRemoval(item))
|
||||
}
|
||||
|
||||
override fun handleBackPressed(): Boolean {
|
||||
return if (store.state.mode is DownloadFragmentState.Mode.Editing) {
|
||||
store.dispatch(DownloadFragmentAction.ExitEditMode)
|
||||
@ -27,4 +43,16 @@ class DefaultDownloadController(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleModeSwitched() {
|
||||
invalidateOptionsMenu.invoke()
|
||||
}
|
||||
|
||||
override fun handleDeleteAll() {
|
||||
displayDeleteAll.invoke()
|
||||
}
|
||||
|
||||
override fun handleDeleteSome(items: Set<DownloadItem>) {
|
||||
deleteDownloadItems.invoke(items)
|
||||
}
|
||||
}
|
||||
|
@ -4,33 +4,49 @@
|
||||
|
||||
package org.mozilla.fenix.library.downloads
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
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.annotation.VisibleForTesting
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.android.synthetic.main.fragment_downloads.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.state.state.content.DownloadState
|
||||
import mozilla.components.feature.downloads.AbstractFetchDownloadService
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.addons.showSnackBar
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.filterNotExistsOnDisk
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.setTextColor
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.library.LibraryPageFragment
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHandler {
|
||||
private lateinit var downloadStore: DownloadFragmentStore
|
||||
private lateinit var downloadView: DownloadView
|
||||
private lateinit var downloadInteractor: DownloadInteractor
|
||||
private var undoScope: CoroutineScope? = null
|
||||
private var pendingDownloadDeletionJob: (suspend () -> Unit)? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@ -45,14 +61,18 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
|
||||
DownloadFragmentStore(
|
||||
DownloadFragmentState(
|
||||
items = items,
|
||||
mode = DownloadFragmentState.Mode.Normal
|
||||
mode = DownloadFragmentState.Mode.Normal,
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val downloadController: DownloadController = DefaultDownloadController(
|
||||
downloadStore,
|
||||
::openItem
|
||||
::openItem,
|
||||
::displayDeleteAll,
|
||||
::invalidateOptionsMenu,
|
||||
::deleteDownloadItems
|
||||
)
|
||||
downloadInteractor = DownloadInteractor(
|
||||
downloadController
|
||||
@ -82,12 +102,55 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
|
||||
|
||||
override val selectedItems get() = downloadStore.state.mode.selectedItems
|
||||
|
||||
private fun invalidateOptionsMenu() {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
requireComponents.analytics.metrics.track(Event.HistoryOpened)
|
||||
private fun displayDeleteAll() {
|
||||
activity?.let { activity ->
|
||||
AlertDialog.Builder(activity).apply {
|
||||
setMessage(R.string.download_delete_all_dialog)
|
||||
setNegativeButton(R.string.delete_browsing_data_prompt_cancel) { dialog: DialogInterface, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
|
||||
// Use fragment's lifecycle; the view may be gone by the time dialog is interacted with.
|
||||
lifecycleScope.launch(IO) {
|
||||
context.let {
|
||||
it.components.useCases.downloadUseCases.removeAllDownloads()
|
||||
}
|
||||
updatePendingDownloadToDelete(downloadStore.state.items.toSet())
|
||||
launch(Dispatchers.Main) {
|
||||
showSnackBar(
|
||||
requireView(),
|
||||
getString(R.string.download_delete_multiple_items_snackbar)
|
||||
)
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
create()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
setHasOptionsMenu(false)
|
||||
private fun deleteDownloadItems(items: Set<DownloadItem>) {
|
||||
updatePendingDownloadToDelete(items)
|
||||
undoScope = CoroutineScope(IO)
|
||||
undoScope?.allowUndo(
|
||||
requireView(),
|
||||
getMultiSelectSnackBarMessage(items),
|
||||
getString(R.string.bookmark_undo_deletion),
|
||||
{
|
||||
undoPendingDeletion(items)
|
||||
},
|
||||
getDeleteDownloadItemsOperation(items)
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@ -104,7 +167,52 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
|
||||
showToolbar(getString(R.string.library_downloads))
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
val menuRes = when (downloadStore.state.mode) {
|
||||
is DownloadFragmentState.Mode.Normal -> R.menu.library_menu
|
||||
is DownloadFragmentState.Mode.Editing -> R.menu.download_select_multi
|
||||
}
|
||||
inflater.inflate(menuRes, menu)
|
||||
|
||||
menu.findItem(R.id.delete_downloads_multi_select)?.title =
|
||||
SpannableString(getString(R.string.bookmark_menu_delete_button)).apply {
|
||||
setTextColor(requireContext(), R.attr.destructive)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.close_history -> {
|
||||
close()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.delete_downloads_multi_select -> {
|
||||
deleteDownloadItems(downloadStore.state.mode.selectedItems)
|
||||
downloadStore.dispatch(DownloadFragmentAction.ExitEditMode)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun getMultiSelectSnackBarMessage(downloadItems: Set<DownloadItem>): String {
|
||||
return if (downloadItems.size > 1) {
|
||||
getString(R.string.download_delete_multiple_items_snackbar)
|
||||
} else {
|
||||
String.format(
|
||||
requireContext().getString(
|
||||
R.string.history_delete_single_item_snackbar
|
||||
), downloadItems.first().fileName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
invokePendingDeletion()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
invokePendingDeletion()
|
||||
return downloadView.onBackPressed()
|
||||
}
|
||||
|
||||
@ -119,4 +227,41 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeleteDownloadItemsOperation(items: Set<DownloadItem>): (suspend () -> Unit) {
|
||||
return {
|
||||
CoroutineScope(IO).launch {
|
||||
downloadStore.dispatch(DownloadFragmentAction.EnterDeletionMode)
|
||||
context?.let {
|
||||
for (item in items) {
|
||||
it.components.useCases.downloadUseCases.removeDownload(item.id)
|
||||
}
|
||||
}
|
||||
downloadStore.dispatch(DownloadFragmentAction.ExitDeletionMode)
|
||||
pendingDownloadDeletionJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePendingDownloadToDelete(items: Set<DownloadItem>) {
|
||||
pendingDownloadDeletionJob = getDeleteDownloadItemsOperation(items)
|
||||
val ids = items.map { item -> item.id }.toSet()
|
||||
downloadStore.dispatch(DownloadFragmentAction.AddPendingDeletionSet(ids))
|
||||
}
|
||||
|
||||
private fun undoPendingDeletion(items: Set<DownloadItem>) {
|
||||
pendingDownloadDeletionJob = null
|
||||
val ids = items.map { item -> item.id }.toSet()
|
||||
downloadStore.dispatch(DownloadFragmentAction.UndoPendingDeletionSet(ids))
|
||||
}
|
||||
|
||||
private fun invokePendingDeletion() {
|
||||
pendingDownloadDeletionJob?.let {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
it.invoke()
|
||||
}.invokeOnCompletion {
|
||||
pendingDownloadDeletionJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* Class representing a history entry
|
||||
* Class representing a downloads entry
|
||||
* @property id Unique id of the download item
|
||||
* @property fileName File name of the download item
|
||||
* @property filePath Full path of the download item
|
||||
@ -35,8 +35,15 @@ class DownloadFragmentStore(initialState: DownloadFragmentState) :
|
||||
/**
|
||||
* Actions to dispatch through the `DownloadStore` to modify `DownloadState` through the reducer.
|
||||
*/
|
||||
|
||||
sealed class DownloadFragmentAction : Action {
|
||||
object ExitEditMode : DownloadFragmentAction()
|
||||
data class AddItemForRemoval(val item: DownloadItem) : DownloadFragmentAction()
|
||||
data class RemoveItemForRemoval(val item: DownloadItem) : DownloadFragmentAction()
|
||||
data class AddPendingDeletionSet(val itemIds: Set<String>) : DownloadFragmentAction()
|
||||
data class UndoPendingDeletionSet(val itemIds: Set<String>) : DownloadFragmentAction()
|
||||
object EnterDeletionMode : DownloadFragmentAction()
|
||||
object ExitDeletionMode : DownloadFragmentAction()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,7 +53,9 @@ sealed class DownloadFragmentAction : Action {
|
||||
*/
|
||||
data class DownloadFragmentState(
|
||||
val items: List<DownloadItem>,
|
||||
val mode: Mode
|
||||
val mode: Mode,
|
||||
val pendingDeletionIds: Set<String>,
|
||||
val isDeletingItems: Boolean
|
||||
) : State {
|
||||
sealed class Mode {
|
||||
open val selectedItems = emptySet<DownloadItem>()
|
||||
@ -64,6 +73,28 @@ private fun downloadStateReducer(
|
||||
action: DownloadFragmentAction
|
||||
): DownloadFragmentState {
|
||||
return when (action) {
|
||||
is DownloadFragmentAction.AddItemForRemoval ->
|
||||
state.copy(mode = DownloadFragmentState.Mode.Editing(state.mode.selectedItems + action.item))
|
||||
is DownloadFragmentAction.RemoveItemForRemoval -> {
|
||||
val selected = state.mode.selectedItems - action.item
|
||||
state.copy(
|
||||
mode = if (selected.isEmpty()) {
|
||||
DownloadFragmentState.Mode.Normal
|
||||
} else {
|
||||
DownloadFragmentState.Mode.Editing(selected)
|
||||
}
|
||||
)
|
||||
}
|
||||
is DownloadFragmentAction.ExitEditMode -> state.copy(mode = DownloadFragmentState.Mode.Normal)
|
||||
is DownloadFragmentAction.EnterDeletionMode -> state.copy(isDeletingItems = true)
|
||||
is DownloadFragmentAction.ExitDeletionMode -> state.copy(isDeletingItems = false)
|
||||
is DownloadFragmentAction.AddPendingDeletionSet ->
|
||||
state.copy(
|
||||
pendingDeletionIds = state.pendingDeletionIds + action.itemIds
|
||||
)
|
||||
is DownloadFragmentAction.UndoPendingDeletionSet ->
|
||||
state.copy(
|
||||
pendingDeletionIds = state.pendingDeletionIds - action.itemIds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,27 @@ class DownloadInteractor(
|
||||
downloadController.handleOpen(item)
|
||||
}
|
||||
|
||||
override fun select(item: DownloadItem) { /* noop */ }
|
||||
override fun select(item: DownloadItem) {
|
||||
downloadController.handleSelect(item)
|
||||
}
|
||||
|
||||
override fun deselect(item: DownloadItem) { /* noop */ }
|
||||
override fun deselect(item: DownloadItem) {
|
||||
downloadController.handleDeselect(item)
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return downloadController.handleBackPressed()
|
||||
}
|
||||
|
||||
override fun onModeSwitched() {
|
||||
downloadController.handleModeSwitched()
|
||||
}
|
||||
|
||||
override fun onDeleteSome(items: Set<DownloadItem>) {
|
||||
downloadController.handleDeleteSome(items)
|
||||
}
|
||||
|
||||
override fun onDeleteAll() {
|
||||
downloadController.handleDeleteAll()
|
||||
}
|
||||
}
|
||||
|
@ -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.downloads
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import mozilla.components.browser.menu2.BrowserMenuController
|
||||
import mozilla.components.concept.menu.MenuController
|
||||
import mozilla.components.concept.menu.candidate.TextMenuCandidate
|
||||
import mozilla.components.concept.menu.candidate.TextStyle
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class DownloadItemMenu(
|
||||
private val context: Context,
|
||||
private val onItemTapped: (Item) -> Unit
|
||||
) {
|
||||
|
||||
enum class Item {
|
||||
Delete
|
||||
}
|
||||
|
||||
val menuController: MenuController by lazy {
|
||||
BrowserMenuController().apply {
|
||||
submitList(menuItems())
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun menuItems(): List<TextMenuCandidate> {
|
||||
return listOf(
|
||||
TextMenuCandidate(
|
||||
text = context.getString(R.string.history_delete_item),
|
||||
textStyle = TextStyle(
|
||||
color = context.getColorFromAttr(R.attr.destructive)
|
||||
)
|
||||
) {
|
||||
onItemTapped.invoke(Item.Delete)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -12,6 +12,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import kotlinx.android.synthetic.main.component_downloads.*
|
||||
import kotlinx.android.synthetic.main.component_downloads.view.*
|
||||
import kotlinx.android.synthetic.main.component_history.view.progress_bar
|
||||
import kotlinx.android.synthetic.main.component_history.view.swipe_refresh
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.library.LibraryPageView
|
||||
@ -27,6 +29,22 @@ interface DownloadViewInteractor : SelectionInteractor<DownloadItem> {
|
||||
* Called on backpressed to exit edit mode
|
||||
*/
|
||||
fun onBackPressed(): Boolean
|
||||
|
||||
/**
|
||||
* Called when the mode is switched so we can invalidate the menu
|
||||
*/
|
||||
fun onModeSwitched()
|
||||
|
||||
/**
|
||||
* Called when multiple downloads items are deleted
|
||||
* @param items the downloads items to delete
|
||||
*/
|
||||
fun onDeleteSome(items: Set<DownloadItem>)
|
||||
|
||||
/**
|
||||
* Called when all downloads items are deleted
|
||||
*/
|
||||
fun onDeleteAll()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,18 +73,41 @@ class DownloadView(
|
||||
}
|
||||
|
||||
fun update(state: DownloadFragmentState) {
|
||||
val oldMode = mode
|
||||
|
||||
view.progress_bar.isVisible = state.isDeletingItems
|
||||
view.swipe_refresh.isEnabled = false
|
||||
mode = state.mode
|
||||
|
||||
updateEmptyState(state.items.isNotEmpty())
|
||||
downloadAdapter.updatePendingDeletionIds(state.pendingDeletionIds)
|
||||
|
||||
updateEmptyState(state.pendingDeletionIds.size != state.items.size)
|
||||
|
||||
downloadAdapter.updateMode(state.mode)
|
||||
downloadAdapter.updateDownloads(state.items)
|
||||
|
||||
setUiForNormalMode(
|
||||
context.getString(R.string.library_downloads)
|
||||
)
|
||||
if (state.mode::class != oldMode::class) {
|
||||
interactor.onModeSwitched()
|
||||
}
|
||||
|
||||
when (val mode = state.mode) {
|
||||
is DownloadFragmentState.Mode.Normal -> {
|
||||
setUiForNormalMode(
|
||||
context.getString(R.string.library_downloads)
|
||||
)
|
||||
}
|
||||
is DownloadFragmentState.Mode.Editing -> {
|
||||
val unselectedItems = oldMode.selectedItems - state.mode.selectedItems
|
||||
|
||||
state.mode.selectedItems.union(unselectedItems).forEach { item ->
|
||||
val index = state.items.indexOf(item)
|
||||
downloadAdapter.notifyItemChanged(index)
|
||||
}
|
||||
setUiForSelectingMode(
|
||||
context.getString(R.string.download_multi_select_title, mode.selectedItems.size)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEmptyState(userHasDownloads: Boolean) {
|
||||
|
@ -5,16 +5,19 @@
|
||||
package org.mozilla.fenix.library.downloads.viewholders
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.download_list_item.view.*
|
||||
import kotlinx.android.synthetic.main.library_site_item.view.*
|
||||
import mozilla.components.feature.downloads.toMegabyteOrKilobyteString
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.hideAndDisable
|
||||
import org.mozilla.fenix.library.SelectionHolder
|
||||
import org.mozilla.fenix.library.downloads.DownloadInteractor
|
||||
import org.mozilla.fenix.library.downloads.DownloadItem
|
||||
import org.mozilla.fenix.ext.getIcon
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.library.downloads.DownloadFragmentState
|
||||
import org.mozilla.fenix.library.downloads.DownloadItemMenu
|
||||
|
||||
class DownloadsListItemViewHolder(
|
||||
view: View,
|
||||
@ -24,24 +27,77 @@ class DownloadsListItemViewHolder(
|
||||
|
||||
private var item: DownloadItem? = null
|
||||
|
||||
init {
|
||||
setupMenu()
|
||||
|
||||
itemView.delete_downloads_button.setOnClickListener {
|
||||
val selected = selectionHolder.selectedItems
|
||||
if (selected.isEmpty()) {
|
||||
downloadInteractor.onDeleteAll()
|
||||
} else {
|
||||
downloadInteractor.onDeleteSome(selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(
|
||||
item: DownloadItem
|
||||
item: DownloadItem,
|
||||
showDeleteButton: Boolean,
|
||||
mode: DownloadFragmentState.Mode,
|
||||
isPendingDeletion: Boolean = false
|
||||
) {
|
||||
itemView.download_layout.visibility = View.VISIBLE
|
||||
itemView.download_layout.visibility = if (isPendingDeletion) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
itemView.download_layout.titleView.text = item.fileName
|
||||
itemView.download_layout.urlView.text = item.size.toLong().toMegabyteOrKilobyteString()
|
||||
|
||||
toggleTopContent(showDeleteButton, mode == DownloadFragmentState.Mode.Normal)
|
||||
|
||||
itemView.download_layout.setSelectionInteractor(item, selectionHolder, downloadInteractor)
|
||||
itemView.download_layout.changeSelected(item in selectionHolder.selectedItems)
|
||||
|
||||
itemView.overflow_menu.hideAndDisable()
|
||||
itemView.favicon.setImageResource(item.getIcon())
|
||||
itemView.favicon.isClickable = false
|
||||
|
||||
itemView.overflow_menu.showAndEnable()
|
||||
|
||||
this.item = item
|
||||
}
|
||||
|
||||
private fun toggleTopContent(
|
||||
showTopContent: Boolean,
|
||||
isNormalMode: Boolean
|
||||
) {
|
||||
itemView.delete_downloads_button.isVisible = showTopContent
|
||||
|
||||
if (showTopContent) {
|
||||
itemView.delete_downloads_button.run {
|
||||
if (isNormalMode) {
|
||||
isEnabled = true
|
||||
alpha = 1f
|
||||
} else {
|
||||
isEnabled = false
|
||||
alpha = DELETE_BUTTON_DISABLED_ALPHA
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMenu() {
|
||||
val downloadMenu = DownloadItemMenu(itemView.context) {
|
||||
val item = this.item ?: return@DownloadItemMenu
|
||||
|
||||
if (it == DownloadItemMenu.Item.Delete) {
|
||||
downloadInteractor.onDeleteSome(setOf(item))
|
||||
}
|
||||
}
|
||||
itemView.download_layout.attachMenu(downloadMenu.menuController)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DELETE_BUTTON_DISABLED_ALPHA = 0.4f
|
||||
const val LAYOUT_ID = R.layout.download_list_item
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,13 @@
|
||||
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete_downloads_button"
|
||||
style="@style/DestructiveButton"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:text="@string/download_delete_all"
|
||||
android:visibility="gone" />
|
||||
|
||||
<org.mozilla.fenix.library.LibrarySiteItemView
|
||||
android:id="@+id/download_layout"
|
||||
android:layout_width="match_parent"
|
||||
|
11
app/src/main/res/menu/download_select_multi.xml
Normal file
11
app/src/main/res/menu/download_select_multi.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?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/. -->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/delete_downloads_multi_select"
|
||||
android:title="@string/download_delete_item"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
@ -665,11 +665,22 @@
|
||||
<string name="history_empty_message">No history here</string>
|
||||
|
||||
<!-- Downloads -->
|
||||
<!-- Text for the button to clear all downloads -->
|
||||
<string name="download_delete_all">Delete downloads</string>
|
||||
<!-- Text for the dialog to confirm clearing all downloads -->
|
||||
<string name="download_delete_all_dialog">Are you sure you want to clear your downloads?</string>
|
||||
<!-- Text for the snackbar to confirm that multiple downloads items have been deleted -->
|
||||
<string name="download_delete_multiple_items_snackbar">Downloads Deleted</string>
|
||||
<!-- Text shown when no download exists -->
|
||||
<string name="download_empty_message">No downloads here</string>
|
||||
<!-- History multi select title in app bar
|
||||
The first parameter is the number of downloads selected -->
|
||||
<string name="download_multi_select_title">%1$d selected</string>
|
||||
<!-- History overflow menu open in new tab button -->
|
||||
<string name="download_menu_open">Open</string>
|
||||
<!-- Text for the button to delete a single history item -->
|
||||
<string name="download_delete_item">Delete</string>
|
||||
|
||||
|
||||
<!-- Crashes -->
|
||||
<!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) -->
|
||||
|
@ -31,7 +31,7 @@ class DownloadAdapterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getItemCount should return the number of tab collections`() {
|
||||
fun `getItemCount should return the number of downloads`() {
|
||||
val download = mockk<DownloadItem>()
|
||||
|
||||
assertEquals(0, adapter.itemCount)
|
||||
|
@ -26,9 +26,15 @@ class DownloadControllerTest {
|
||||
private val store: DownloadFragmentStore = mockk(relaxed = true)
|
||||
private val state: DownloadFragmentState = mockk(relaxed = true)
|
||||
private val openToFileManager: (DownloadItem, BrowsingMode?) -> Unit = mockk(relaxed = true)
|
||||
private val displayDeleteAll: () -> Unit = mockk(relaxed = true)
|
||||
private val invalidateOptionsMenu: () -> Unit = mockk(relaxed = true)
|
||||
private val deleteDownloadItems: (Set<DownloadItem>) -> Unit = mockk(relaxed = true)
|
||||
private val controller = DefaultDownloadController(
|
||||
store,
|
||||
openToFileManager
|
||||
openToFileManager,
|
||||
displayDeleteAll,
|
||||
invalidateOptionsMenu,
|
||||
deleteDownloadItems
|
||||
)
|
||||
|
||||
@Before
|
||||
@ -65,4 +71,55 @@ class DownloadControllerTest {
|
||||
|
||||
assertFalse(controller.handleBackPressed())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onPressDownloadItemInEditMode() {
|
||||
every { state.mode } returns DownloadFragmentState.Mode.Editing(setOf())
|
||||
|
||||
controller.handleSelect(downloadItem)
|
||||
|
||||
verify {
|
||||
store.dispatch(DownloadFragmentAction.AddItemForRemoval(downloadItem))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onPressSelectedDownloadItemInEditMode() {
|
||||
every { state.mode } returns DownloadFragmentState.Mode.Editing(setOf(downloadItem))
|
||||
|
||||
controller.handleDeselect(downloadItem)
|
||||
|
||||
verify {
|
||||
store.dispatch(DownloadFragmentAction.RemoveItemForRemoval(downloadItem))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onModeSwitched() {
|
||||
controller.handleModeSwitched()
|
||||
|
||||
verify {
|
||||
invalidateOptionsMenu.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDeleteAll() {
|
||||
controller.handleDeleteAll()
|
||||
|
||||
verify {
|
||||
displayDeleteAll.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDeleteSome() {
|
||||
val itemsToDelete = setOf(downloadItem)
|
||||
|
||||
controller.handleDeleteSome(itemsToDelete)
|
||||
|
||||
verify {
|
||||
deleteDownloadItems(itemsToDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,70 @@
|
||||
/* 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.downloads
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.browser.state.state.content.DownloadState
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotSame
|
||||
import org.junit.Test
|
||||
|
||||
class DownloadFragmentStoreTest {
|
||||
private val downloadItem = DownloadItem("0", "title", "url", "77", "jpg", DownloadState.Status.COMPLETED)
|
||||
private val newDownloadItem = DownloadItem("1", "title", "url", "77", "jpg", DownloadState.Status.COMPLETED)
|
||||
|
||||
@Test
|
||||
fun exitEditMode() = runBlocking {
|
||||
val initialState = oneItemEditState()
|
||||
val store = DownloadFragmentStore(initialState)
|
||||
|
||||
store.dispatch(DownloadFragmentAction.ExitEditMode).join()
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(store.state.mode, DownloadFragmentState.Mode.Normal)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun itemAddedForRemoval() = runBlocking {
|
||||
val initialState = emptyDefaultState()
|
||||
val store = DownloadFragmentStore(initialState)
|
||||
|
||||
store.dispatch(DownloadFragmentAction.AddItemForRemoval(newDownloadItem)).join()
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
DownloadFragmentState.Mode.Editing(setOf(newDownloadItem))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemForRemoval() = runBlocking {
|
||||
val initialState = twoItemEditState()
|
||||
val store = DownloadFragmentStore(initialState)
|
||||
|
||||
store.dispatch(DownloadFragmentAction.RemoveItemForRemoval(newDownloadItem)).join()
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(store.state.mode, DownloadFragmentState.Mode.Editing(setOf(downloadItem)))
|
||||
}
|
||||
|
||||
private fun emptyDefaultState(): DownloadFragmentState = DownloadFragmentState(
|
||||
items = listOf(),
|
||||
mode = DownloadFragmentState.Mode.Normal,
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
|
||||
private fun oneItemEditState(): DownloadFragmentState = DownloadFragmentState(
|
||||
items = listOf(),
|
||||
mode = DownloadFragmentState.Mode.Editing(setOf(downloadItem)),
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
|
||||
private fun twoItemEditState(): DownloadFragmentState = DownloadFragmentState(
|
||||
items = listOf(),
|
||||
mode = DownloadFragmentState.Mode.Editing(setOf(downloadItem, newDownloadItem)),
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
}
|
@ -25,6 +25,24 @@ class DownloadInteractorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSelect() {
|
||||
interactor.select(downloadItem)
|
||||
|
||||
verifyAll {
|
||||
controller.handleSelect(downloadItem)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDeselect() {
|
||||
interactor.deselect(downloadItem)
|
||||
|
||||
verifyAll {
|
||||
controller.handleDeselect(downloadItem)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBackPressed() {
|
||||
every {
|
||||
@ -38,4 +56,32 @@ class DownloadInteractorTest {
|
||||
}
|
||||
assertTrue(backpressHandled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onModeSwitched() {
|
||||
interactor.onModeSwitched()
|
||||
|
||||
verifyAll {
|
||||
controller.handleModeSwitched()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDeleteAll() {
|
||||
interactor.onDeleteAll()
|
||||
|
||||
verifyAll {
|
||||
controller.handleDeleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDeleteSome() {
|
||||
val items = setOf(downloadItem)
|
||||
|
||||
interactor.onDeleteSome(items)
|
||||
verifyAll {
|
||||
controller.handleDeleteSome(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user