Merge bookmark item and folder view holders

pull/128/head^2
Tiger Oakes 4 years ago committed by ekager
parent 3059a57747
commit b927b688c9

@ -63,8 +63,6 @@ class LibrarySiteItemView @JvmOverloads constructor(
val overflowView: ImageButton get() = overflow_menu
private var iconUrl: String? = null
init {
LayoutInflater.from(context).inflate(R.layout.library_site_item, this, true)
@ -86,9 +84,6 @@ class LibrarySiteItemView @JvmOverloads constructor(
}
fun loadFavicon(url: String) {
if (iconUrl == url) return
iconUrl = url
context.components.core.icons.loadIntoView(favicon, url)
}

@ -14,10 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkFolderViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkItemViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkNodeViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkSeparatorViewHolder
@ -70,7 +67,8 @@ class BookmarkAdapter(private val emptyView: View, private val interactor: Bookm
titleChanged = oldItem.title != newItem.title,
urlChanged = oldItem.url != newItem.url,
selectedChanged = oldItem in oldMode.selectedItems != newItem in newMode.selectedItems,
modeChanged = oldMode::class != newMode::class
modeChanged = oldMode::class != newMode::class,
iconChanged = oldItem.type != newItem.type || oldItem.url != newItem.url
)
}
@ -79,24 +77,20 @@ class BookmarkAdapter(private val emptyView: View, private val interactor: Bookm
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.bookmark_list_item, parent, false) as LibrarySiteItemView
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
LibrarySiteItemView.ItemType.SITE.ordinal -> BookmarkItemViewHolder(view, interactor)
LibrarySiteItemView.ItemType.FOLDER.ordinal -> BookmarkFolderViewHolder(view, interactor)
BookmarkSeparatorViewHolder.LAYOUT_ID -> BookmarkSeparatorViewHolder(view)
BookmarkNodeViewHolder.LAYOUT_ID ->
BookmarkNodeViewHolder(view as LibrarySiteItemView, interactor)
BookmarkSeparatorViewHolder.LAYOUT_ID ->
BookmarkSeparatorViewHolder(view)
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
}
}
override fun getItemViewType(position: Int): Int {
return when (tree[position].type) {
BookmarkNodeType.ITEM -> LibrarySiteItemView.ItemType.SITE.ordinal
BookmarkNodeType.FOLDER -> LibrarySiteItemView.ItemType.FOLDER.ordinal
BookmarkNodeType.SEPARATOR -> BookmarkSeparatorViewHolder.LAYOUT_ID
else -> throw IllegalStateException("Item $tree[position] does not match to a ViewType")
}
override fun getItemViewType(position: Int) = when (tree[position].type) {
BookmarkNodeType.ITEM, BookmarkNodeType.FOLDER -> BookmarkNodeViewHolder.LAYOUT_ID
BookmarkNodeType.SEPARATOR -> BookmarkSeparatorViewHolder.LAYOUT_ID
}
override fun getItemCount(): Int = tree.size
@ -106,16 +100,18 @@ class BookmarkAdapter(private val emptyView: View, private val interactor: Bookm
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNotEmpty() && payloads[0] is BookmarkPayload) {
(holder as? BookmarkNodeViewHolder)
?.bind(tree[position], mode, payloads[0] as BookmarkPayload)
} else {
super.onBindViewHolder(holder, position, payloads)
(holder as? BookmarkNodeViewHolder)?.apply {
val diffPayload = if (payloads.isNotEmpty() && payloads[0] is BookmarkPayload) {
payloads[0] as BookmarkPayload
} else {
BookmarkPayload()
}
bind(tree[position], mode, diffPayload)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as? BookmarkNodeViewHolder)?.bind(tree[position], mode)
(holder as? BookmarkNodeViewHolder)?.bind(tree[position], mode, BookmarkPayload())
}
}
@ -126,12 +122,22 @@ class BookmarkAdapter(private val emptyView: View, private val interactor: Bookm
* @property urlChanged true if there has been a change to [BookmarkNode.url].
* @property selectedChanged true if there has been a change in the BookmarkNode's selected state.
* @property modeChanged true if there has been a change in the state's mode type.
* @property iconChanged true if the icon displayed for the node should be changed.
*/
data class BookmarkPayload(
val titleChanged: Boolean,
val urlChanged: Boolean,
val selectedChanged: Boolean,
val modeChanged: Boolean
)
val modeChanged: Boolean,
val iconChanged: Boolean
) {
constructor() : this(
titleChanged = true,
urlChanged = true,
selectedChanged = true,
modeChanged = true,
iconChanged = true
)
}
fun BookmarkNode.inRoots() = enumValues<BookmarkRoot>().any { it.id == guid }

@ -1,81 +0,0 @@
/* 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.bookmarks.viewholders
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
import org.mozilla.fenix.library.bookmarks.inRoots
/**
* Represents a folder with other bookmarks inside.
*/
class BookmarkFolderViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) : BookmarkNodeViewHolder(view, interactor) {
override var item: BookmarkNode? = null
init {
containerView.displayAs(LibrarySiteItemView.ItemType.FOLDER)
}
override fun bind(
item: BookmarkNode,
mode: BookmarkFragmentState.Mode
) {
bind(item, mode, BookmarkPayload(true, true, true, true))
}
override fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode, payload: BookmarkPayload) {
this.item = item
setSelectionListeners(item, mode)
if (!item.inRoots()) {
updateMenu(item.type)
if (payload.modeChanged) {
if (mode is BookmarkFragmentState.Mode.Selecting) {
containerView.overflowView.hideAndDisable()
} else {
containerView.overflowView.showAndEnable()
}
}
} else {
containerView.overflowView.visibility = View.GONE
}
if (payload.selectedChanged) {
containerView.changeSelected(item in mode.selectedItems)
}
containerView.iconView.setImageDrawable(
AppCompatResources.getDrawable(
containerView.context,
R.drawable.ic_folder_icon
)?.apply {
setTint(
ContextCompat.getColor(
containerView.context,
R.color.primary_text_light_theme
)
)
}
)
if (payload.titleChanged) {
containerView.titleView.text = item.title
}
}
}

@ -1,76 +0,0 @@
/* 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.bookmarks.viewholders
import androidx.annotation.VisibleForTesting
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
/**
* Represents a bookmarked website in the bookmarks page.
*/
class BookmarkItemViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) : BookmarkNodeViewHolder(view, interactor) {
override var item: BookmarkNode? = null
init {
containerView.displayAs(LibrarySiteItemView.ItemType.SITE)
}
override fun bind(
item: BookmarkNode,
mode: BookmarkFragmentState.Mode
) {
bind(item, mode, BookmarkPayload(true, true, true, true))
}
override fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode, payload: BookmarkPayload) {
this.item = item
updateMenu(item.type)
if (payload.modeChanged) {
if (mode is BookmarkFragmentState.Mode.Selecting) {
containerView.overflowView.hideAndDisable()
} else {
containerView.overflowView.showAndEnable()
}
}
if (payload.selectedChanged) {
containerView.changeSelected(item in mode.selectedItems)
}
if (payload.titleChanged) {
containerView.titleView.text = if (item.title.isNullOrBlank()) item.url else item.title
} else if (payload.urlChanged && item.title.isNullOrBlank()) {
containerView.titleView.text = item.url
}
if (payload.urlChanged) {
containerView.urlView.text = item.url
setColorsAndIcons(item.url)
}
setSelectionListeners(item, mode)
}
@VisibleForTesting
internal fun setColorsAndIcons(url: String?) {
if (url != null && url.startsWith("http")) {
containerView.loadFavicon(url)
} else {
containerView.iconView.setImageDrawable(null)
}
}
}

@ -4,45 +4,38 @@
package org.mozilla.fenix.library.bookmarks.viewholders
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getDrawableWithTint
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
import org.mozilla.fenix.library.bookmarks.inRoots
import org.mozilla.fenix.utils.Do
/**
* Base class for bookmark node view holders.
*/
abstract class BookmarkNodeViewHolder(
protected val containerView: LibrarySiteItemView,
class BookmarkNodeViewHolder(
private val containerView: LibrarySiteItemView,
private val interactor: BookmarkViewInteractor
) : RecyclerView.ViewHolder(containerView) {
abstract var item: BookmarkNode?
private lateinit var menu: BookmarkItemMenu
var item: BookmarkNode? = null
private val menu: BookmarkItemMenu
init {
setupMenu()
}
abstract fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode)
abstract fun bind(
item: BookmarkNode,
mode: BookmarkFragmentState.Mode,
payload: BookmarkPayload
)
protected fun setSelectionListeners(item: BookmarkNode, selectionHolder: SelectionHolder<BookmarkNode>) {
containerView.setSelectionInteractor(item, selectionHolder, interactor)
}
private fun setupMenu() {
menu = BookmarkItemMenu(containerView.context) { menuItem ->
val item = this.item ?: return@BookmarkItemMenu
Do exhaustive when (menuItem) {
@ -58,5 +51,71 @@ abstract class BookmarkNodeViewHolder(
containerView.attachMenu(menu.menuController)
}
protected fun updateMenu(itemType: BookmarkNodeType) = menu.updateMenu(itemType)
fun bind(
item: BookmarkNode,
mode: BookmarkFragmentState.Mode,
payload: BookmarkPayload
) {
this.item = item
containerView.urlView.isVisible = item.type == BookmarkNodeType.ITEM
containerView.setSelectionInteractor(item, mode, interactor)
menu.updateMenu(item.type)
// Hide menu button if this item is a root folder or is selected
if (item.type == BookmarkNodeType.FOLDER && item.inRoots()) {
containerView.overflowView.visibility = View.GONE
} else if (payload.modeChanged) {
if (mode is BookmarkFragmentState.Mode.Selecting) {
containerView.overflowView.hideAndDisable()
} else {
containerView.overflowView.showAndEnable()
}
}
if (payload.selectedChanged) {
containerView.changeSelected(item in mode.selectedItems)
}
val useTitleFallback = item.type == BookmarkNodeType.ITEM && item.title.isNullOrBlank()
if (payload.titleChanged) {
containerView.titleView.text = if (useTitleFallback) item.url else item.title
} else if (payload.urlChanged && useTitleFallback) {
containerView.titleView.text = item.url
}
if (payload.urlChanged) {
containerView.urlView.text = item.url
}
if (payload.iconChanged) {
updateIcon(item)
}
}
private fun updateIcon(item: BookmarkNode) {
val context = containerView.context
val iconView = containerView.iconView
val url = item.url
when {
// Item is a folder
item.type == BookmarkNodeType.FOLDER ->
iconView.setImageDrawable(
context.getDrawableWithTint(
R.drawable.ic_folder_icon,
ContextCompat.getColor(context, R.color.primary_text_light_theme)
)
)
// Item has a http/https URL
url != null && url.startsWith("http") ->
context.components.core.icons.loadIntoView(iconView, url)
else ->
iconView.setImageDrawable(null)
}
}
companion object {
const val LAYOUT_ID = R.layout.bookmark_list_item
}
}

@ -1,87 +0,0 @@
/* 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.bookmarks.viewholders
import androidx.appcompat.content.res.AppCompatResources
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentInteractor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
class BookmarkFolderViewHolderTest {
@MockK
private lateinit var interactor: BookmarkFragmentInteractor
@MockK(relaxed = true)
private lateinit var siteItemView: LibrarySiteItemView
private lateinit var holder: BookmarkFolderViewHolder
private val folder = BookmarkNode(
type = BookmarkNodeType.FOLDER,
guid = "456",
parentGuid = "123",
position = 0,
title = "Folder",
url = null,
children = listOf()
)
@Before
fun setup() {
MockKAnnotations.init(this)
mockkStatic(AppCompatResources::class)
every { AppCompatResources.getDrawable(any(), any()) } returns mockk(relaxed = true)
holder = BookmarkFolderViewHolder(siteItemView, interactor)
}
@Test
fun `binds title and selected state`() {
holder.bind(folder, BookmarkFragmentState.Mode.Normal())
verify {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.showAndEnable()
siteItemView.changeSelected(false)
}
holder.bind(folder, BookmarkFragmentState.Mode.Selecting(setOf(folder)))
verify {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(true)
}
}
@Test
fun `bind with payload of no changes does not rebind views`() {
holder.bind(
folder,
BookmarkFragmentState.Mode.Normal(),
BookmarkPayload(false, false, false, false)
)
verify(inverse = true) {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.showAndEnable()
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(any())
}
}
}

@ -4,13 +4,23 @@
package org.mozilla.fenix.library.bookmarks.viewholders
import androidx.appcompat.content.res.AppCompatResources
import io.mockk.Called
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.IconRequest
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
@ -18,15 +28,12 @@ import org.mozilla.fenix.library.bookmarks.BookmarkFragmentInteractor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
class BookmarkItemViewHolderTest {
class BookmarkNodeViewHolderTest {
@MockK
private lateinit var interactor: BookmarkFragmentInteractor
@MockK(relaxed = true)
private lateinit var siteItemView: LibrarySiteItemView
private lateinit var holder: BookmarkItemViewHolder
@MockK private lateinit var interactor: BookmarkFragmentInteractor
@MockK(relaxed = true) private lateinit var siteItemView: LibrarySiteItemView
@MockK private lateinit var icons: BrowserIcons
private lateinit var holder: BookmarkNodeViewHolder
private val item = BookmarkNode(
type = BookmarkNodeType.ITEM,
@ -37,17 +44,45 @@ class BookmarkItemViewHolderTest {
url = "https://www.mozilla.org",
children = listOf()
)
private val folder = BookmarkNode(
type = BookmarkNodeType.FOLDER,
guid = "456",
parentGuid = "123",
position = 0,
title = "Folder",
url = null,
children = listOf()
)
private val falsePayload = BookmarkPayload(
titleChanged = false,
urlChanged = false,
selectedChanged = false,
modeChanged = false,
iconChanged = false
)
@Before
fun setup() {
MockKAnnotations.init(this)
holder = BookmarkItemViewHolder(siteItemView, interactor)
mockkStatic(AppCompatResources::class)
every { AppCompatResources.getDrawable(any(), any()) } returns mockk(relaxed = true)
every { siteItemView.context.components.core.icons } returns icons
every { icons.loadIntoView(siteItemView.iconView, any()) } returns mockk()
holder = BookmarkNodeViewHolder(siteItemView, interactor)
}
@After
fun teardown() {
unmockkStatic(AppCompatResources::class)
}
@Test
fun `binds views for unselected item`() {
val mode = BookmarkFragmentState.Mode.Normal()
holder.bind(item, mode)
holder.bind(item, mode, BookmarkPayload())
verify {
siteItemView.setSelectionInteractor(item, mode, interactor)
@ -55,14 +90,14 @@ class BookmarkItemViewHolderTest {
siteItemView.urlView.text = item.url
siteItemView.overflowView.showAndEnable()
siteItemView.changeSelected(false)
holder.setColorsAndIcons(item.url)
icons.loadIntoView(siteItemView.iconView, IconRequest(item.url!!))
}
}
@Test
fun `binds views for selected item`() {
fun `binds views for selected item for item`() {
val mode = BookmarkFragmentState.Mode.Selecting(setOf(item))
holder.bind(item, mode)
holder.bind(item, mode, BookmarkPayload())
verify {
siteItemView.setSelectionInteractor(item, mode, interactor)
@ -70,16 +105,15 @@ class BookmarkItemViewHolderTest {
siteItemView.urlView.text = item.url
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(true)
holder.setColorsAndIcons(item.url)
}
}
@Test
fun `bind with payload of no changes does not rebind views`() {
fun `bind with payload of no changes does not rebind views for item`() {
holder.bind(
item,
BookmarkFragmentState.Mode.Normal(),
BookmarkPayload(false, false, false, false)
falsePayload
)
verify(inverse = true) {
@ -88,28 +122,28 @@ class BookmarkItemViewHolderTest {
siteItemView.overflowView.showAndEnable()
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(any())
holder.setColorsAndIcons(item.url)
}
verify { siteItemView.iconView wasNot Called }
}
@Test
fun `binding an item with a null title uses the url as the title`() {
fun `binding an item with a null title uses the url as the title for item`() {
val item = item.copy(title = null)
holder.bind(item, BookmarkFragmentState.Mode.Normal())
holder.bind(item, BookmarkFragmentState.Mode.Normal(), BookmarkPayload())
verify { siteItemView.titleView.text = item.url }
}
@Test
fun `binding an item with a blank title uses the url as the title`() {
fun `binding an item with a blank title uses the url as the title for item`() {
val item = item.copy(title = " ")
holder.bind(item, BookmarkFragmentState.Mode.Normal())
holder.bind(item, BookmarkFragmentState.Mode.Normal(), BookmarkPayload())
verify { siteItemView.titleView.text = item.url }
}
@Test
fun `rebinds title if item title is null and the item url has changed`() {
fun `rebinds title if item title is null and the item url has changed for item`() {
val item = item.copy(title = null)
holder.bind(
item,
@ -118,7 +152,8 @@ class BookmarkItemViewHolderTest {
titleChanged = false,
urlChanged = true,
selectedChanged = false,
modeChanged = false
modeChanged = false,
iconChanged = false
)
)
@ -126,7 +161,7 @@ class BookmarkItemViewHolderTest {
}
@Test
fun `rebinds title if item title is blank and the item url has changed`() {
fun `rebinds title if item title is blank and the item url has changed for item`() {
val item = item.copy(title = " ")
holder.bind(
item,
@ -135,10 +170,46 @@ class BookmarkItemViewHolderTest {
titleChanged = false,
urlChanged = true,
selectedChanged = false,
modeChanged = false
modeChanged = false,
iconChanged = false
)
)
verify { siteItemView.titleView.text = item.url }
}
@Test
fun `binds title and selected state for folder`() {
holder.bind(folder, BookmarkFragmentState.Mode.Normal(), BookmarkPayload())
verify {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.showAndEnable()
siteItemView.changeSelected(false)
}
holder.bind(folder, BookmarkFragmentState.Mode.Selecting(setOf(folder)), BookmarkPayload())
verify {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(true)
}
}
@Test
fun `bind with payload of no changes does not rebind views for folder`() {
holder.bind(
folder,
BookmarkFragmentState.Mode.Normal(),
falsePayload
)
verify(inverse = true) {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.showAndEnable()
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(any())
}
}
}
Loading…
Cancel
Save