diff --git a/app/src/main/java/org/mozilla/fenix/components/SectionedAdapter.kt b/app/src/main/java/org/mozilla/fenix/components/SectionedAdapter.kt deleted file mode 100644 index f3e654e1c..000000000 --- a/app/src/main/java/org/mozilla/fenix/components/SectionedAdapter.kt +++ /dev/null @@ -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() { - 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!") - } -} diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt index 0135b1173..a9c213c32 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt @@ -7,42 +7,36 @@ package org.mozilla.fenix.library.history import android.content.Context import android.text.format.DateUtils import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.CompoundButton import androidx.recyclerview.widget.RecyclerView import io.reactivex.Observer import org.mozilla.fenix.R -import kotlinx.android.synthetic.main.history_header.view.* -import kotlinx.android.synthetic.main.history_list_item.view.* -import mozilla.components.browser.menu.BrowserMenu -import org.mozilla.fenix.components.SectionedAdapter +import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder +import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder +import org.mozilla.fenix.library.history.viewholders.HistoryListItemViewHolder import java.lang.IllegalStateException import java.util.Date import java.util.Calendar -private class HistoryList(val history: List) { - enum class Range { - Today, ThisWeek, ThisMonth, Older; - - 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) - } - } +private sealed class AdapterItem { + object DeleteButton : AdapterItem() + data class SectionHeader(val range: Range) : AdapterItem() + data class Item(val item: HistoryItem) : AdapterItem() +} - val ranges: List - get() = grouped.keys.toList() +private enum class Range { + Today, ThisWeek, ThisMonth, Older; - fun itemsInRange(range: Range): List { - return grouped[range] ?: listOf() + 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) } +} - fun item(range: Range, index: Int): HistoryItem? = grouped[range]?.let { it[index] } - - private val grouped: Map> +private class HistoryList(val history: List) { + val items: List init { val oneDayAgo = getDaysAgo(zero_days).time @@ -51,8 +45,10 @@ private class HistoryList(val history: List) { val lastWeek = LongRange(sevenDaysAgo, oneDayAgo) val lastMonth = LongRange(thirtyDaysAgo, sevenDaysAgo) + val items = mutableListOf() + items.add(AdapterItem.DeleteButton) - grouped = history.groupBy { item -> + val groups = history.groupBy { item -> when { DateUtils.isToday(item.visitedAt) -> Range.Today lastWeek.contains(item.visitedAt) -> Range.ThisWeek @@ -60,176 +56,86 @@ private class HistoryList(val history: List) { else -> Range.Older } } - } - private fun getDaysAgo(daysAgo: Int): Date { - val calendar = Calendar.getInstance() - calendar.add(Calendar.DAY_OF_YEAR, -daysAgo) + items.addAll(groups.adapterItemsForRange(Range.Today)) + items.addAll(groups.adapterItemsForRange(Range.ThisWeek)) + items.addAll(groups.adapterItemsForRange(Range.ThisMonth)) + items.addAll(groups.adapterItemsForRange(Range.Older)) + this.items = items + } - return calendar.time + private fun Map>.adapterItemsForRange(range: Range): List { + return this[range]?.let { historyItems -> + val items = mutableListOf() + if (historyItems.isNotEmpty()) { + items.add(AdapterItem.SectionHeader(range)) + for (item in historyItems) { + items.add(AdapterItem.Item(item)) + } + } + items + } ?: listOf() } companion object { private const val zero_days = 0 private const val seven_days = 7 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( private val actionEmitter: Observer -) : SectionedAdapter() { - override fun numberOfSections(): Int = historyList.ranges.size - - override fun numberOfRowsInSection(section: Int): Int = historyList.itemsInRange(historyList.ranges[section]).size +) : RecyclerView.Adapter() { + private var historyList: HistoryList = HistoryList(emptyList()) + private var mode: HistoryState.Mode = HistoryState.Mode.Normal - override fun onCreateHeaderViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(HistoryHeaderViewHolder.LAYOUT_ID, parent, false) - return HistoryHeaderViewHolder(view) + fun updateData(items: List, mode: HistoryState.Mode) { + this.historyList = HistoryList(items) + this.mode = mode + notifyDataSetChanged() } - override fun onBindHeaderViewHolder(holder: RecyclerView.ViewHolder, header: SectionType.Header) { - val sectionTitle = historyList.ranges[header.index].humanReadable(holder.itemView.context) + override fun getItemCount(): Int = historyList.items.size - when (holder) { - is HistoryHeaderViewHolder -> holder.bind(sectionTitle) + override fun getItemViewType(position: Int): Int { + 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 { - val view = LayoutInflater - .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 - ) : 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) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - 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. - // This prevent us from cutting off the animation - val shouldCheck = mode.selectedItems.contains(item) - if (checkbox.isChecked != shouldCheck) { - checkbox.isChecked = mode.selectedItems.contains(item) + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is HistoryDeleteButtonViewHolder -> holder.bind(mode) + is HistoryHeaderViewHolder -> historyList.items[position].also { + if (it is AdapterItem.SectionHeader) { + holder.bind(it.range.humanReadable(holder.itemView.context)) } - 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)) } - } - } + is HistoryListItemViewHolder -> (historyList.items[position] as AdapterItem.Item).also { + holder.bind(it.item, mode) } - } - - 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, mode: HistoryState.Mode) { - this.historyList = HistoryList(items) - this.mode = mode - notifyDataSetChanged() + } } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryUIView.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryUIView.kt index d17423671..67a81e64e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryUIView.kt @@ -36,39 +36,13 @@ class HistoryUIView( adapter = HistoryAdapter(actionEmitter) 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 { mode = it.mode - updateDeleteButton() (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 { if (mode is HistoryState.Mode.Editing) { actionEmitter.onNext(HistoryAction.BackPressed) diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryDeleteButtonViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryDeleteButtonViewHolder.kt new file mode 100644 index 000000000..1ed63d2fb --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryDeleteButtonViewHolder.kt @@ -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 +) : 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryHeaderViewHolder.kt new file mode 100644 index 000000000..2147e9473 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryHeaderViewHolder.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt new file mode 100644 index 000000000..b229e0aa9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt @@ -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 +) : 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 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/component_history.xml b/app/src/main/res/layout/component_history.xml index 7c5382621..0e643b77d 100644 --- a/app/src/main/res/layout/component_history.xml +++ b/app/src/main/res/layout/component_history.xml @@ -9,30 +9,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> - - - - + + + + + \ No newline at end of file