Fixes #1397 - Adds the delete button back to the history recyclerview

nightly-build-test
Jeff Boek 5 years ago
parent 4245f71d93
commit 4a32ef8ed8

@ -1,82 +0,0 @@
package org.mozilla.fenix.components
/* 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/. */
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import java.lang.IllegalStateException
@SuppressWarnings("TooManyFunctions")
abstract class SectionedAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
sealed class SectionType {
data class Header(val index: Int) : SectionType()
data class Row(val section: Int, val row: Int) : SectionType()
val viewType: Int
get() = when (this) {
is Header -> HeaderViewType
is Row -> RowViewType
}
companion object {
const val HeaderViewType = 0
const val RowViewType = 1
}
}
abstract fun numberOfSections(): Int
abstract fun numberOfRowsInSection(section: Int): Int
abstract fun onCreateHeaderViewHolder(parent: ViewGroup): RecyclerView.ViewHolder
abstract fun onBindHeaderViewHolder(holder: RecyclerView.ViewHolder, header: SectionType.Header)
abstract fun onCreateItemViewHolder(parent: ViewGroup): RecyclerView.ViewHolder
abstract fun onBindItemViewHolder(holder: RecyclerView.ViewHolder, row: SectionType.Row)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
SectionType.HeaderViewType -> onCreateHeaderViewHolder(parent)
SectionType.RowViewType -> onCreateItemViewHolder(parent)
else -> throw IllegalStateException("ViewType: $viewType is invalid ")
}
}
override fun getItemViewType(position: Int): Int {
return sectionTypeForPosition(position).viewType
}
final override fun getItemCount(): Int {
var count = 0
for (i in 0 until numberOfSections()) {
count += numberOfRowsInSection(i) + 1
}
return count
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val sectionType = sectionTypeForPosition(position)
when (sectionType) {
is SectionType.Header -> onBindHeaderViewHolder(holder, sectionType)
is SectionType.Row -> onBindItemViewHolder(holder, sectionType)
}
}
private fun sectionTypeForPosition(position: Int): SectionType {
var currentPosition = 0
for (sectionIndex in 0 until numberOfSections()) {
if (position == currentPosition) { return SectionType.Header(sectionIndex) }
currentPosition += 1
for (rowIndex in 0 until numberOfRowsInSection(sectionIndex)) {
if (currentPosition == position) { return SectionType.Row(sectionIndex, rowIndex) }
currentPosition += 1
}
}
throw IllegalStateException("Position $position is out of bounds!")
}
}

@ -7,42 +7,36 @@ package org.mozilla.fenix.library.history
import android.content.Context import android.content.Context
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer import io.reactivex.Observer
import org.mozilla.fenix.R import org.mozilla.fenix.R
import kotlinx.android.synthetic.main.history_header.view.* import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder
import kotlinx.android.synthetic.main.history_list_item.view.* import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder
import mozilla.components.browser.menu.BrowserMenu import org.mozilla.fenix.library.history.viewholders.HistoryListItemViewHolder
import org.mozilla.fenix.components.SectionedAdapter
import java.lang.IllegalStateException import java.lang.IllegalStateException
import java.util.Date import java.util.Date
import java.util.Calendar import java.util.Calendar
private class HistoryList(val history: List<HistoryItem>) { private sealed class AdapterItem {
enum class Range { object DeleteButton : AdapterItem()
Today, ThisWeek, ThisMonth, Older; data class SectionHeader(val range: Range) : AdapterItem()
data class Item(val item: HistoryItem) : AdapterItem()
fun humanReadable(context: Context): String = when (this) { }
Today -> context.getString(R.string.history_today)
ThisWeek -> context.getString(R.string.history_this_week)
ThisMonth -> context.getString(R.string.history_this_month)
Older -> context.getString(R.string.history_older)
}
}
val ranges: List<Range> private enum class Range {
get() = grouped.keys.toList() Today, ThisWeek, ThisMonth, Older;
fun itemsInRange(range: Range): List<HistoryItem> { fun humanReadable(context: Context): String = when (this) {
return grouped[range] ?: listOf() Today -> context.getString(R.string.history_today)
ThisWeek -> context.getString(R.string.history_this_week)
ThisMonth -> context.getString(R.string.history_this_month)
Older -> context.getString(R.string.history_older)
} }
}
fun item(range: Range, index: Int): HistoryItem? = grouped[range]?.let { it[index] } private class HistoryList(val history: List<HistoryItem>) {
val items: List<AdapterItem>
private val grouped: Map<Range, List<HistoryItem>>
init { init {
val oneDayAgo = getDaysAgo(zero_days).time val oneDayAgo = getDaysAgo(zero_days).time
@ -51,8 +45,10 @@ private class HistoryList(val history: List<HistoryItem>) {
val lastWeek = LongRange(sevenDaysAgo, oneDayAgo) val lastWeek = LongRange(sevenDaysAgo, oneDayAgo)
val lastMonth = LongRange(thirtyDaysAgo, sevenDaysAgo) val lastMonth = LongRange(thirtyDaysAgo, sevenDaysAgo)
val items = mutableListOf<AdapterItem>()
items.add(AdapterItem.DeleteButton)
grouped = history.groupBy { item -> val groups = history.groupBy { item ->
when { when {
DateUtils.isToday(item.visitedAt) -> Range.Today DateUtils.isToday(item.visitedAt) -> Range.Today
lastWeek.contains(item.visitedAt) -> Range.ThisWeek lastWeek.contains(item.visitedAt) -> Range.ThisWeek
@ -60,176 +56,86 @@ private class HistoryList(val history: List<HistoryItem>) {
else -> Range.Older else -> Range.Older
} }
} }
}
private fun getDaysAgo(daysAgo: Int): Date { items.addAll(groups.adapterItemsForRange(Range.Today))
val calendar = Calendar.getInstance() items.addAll(groups.adapterItemsForRange(Range.ThisWeek))
calendar.add(Calendar.DAY_OF_YEAR, -daysAgo) items.addAll(groups.adapterItemsForRange(Range.ThisMonth))
items.addAll(groups.adapterItemsForRange(Range.Older))
this.items = items
}
return calendar.time private fun Map<Range, List<HistoryItem>>.adapterItemsForRange(range: Range): List<AdapterItem> {
return this[range]?.let { historyItems ->
val items = mutableListOf<AdapterItem>()
if (historyItems.isNotEmpty()) {
items.add(AdapterItem.SectionHeader(range))
for (item in historyItems) {
items.add(AdapterItem.Item(item))
}
}
items
} ?: listOf()
} }
companion object { companion object {
private const val zero_days = 0 private const val zero_days = 0
private const val seven_days = 7 private const val seven_days = 7
private const val thirty_days = 30 private const val thirty_days = 30
private fun getDaysAgo(daysAgo: Int): Date {
val calendar = Calendar.getInstance()
calendar.add(Calendar.DAY_OF_YEAR, -daysAgo)
return calendar.time
}
} }
} }
class HistoryAdapter( class HistoryAdapter(
private val actionEmitter: Observer<HistoryAction> private val actionEmitter: Observer<HistoryAction>
) : SectionedAdapter() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun numberOfSections(): Int = historyList.ranges.size private var historyList: HistoryList = HistoryList(emptyList())
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
override fun numberOfRowsInSection(section: Int): Int = historyList.itemsInRange(historyList.ranges[section]).size
override fun onCreateHeaderViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { fun updateData(items: List<HistoryItem>, mode: HistoryState.Mode) {
val view = LayoutInflater.from(parent.context).inflate(HistoryHeaderViewHolder.LAYOUT_ID, parent, false) this.historyList = HistoryList(items)
return HistoryHeaderViewHolder(view) this.mode = mode
notifyDataSetChanged()
} }
override fun onBindHeaderViewHolder(holder: RecyclerView.ViewHolder, header: SectionType.Header) { override fun getItemCount(): Int = historyList.items.size
val sectionTitle = historyList.ranges[header.index].humanReadable(holder.itemView.context)
when (holder) { override fun getItemViewType(position: Int): Int {
is HistoryHeaderViewHolder -> holder.bind(sectionTitle) return when (historyList.items[position]) {
is AdapterItem.DeleteButton -> HistoryDeleteButtonViewHolder.LAYOUT_ID
is AdapterItem.SectionHeader -> HistoryHeaderViewHolder.LAYOUT_ID
is AdapterItem.Item -> HistoryListItemViewHolder.LAYOUT_ID
} }
} }
override fun onCreateItemViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
.from(parent.context)
.inflate(HistoryListItemViewHolder.LAYOUT_ID, parent, false)
return HistoryListItemViewHolder(view, actionEmitter)
}
override fun onBindItemViewHolder(holder: RecyclerView.ViewHolder, row: SectionType.Row) {
val item = historyList.ranges[row.section]
.let { historyList.item(it, row.row) } ?: throw IllegalStateException("No item for row: $row")
(holder as? HistoryListItemViewHolder)?.bind(item, mode)
}
class HistoryListItemViewHolder(
view: View,
private val actionEmitter: Observer<HistoryAction>
) : RecyclerView.ViewHolder(view) {
private val checkbox = view.should_remove_checkbox
private val favicon = view.history_favicon
private val title = view.history_title
private val url = view.history_url
private val menuButton = view.history_item_overflow
private var item: HistoryItem? = null
private lateinit var historyMenu: HistoryItemMenu
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
private val checkListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
if (mode is HistoryState.Mode.Normal) {
return@OnCheckedChangeListener
}
item?.apply {
val action = if (isChecked) {
HistoryAction.AddItemForRemoval(this)
} else {
HistoryAction.RemoveItemForRemoval(this)
}
actionEmitter.onNext(action) return when (viewType) {
} HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(view, actionEmitter)
HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view)
HistoryListItemViewHolder.LAYOUT_ID -> HistoryListItemViewHolder(view, actionEmitter)
else -> throw IllegalStateException()
} }
}
init {
setupMenu()
view.setOnClickListener {
if (mode is HistoryState.Mode.Editing) {
checkbox.isChecked = !checkbox.isChecked
return@setOnClickListener
}
item?.apply {
actionEmitter.onNext(HistoryAction.Select(this))
}
}
view.setOnLongClickListener {
item?.apply {
actionEmitter.onNext(HistoryAction.EnterEditMode(this))
}
true
}
menuButton.setOnClickListener {
historyMenu.menuBuilder.build(view.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN)
}
checkbox.setOnCheckedChangeListener(checkListener)
}
fun bind(item: HistoryItem, mode: HistoryState.Mode) {
this.item = item
this.mode = mode
title.text = item.title
url.text = item.url
val isEditing = mode is HistoryState.Mode.Editing
checkbox.visibility = if (isEditing) { View.VISIBLE } else { View.GONE }
favicon.visibility = if (isEditing) { View.INVISIBLE } else { View.VISIBLE }
if (mode is HistoryState.Mode.Editing) {
checkbox.setOnCheckedChangeListener(null)
// Don't set the checkbox if it already contains the right value. override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// This prevent us from cutting off the animation when (holder) {
val shouldCheck = mode.selectedItems.contains(item) is HistoryDeleteButtonViewHolder -> holder.bind(mode)
if (checkbox.isChecked != shouldCheck) { is HistoryHeaderViewHolder -> historyList.items[position].also {
checkbox.isChecked = mode.selectedItems.contains(item) if (it is AdapterItem.SectionHeader) {
holder.bind(it.range.humanReadable(holder.itemView.context))
} }
checkbox.setOnCheckedChangeListener(checkListener)
} }
} is HistoryListItemViewHolder -> (historyList.items[position] as AdapterItem.Item).also {
holder.bind(it.item, mode)
private fun setupMenu() {
this.historyMenu = HistoryItemMenu(itemView.context) {
when (it) {
is HistoryItemMenu.Item.Delete -> {
item?.apply { actionEmitter.onNext(HistoryAction.Delete.One(this)) }
}
}
} }
} }
companion object {
const val LAYOUT_ID = R.layout.history_list_item
}
}
class HistoryHeaderViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
private val title = view.history_header_title
fun bind(title: String) {
this.title.text = title
}
companion object {
const val LAYOUT_ID = R.layout.history_header
}
}
private var historyList: HistoryList = HistoryList(emptyList())
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
fun updateData(items: List<HistoryItem>, mode: HistoryState.Mode) {
this.historyList = HistoryList(items)
this.mode = mode
notifyDataSetChanged()
} }
} }

@ -36,39 +36,13 @@ class HistoryUIView(
adapter = HistoryAdapter(actionEmitter) adapter = HistoryAdapter(actionEmitter)
layoutManager = LinearLayoutManager(container.context) layoutManager = LinearLayoutManager(container.context)
} }
view.delete_history_button.setOnClickListener {
val mode = mode
val action = when (mode) {
is HistoryState.Mode.Normal -> HistoryAction.Delete.All
is HistoryState.Mode.Editing -> HistoryAction.Delete.Some(mode.selectedItems)
}
actionEmitter.onNext(action)
}
} }
override fun updateView() = Consumer<HistoryState> { override fun updateView() = Consumer<HistoryState> {
mode = it.mode mode = it.mode
updateDeleteButton()
(view.history_list.adapter as HistoryAdapter).updateData(it.items, it.mode) (view.history_list.adapter as HistoryAdapter).updateData(it.items, it.mode)
} }
private fun updateDeleteButton() {
val mode = mode
val text = if (mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
view.delete_history_button_text.context.resources.getString(
R.string.history_delete_some,
mode.selectedItems.size
)
} else {
view.delete_history_button_text.context.resources.getString(R.string.history_delete_all)
}
view.delete_history_button.contentDescription = text
view.delete_history_button_text.text = text
}
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
if (mode is HistoryState.Mode.Editing) { if (mode is HistoryState.Mode.Editing) {
actionEmitter.onNext(HistoryAction.BackPressed) actionEmitter.onNext(HistoryAction.BackPressed)

@ -0,0 +1,54 @@
/* 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.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.synthetic.main.delete_history_button.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.HistoryAction
import org.mozilla.fenix.library.history.HistoryState
class HistoryDeleteButtonViewHolder(
view: View,
private val actionEmitter: Observer<HistoryAction>
) : RecyclerView.ViewHolder(view) {
private var mode: HistoryState.Mode? = null
val delete_history_button_text = view.delete_history_button_text
val delete_history_button = view.delete_history_button
init {
delete_history_button.setOnClickListener {
mode?.also {
val action = when (it) {
is HistoryState.Mode.Normal -> HistoryAction.Delete.All
is HistoryState.Mode.Editing -> HistoryAction.Delete.Some(it.selectedItems)
}
actionEmitter.onNext(action)
}
}
}
fun bind(mode: HistoryState.Mode) {
val mode = mode
val text = if (mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
delete_history_button_text.context.resources.getString(
R.string.history_delete_some,
mode.selectedItems.size
)
} else {
delete_history_button_text.context.resources.getString(R.string.history_delete_all)
}
delete_history_button.contentDescription = text
delete_history_button_text.text = text
}
companion object {
const val LAYOUT_ID = R.layout.delete_history_button
}
}

@ -0,0 +1,24 @@
/* 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.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_header.view.*
import org.mozilla.fenix.R
class HistoryHeaderViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
private val title = view.history_header_title
fun bind(title: String) {
this.title.text = title
}
companion object {
const val LAYOUT_ID = R.layout.history_header
}
}

@ -0,0 +1,117 @@
/* 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.viewholders
import android.view.View
import android.widget.CompoundButton
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.synthetic.main.history_list_item.view.*
import mozilla.components.browser.menu.BrowserMenu
import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.HistoryAction
import org.mozilla.fenix.library.history.HistoryItem
import org.mozilla.fenix.library.history.HistoryItemMenu
import org.mozilla.fenix.library.history.HistoryState
class HistoryListItemViewHolder(
view: View,
private val actionEmitter: Observer<HistoryAction>
) : RecyclerView.ViewHolder(view) {
private val checkbox = view.should_remove_checkbox
private val favicon = view.history_favicon
private val title = view.history_title
private val url = view.history_url
private val menuButton = view.history_item_overflow
private var item: HistoryItem? = null
private lateinit var historyMenu: HistoryItemMenu
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
private val checkListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
if (mode is HistoryState.Mode.Normal) {
return@OnCheckedChangeListener
}
item?.apply {
val action = if (isChecked) {
HistoryAction.AddItemForRemoval(this)
} else {
HistoryAction.RemoveItemForRemoval(this)
}
actionEmitter.onNext(action)
}
}
init {
setupMenu()
view.setOnClickListener {
if (mode is HistoryState.Mode.Editing) {
checkbox.isChecked = !checkbox.isChecked
return@setOnClickListener
}
item?.apply {
actionEmitter.onNext(HistoryAction.Select(this))
}
}
view.setOnLongClickListener {
item?.apply {
actionEmitter.onNext(HistoryAction.EnterEditMode(this))
}
true
}
menuButton.setOnClickListener {
historyMenu.menuBuilder.build(view.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN)
}
checkbox.setOnCheckedChangeListener(checkListener)
}
fun bind(item: HistoryItem, mode: HistoryState.Mode) {
this.item = item
this.mode = mode
title.text = item.title
url.text = item.url
val isEditing = mode is HistoryState.Mode.Editing
checkbox.visibility = if (isEditing) { View.VISIBLE } else { View.GONE }
favicon.visibility = if (isEditing) { View.INVISIBLE } else { View.VISIBLE }
if (mode is HistoryState.Mode.Editing) {
checkbox.setOnCheckedChangeListener(null)
// Don't set the checkbox if it already contains the right value.
// This prevent us from cutting off the animation
val shouldCheck = mode.selectedItems.contains(item)
if (checkbox.isChecked != shouldCheck) {
checkbox.isChecked = mode.selectedItems.contains(item)
}
checkbox.setOnCheckedChangeListener(checkListener)
}
}
private fun setupMenu() {
this.historyMenu = HistoryItemMenu(itemView.context) {
when (it) {
is HistoryItemMenu.Item.Delete -> {
item?.apply { actionEmitter.onNext(HistoryAction.Delete.One(this)) }
}
}
}
}
companion object {
const val LAYOUT_ID = R.layout.history_list_item
}
}

@ -9,30 +9,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout
android:id="@+id/delete_history_button"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/button_background"
android:layout_margin="16dp"
android:padding="6dp"
android:clickable="true"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/delete_history_button_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/history_delete_all"
android:textColor="?attr/deleteColor"
android:drawablePadding="8dp"
android:textSize="16sp"
android:gravity="center"
android:clickable="false"
android:focusable="false"
android:layout_gravity="center" />
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/history_list" android:id="@+id/history_list"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -0,0 +1,29 @@
<?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"
android:id="@+id/delete_history_button"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/button_background"
android:layout_margin="16dp"
android:padding="6dp"
android:clickable="true"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/delete_history_button_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/history_delete_all"
android:textColor="?attr/deleteColor"
android:drawablePadding="8dp"
android:textSize="16sp"
android:gravity="center"
android:clickable="false"
android:focusable="false"
android:layout_gravity="center" />
</FrameLayout>
Loading…
Cancel
Save