For #349: View Downloads
parent
929ec541b0
commit
f83372b67c
|
@ -48,4 +48,9 @@ object FeatureFlags {
|
||||||
* Enables downloads with external download managers.
|
* Enables downloads with external download managers.
|
||||||
*/
|
*/
|
||||||
val externalDownloadManager = Config.channel.isNightlyOrDebug
|
val externalDownloadManager = Config.channel.isNightlyOrDebug
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables viewing downloads in browser.
|
||||||
|
*/
|
||||||
|
val viewDownloads = Config.channel.isNightlyOrDebug
|
||||||
}
|
}
|
||||||
|
|
|
@ -488,7 +488,7 @@ sealed class Event {
|
||||||
NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX,
|
NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX,
|
||||||
SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON,
|
SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON,
|
||||||
READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER,
|
READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER,
|
||||||
BOOKMARKS, HISTORY, SYNC_TABS
|
BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS
|
||||||
}
|
}
|
||||||
|
|
||||||
override val extras: Map<Events.browserMenuActionKeys, String>?
|
override val extras: Map<Events.browserMenuActionKeys, String>?
|
||||||
|
|
|
@ -380,6 +380,13 @@ class DefaultBrowserToolbarController(
|
||||||
BrowserFragmentDirections.actionGlobalHistoryFragment()
|
BrowserFragmentDirections.actionGlobalHistoryFragment()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically {
|
||||||
|
navController.nav(
|
||||||
|
R.id.browserFragment,
|
||||||
|
BrowserFragmentDirections.actionGlobalDownloadsFragment()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,6 +421,7 @@ class DefaultBrowserToolbarController(
|
||||||
ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
|
ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
|
||||||
ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
|
ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
|
||||||
ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
|
ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
|
||||||
|
ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))
|
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))
|
||||||
|
|
|
@ -179,6 +179,7 @@ class DefaultToolbarMenu(
|
||||||
.shouldDeleteBrowsingDataOnQuit
|
.shouldDeleteBrowsingDataOnQuit
|
||||||
|
|
||||||
val menuItems = listOfNotNull(
|
val menuItems = listOfNotNull(
|
||||||
|
if (FeatureFlags.viewDownloads) downloadsItem else null,
|
||||||
historyItem,
|
historyItem,
|
||||||
bookmarksItem,
|
bookmarksItem,
|
||||||
if (FeatureFlags.syncedTabs) syncedTabs else null,
|
if (FeatureFlags.syncedTabs) syncedTabs else null,
|
||||||
|
@ -333,6 +334,14 @@ class DefaultToolbarMenu(
|
||||||
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
|
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val downloadsItem = BrowserMenuImageText(
|
||||||
|
"Downloads",
|
||||||
|
R.drawable.ic_download,
|
||||||
|
primaryTextColor()
|
||||||
|
) {
|
||||||
|
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
|
||||||
|
}
|
||||||
|
|
||||||
@ColorRes
|
@ColorRes
|
||||||
private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)
|
private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ interface ToolbarMenu {
|
||||||
object ReaderModeAppearance : Item()
|
object ReaderModeAppearance : Item()
|
||||||
object Bookmarks : Item()
|
object Bookmarks : Item()
|
||||||
object History : Item()
|
object History : Item()
|
||||||
|
object Downloads : Item()
|
||||||
}
|
}
|
||||||
|
|
||||||
val menuBuilder: BrowserMenuBuilder
|
val menuBuilder: BrowserMenuBuilder
|
||||||
|
|
|
@ -771,6 +771,15 @@ class HomeFragment : Fragment() {
|
||||||
HomeFragmentDirections.actionGlobalHistoryFragment()
|
HomeFragmentDirections.actionGlobalHistoryFragment()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HomeMenu.Item.Downloads -> {
|
||||||
|
hideOnboardingIfNeeded()
|
||||||
|
nav(
|
||||||
|
R.id.homeFragment,
|
||||||
|
HomeFragmentDirections.actionGlobalDownloadsFragment()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
HomeMenu.Item.Help -> {
|
HomeMenu.Item.Help -> {
|
||||||
hideOnboardingIfNeeded()
|
hideOnboardingIfNeeded()
|
||||||
(activity as HomeActivity).openToBrowserAndLoad(
|
(activity as HomeActivity).openToBrowserAndLoad(
|
||||||
|
|
|
@ -43,6 +43,7 @@ class HomeMenu(
|
||||||
object SyncedTabs : Item()
|
object SyncedTabs : Item()
|
||||||
object History : Item()
|
object History : Item()
|
||||||
object Bookmarks : Item()
|
object Bookmarks : Item()
|
||||||
|
object Downloads : Item()
|
||||||
object Quit : Item()
|
object Quit : Item()
|
||||||
object Sync : Item()
|
object Sync : Item()
|
||||||
}
|
}
|
||||||
|
@ -144,6 +145,14 @@ class HomeMenu(
|
||||||
onItemTapped.invoke(Item.Help)
|
onItemTapped.invoke(Item.Help)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val downloadsItem = BrowserMenuImageText(
|
||||||
|
"Downloads",
|
||||||
|
R.drawable.ic_download,
|
||||||
|
primaryTextColor
|
||||||
|
) {
|
||||||
|
onItemTapped.invoke(Item.Downloads)
|
||||||
|
}
|
||||||
|
|
||||||
// Only query account manager if it has been initialized.
|
// Only query account manager if it has been initialized.
|
||||||
// We don't want to cause its initialization just for this check.
|
// We don't want to cause its initialization just for this check.
|
||||||
val accountAuthItem = if (context.components.backgroundServices.accountManagerAvailableQueue.isReady()) {
|
val accountAuthItem = if (context.components.backgroundServices.accountManagerAvailableQueue.isReady()) {
|
||||||
|
@ -161,6 +170,7 @@ class HomeMenu(
|
||||||
if (FeatureFlags.syncedTabs) syncedTabsItem else null,
|
if (FeatureFlags.syncedTabs) syncedTabsItem else null,
|
||||||
bookmarksItem,
|
bookmarksItem,
|
||||||
historyItem,
|
historyItem,
|
||||||
|
if (FeatureFlags.viewDownloads) downloadsItem else null,
|
||||||
BrowserMenuDivider(),
|
BrowserMenuDivider(),
|
||||||
addons,
|
addons,
|
||||||
BrowserMenuDivider(),
|
BrowserMenuDivider(),
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
/* 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.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.mozilla.fenix.library.SelectionHolder
|
||||||
|
import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder
|
||||||
|
|
||||||
|
class DownloadAdapter(
|
||||||
|
private val downloadInteractor: DownloadInteractor
|
||||||
|
) : RecyclerView.Adapter<DownloadsListItemViewHolder>(), SelectionHolder<DownloadItem> {
|
||||||
|
private var downloads: List<DownloadItem> = listOf()
|
||||||
|
private var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
|
||||||
|
override val selectedItems get() = mode.selectedItems
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = downloads.size
|
||||||
|
override fun getItemViewType(position: Int): Int = DownloadsListItemViewHolder.LAYOUT_ID
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsListItemViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||||
|
return DownloadsListItemViewHolder(view, downloadInteractor, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMode(mode: DownloadFragmentState.Mode) {
|
||||||
|
this.mode = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) {
|
||||||
|
holder.bind(downloads[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloads(downloads: List<DownloadItem>) {
|
||||||
|
this.downloads = downloads
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
/* 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 org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
|
||||||
|
interface DownloadController {
|
||||||
|
fun handleOpen(item: DownloadItem, mode: BrowsingMode? = null)
|
||||||
|
fun handleBackPressed(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultDownloadController(
|
||||||
|
private val store: DownloadFragmentStore,
|
||||||
|
private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit
|
||||||
|
) : DownloadController {
|
||||||
|
override fun handleOpen(item: DownloadItem, mode: BrowsingMode?) {
|
||||||
|
openToFileManager(item, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleBackPressed(): Boolean {
|
||||||
|
return if (store.state.mode is DownloadFragmentState.Mode.Editing) {
|
||||||
|
store.dispatch(DownloadFragmentAction.ExitEditMode)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
/* 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.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import kotlinx.android.synthetic.main.fragment_downloads.view.*
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
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.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
|
import org.mozilla.fenix.library.LibraryPageFragment
|
||||||
|
|
||||||
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
|
class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHandler {
|
||||||
|
private lateinit var downloadStore: DownloadFragmentStore
|
||||||
|
private lateinit var downloadView: DownloadView
|
||||||
|
private lateinit var downloadInteractor: DownloadInteractor
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_downloads, container, false)
|
||||||
|
|
||||||
|
val items = requireComponents.core.store.state.downloads.map {
|
||||||
|
DownloadItem(
|
||||||
|
it.value.id,
|
||||||
|
it.value.fileName,
|
||||||
|
it.value.filePath,
|
||||||
|
it.value.contentLength.toString(),
|
||||||
|
it.value.contentType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadStore = StoreProvider.get(this) {
|
||||||
|
DownloadFragmentStore(
|
||||||
|
DownloadFragmentState(
|
||||||
|
items = items,
|
||||||
|
mode = DownloadFragmentState.Mode.Normal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadController: DownloadController = DefaultDownloadController(
|
||||||
|
downloadStore,
|
||||||
|
::openItem
|
||||||
|
)
|
||||||
|
downloadInteractor = DownloadInteractor(
|
||||||
|
downloadController
|
||||||
|
)
|
||||||
|
downloadView = DownloadView(view.downloadsLayout, downloadInteractor)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override val selectedItems get() = downloadStore.state.mode.selectedItems
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
requireComponents.analytics.metrics.track(Event.HistoryOpened)
|
||||||
|
|
||||||
|
setHasOptionsMenu(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
consumeFrom(downloadStore) {
|
||||||
|
downloadView.update(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
showToolbar(getString(R.string.library_downloads))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return downloadView.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openItem(item: DownloadItem, mode: BrowsingMode? = null) {
|
||||||
|
|
||||||
|
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }
|
||||||
|
context?.let {
|
||||||
|
AbstractFetchDownloadService.openFile(
|
||||||
|
context = it,
|
||||||
|
contentType = item.contentType,
|
||||||
|
filePath = item.filePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/* 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 mozilla.components.lib.state.Action
|
||||||
|
import mozilla.components.lib.state.State
|
||||||
|
import mozilla.components.lib.state.Store
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a history 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
|
||||||
|
* @property size The size in bytes of the download item
|
||||||
|
* @property contentType The type of file the download is
|
||||||
|
*/
|
||||||
|
data class DownloadItem(val id: Long, val fileName: String?, val filePath: String, val size: String, val contentType: String?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Store] for holding the [DownloadFragmentState] and applying [DownloadFragmentAction]s.
|
||||||
|
*/
|
||||||
|
class DownloadFragmentStore(initialState: DownloadFragmentState) :
|
||||||
|
Store<DownloadFragmentState, DownloadFragmentAction>(initialState, ::downloadStateReducer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to dispatch through the `DownloadStore` to modify `DownloadState` through the reducer.
|
||||||
|
*/
|
||||||
|
sealed class DownloadFragmentAction : Action {
|
||||||
|
object ExitEditMode : DownloadFragmentAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state for the Download Screen
|
||||||
|
* @property items List of DownloadItem to display
|
||||||
|
* @property mode Current Mode of Download
|
||||||
|
*/
|
||||||
|
data class DownloadFragmentState(
|
||||||
|
val items: List<DownloadItem>,
|
||||||
|
val mode: Mode
|
||||||
|
) : State {
|
||||||
|
sealed class Mode {
|
||||||
|
open val selectedItems = emptySet<DownloadItem>()
|
||||||
|
|
||||||
|
object Normal : Mode()
|
||||||
|
data class Editing(override val selectedItems: Set<DownloadItem>) : DownloadFragmentState.Mode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DownloadState Reducer.
|
||||||
|
*/
|
||||||
|
private fun downloadStateReducer(
|
||||||
|
state: DownloadFragmentState,
|
||||||
|
action: DownloadFragmentAction
|
||||||
|
): DownloadFragmentState {
|
||||||
|
return when (action) {
|
||||||
|
is DownloadFragmentAction.ExitEditMode -> state.copy(mode = DownloadFragmentState.Mode.Normal)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/* 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
|
||||||
|
/**
|
||||||
|
* Interactor for the download screen
|
||||||
|
* Provides implementations for the DownloadViewInteractor
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
|
class DownloadInteractor(
|
||||||
|
private val downloadController: DownloadController
|
||||||
|
) : DownloadViewInteractor {
|
||||||
|
override fun open(item: DownloadItem) {
|
||||||
|
downloadController.handleOpen(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun select(item: DownloadItem) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deselect(item: DownloadItem) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return downloadController.handleBackPressed()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
|
||||||
|
|
||||||
|
/* 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.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import kotlinx.android.synthetic.main.component_downloads.view.*
|
||||||
|
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.library.LibraryPageView
|
||||||
|
import org.mozilla.fenix.library.SelectionInteractor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the DownloadViewInteractor. This interface is implemented by objects that want
|
||||||
|
* to respond to user interaction on the DownloadView
|
||||||
|
*/
|
||||||
|
interface DownloadViewInteractor : SelectionInteractor<DownloadItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on backpressed to exit edit mode
|
||||||
|
*/
|
||||||
|
fun onBackPressed(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View that contains and configures the Downloads List
|
||||||
|
*/
|
||||||
|
class DownloadView(
|
||||||
|
container: ViewGroup,
|
||||||
|
val interactor: DownloadInteractor
|
||||||
|
) : LibraryPageView(container), UserInteractionHandler {
|
||||||
|
|
||||||
|
val view: View = LayoutInflater.from(container.context)
|
||||||
|
.inflate(R.layout.component_downloads, container, true)
|
||||||
|
|
||||||
|
var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
|
||||||
|
private set
|
||||||
|
|
||||||
|
val downloadAdapter = DownloadAdapter(interactor)
|
||||||
|
private val layoutManager = LinearLayoutManager(container.context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.download_list.apply {
|
||||||
|
layoutManager = this@DownloadView.layoutManager
|
||||||
|
adapter = downloadAdapter
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(state: DownloadFragmentState) {
|
||||||
|
|
||||||
|
view.swipe_refresh.isEnabled =
|
||||||
|
state.mode === DownloadFragmentState.Mode.Normal
|
||||||
|
mode = state.mode
|
||||||
|
|
||||||
|
downloadAdapter.updateMode(state.mode)
|
||||||
|
downloadAdapter.updateDownloads(state.items)
|
||||||
|
|
||||||
|
setUiForNormalMode(
|
||||||
|
context.getString(R.string.library_downloads)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return interactor.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/* 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.viewholders
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.download_list_item.view.*
|
||||||
|
import kotlinx.android.synthetic.main.library_site_item.view.*
|
||||||
|
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 mozilla.components.feature.downloads.toMegabyteString
|
||||||
|
|
||||||
|
class DownloadsListItemViewHolder(
|
||||||
|
view: View,
|
||||||
|
private val downloadInteractor: DownloadInteractor,
|
||||||
|
private val selectionHolder: SelectionHolder<DownloadItem>
|
||||||
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
private var item: DownloadItem? = null
|
||||||
|
|
||||||
|
fun bind(
|
||||||
|
item: DownloadItem
|
||||||
|
) {
|
||||||
|
itemView.download_layout.visibility = View.VISIBLE
|
||||||
|
itemView.download_layout.titleView.text = item.fileName
|
||||||
|
itemView.download_layout.urlView.text = item.size.toLong().toMegabyteString()
|
||||||
|
|
||||||
|
itemView.download_layout.setSelectionInteractor(item, selectionHolder, downloadInteractor)
|
||||||
|
itemView.download_layout.changeSelected(item in selectionHolder.selectedItems)
|
||||||
|
|
||||||
|
itemView.overflow_menu.hideAndDisable()
|
||||||
|
itemView.favicon.setImageResource(R.drawable.ic_download_default)
|
||||||
|
itemView.favicon.isClickable = false
|
||||||
|
|
||||||
|
this.item = item
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.download_list_item
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/download_wrapper"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="8dp"
|
||||||
|
android:translationY="-3dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_empty_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/download_empty_message"
|
||||||
|
android:textColor="?secondaryText"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipe_refresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" >
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/download_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
tools:listitem="@layout/download_list_item"/>
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<org.mozilla.fenix.library.LibrarySiteItemView
|
||||||
|
android:id="@+id/download_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="@dimen/library_item_height" />
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?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/. -->
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/downloadsLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="org.mozilla.fenix.library.downloads.DownloadFragment" />
|
|
@ -70,6 +70,9 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_historyFragment"
|
android:id="@+id/action_global_historyFragment"
|
||||||
app:destination="@id/historyFragment" />
|
app:destination="@id/historyFragment" />
|
||||||
|
|
||||||
|
<action android:id="@+id/action_global_downloadsFragment"
|
||||||
|
app:destination="@id/downloadsFragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_accountProblemFragment"
|
android:id="@+id/action_global_accountProblemFragment"
|
||||||
app:destination="@id/accountProblemFragment" />
|
app:destination="@id/accountProblemFragment" />
|
||||||
|
@ -239,6 +242,12 @@
|
||||||
android:label="@string/library_history"
|
android:label="@string/library_history"
|
||||||
tools:layout="@layout/fragment_history" />
|
tools:layout="@layout/fragment_history" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/downloadsFragment"
|
||||||
|
android:name="org.mozilla.fenix.library.downloads.DownloadFragment"
|
||||||
|
android:label="Downloads"
|
||||||
|
tools:layout="@layout/fragment_downloads" />
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/bookmarkFragment"
|
android:id="@+id/bookmarkFragment"
|
||||||
android:name="org.mozilla.fenix.library.bookmarks.BookmarkFragment"
|
android:name="org.mozilla.fenix.library.bookmarks.BookmarkFragment"
|
||||||
|
|
|
@ -563,6 +563,13 @@
|
||||||
<!-- Text shown when no history exists -->
|
<!-- Text shown when no history exists -->
|
||||||
<string name="history_empty_message">No history here</string>
|
<string name="history_empty_message">No history here</string>
|
||||||
|
|
||||||
|
<!-- Downloads -->
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
<!-- Crashes -->
|
<!-- Crashes -->
|
||||||
<!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) -->
|
<!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) -->
|
||||||
<string name="tab_crash_title_2">Sorry. %1$s can’t load that page.</string>
|
<string name="tab_crash_title_2">Sorry. %1$s can’t load that page.</string>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* 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 androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class DownloadAdapterTest {
|
||||||
|
|
||||||
|
private lateinit var interactor: DownloadInteractor
|
||||||
|
private lateinit var adapter: DownloadAdapter
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
interactor = mockk()
|
||||||
|
adapter = DownloadAdapter(interactor)
|
||||||
|
|
||||||
|
every { interactor.select(any()) } just Runs
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getItemCount should return the number of tab collections`() {
|
||||||
|
val download = mockk<DownloadItem>()
|
||||||
|
|
||||||
|
assertEquals(0, adapter.itemCount)
|
||||||
|
|
||||||
|
adapter.updateDownloads(
|
||||||
|
downloads = listOf(download)
|
||||||
|
)
|
||||||
|
assertEquals(1, adapter.itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updateData inserts item`() {
|
||||||
|
val download = mockk<DownloadItem> {
|
||||||
|
}
|
||||||
|
val observer = mockk<RecyclerView.AdapterDataObserver>(relaxed = true)
|
||||||
|
adapter.registerAdapterDataObserver(observer)
|
||||||
|
adapter.updateDownloads(
|
||||||
|
downloads = listOf(download)
|
||||||
|
)
|
||||||
|
verify { observer.onChanged() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/* 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 io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestCoroutineScope
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class DownloadControllerTest {
|
||||||
|
private val downloadItem = DownloadItem(0, "title", "url", "77", "jpg")
|
||||||
|
private val scope: CoroutineScope = TestCoroutineScope()
|
||||||
|
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 invalidateOptionsMenu: () -> Unit = mockk(relaxed = true)
|
||||||
|
private val controller = DefaultDownloadController(
|
||||||
|
store,
|
||||||
|
openToFileManager
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
every { store.state } returns state
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onPressDownloadItemInNormalMode() {
|
||||||
|
controller.handleOpen(downloadItem)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
openToFileManager(downloadItem, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onOpenItemInNormalMode() {
|
||||||
|
controller.handleOpen(downloadItem, BrowsingMode.Normal)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
openToFileManager(downloadItem, BrowsingMode.Normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onBackPressedInNormalMode() {
|
||||||
|
every { state.mode } returns DownloadFragmentState.Mode.Normal
|
||||||
|
|
||||||
|
assertFalse(controller.handleBackPressed())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/* 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 io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verifyAll
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DownloadInteractorTest {
|
||||||
|
private val downloadItem = DownloadItem(0, "title", "url", "5.6 mb", "png")
|
||||||
|
val controller: DownloadController = mockk(relaxed = true)
|
||||||
|
val interactor = DownloadInteractor(controller)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onOpen() {
|
||||||
|
interactor.open(downloadItem)
|
||||||
|
|
||||||
|
verifyAll {
|
||||||
|
controller.handleOpen(downloadItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onBackPressed() {
|
||||||
|
every {
|
||||||
|
controller.handleBackPressed()
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
val backpressHandled = interactor.onBackPressed()
|
||||||
|
|
||||||
|
verifyAll {
|
||||||
|
controller.handleBackPressed()
|
||||||
|
}
|
||||||
|
assertTrue(backpressHandled)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue