1
0
Fork 0

For #4137 - Adds pagination to the history view

master
ekager 2019-07-24 14:37:10 -07:00 committed by Jeff Boek
parent 0773069dad
commit 4494e40dbc
17 changed files with 525 additions and 425 deletions

View File

@ -0,0 +1,53 @@
/* 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.components.history
import kotlinx.coroutines.runBlocking
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.concept.storage.VisitInfo
import mozilla.components.concept.storage.VisitType
/**
* An Interface for providing a paginated list of [VisitInfo]
*/
interface PagedHistoryProvider {
/**
* Gets a list of [VisitInfo]
* @param offset How much to offset the list by
* @param numberOfItems How many items to fetch
* @param onComplete A callback that returns the list of [VisitInfo]
*/
fun getHistory(offset: Long, numberOfItems: Long, onComplete: (List<VisitInfo>) -> Unit)
}
// A PagedList DataSource runs on a background thread automatically.
// If we run this in our own coroutineScope it breaks the PagedList
fun HistoryStorage.createSynchronousPagedHistoryProvider(): PagedHistoryProvider {
return object : PagedHistoryProvider {
override fun getHistory(
offset: Long,
numberOfItems: Long,
onComplete: (List<VisitInfo>) -> Unit
) {
runBlocking {
val history = this@createSynchronousPagedHistoryProvider.getVisitsPaginated(
offset,
numberOfItems,
listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
)
)
onComplete(history)
}
}
}
}

View File

@ -8,25 +8,14 @@ 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.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.paging.PagedListAdapter
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibrarySiteItemView
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 org.mozilla.fenix.library.history.viewholders.HistoryListItemViewHolder
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
private sealed class AdapterItem { enum class HistoryItemTimeGroup {
object DeleteButton : AdapterItem()
data class SectionHeader(val range: Range) : AdapterItem()
data class Item(val item: HistoryItem) : AdapterItem()
}
private enum class Range {
Today, ThisWeek, ThisMonth, Older; Today, ThisWeek, ThisMonth, Older;
fun humanReadable(context: Context): String = when (this) { fun humanReadable(context: Context): String = when (this) {
@ -37,54 +26,41 @@ private enum class Range {
} }
} }
private class HistoryList(val history: List<HistoryItem>) { class HistoryAdapter(
val items: List<AdapterItem> private val historyInteractor: HistoryInteractor
) : PagedListAdapter<HistoryItem, HistoryListItemViewHolder>(historyDiffCallback) {
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
init { override fun getItemViewType(position: Int): Int = HistoryListItemViewHolder.LAYOUT_ID
val oneDayAgo = getDaysAgo(zero_days).time
val sevenDaysAgo = getDaysAgo(seven_days).time
val thirtyDaysAgo = getDaysAgo(thirty_days).time
val lastWeek = LongRange(sevenDaysAgo, oneDayAgo) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryListItemViewHolder {
val lastMonth = LongRange(thirtyDaysAgo, sevenDaysAgo) val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
val items = mutableListOf<AdapterItem>() return HistoryListItemViewHolder(view, historyInteractor)
items.add(AdapterItem.DeleteButton)
val groups = history.groupBy { item ->
when {
DateUtils.isToday(item.visitedAt) -> Range.Today
lastWeek.contains(item.visitedAt) -> Range.ThisWeek
lastMonth.contains(item.visitedAt) -> Range.ThisMonth
else -> Range.Older
}
}
items.addAll(groups.adapterItemsForRange(Range.Today))
items.addAll(groups.adapterItemsForRange(Range.ThisWeek))
items.addAll(groups.adapterItemsForRange(Range.ThisMonth))
items.addAll(groups.adapterItemsForRange(Range.Older))
// No history only the delete button, so let's clear the list to show the empty text
if (items.size == 1) items.clear()
this.items = items
} }
private fun Map<Range, List<HistoryItem>>.adapterItemsForRange(range: Range): List<AdapterItem> { fun updateMode(mode: HistoryState.Mode) {
return this[range]?.let { historyItems -> this.mode = mode
val items = mutableListOf<AdapterItem>() }
if (historyItems.isNotEmpty()) {
items.add(AdapterItem.SectionHeader(range)) override fun onBindViewHolder(holder: HistoryListItemViewHolder, position: Int) {
for (item in historyItems) { val previous = if (position == 0) null else getItem(position - 1)
items.add(AdapterItem.Item(item)) val current = getItem(position) ?: return
}
} val previousHeader = previous?.let(::timeGroupForHistoryItem)
items val currentHeader = timeGroupForHistoryItem(current)
} ?: listOf() val timeGroup = if (currentHeader != previousHeader) currentHeader else null
holder.bind(current, timeGroup, position == 0, mode)
} }
companion object { companion object {
private const val zero_days = 0 private const val zeroDays = 0
private const val seven_days = 7 private const val sevenDays = 7
private const val thirty_days = 30 private const val thirtyDays = 30
private val oneDayAgo = getDaysAgo(zeroDays).time
private val sevenDaysAgo = getDaysAgo(sevenDays).time
private val thirtyDaysAgo = getDaysAgo(thirtyDays).time
private val lastWeekRange = LongRange(sevenDaysAgo, oneDayAgo)
private val lastMonthRange = LongRange(thirtyDaysAgo, sevenDaysAgo)
private fun getDaysAgo(daysAgo: Int): Date { private fun getDaysAgo(daysAgo: Int): Date {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
@ -92,96 +68,27 @@ private class HistoryList(val history: List<HistoryItem>) {
return calendar.time return calendar.time
} }
}
}
class HistoryAdapter(private val historyInteractor: HistoryInteractor) : private fun timeGroupForHistoryItem(item: HistoryItem): HistoryItemTimeGroup {
RecyclerView.Adapter<RecyclerView.ViewHolder>() { return when {
private var historyList: HistoryList = HistoryList(emptyList()) DateUtils.isToday(item.visitedAt) -> HistoryItemTimeGroup.Today
private var mode: HistoryState.Mode = HistoryState.Mode.Normal lastWeekRange.contains(item.visitedAt) -> HistoryItemTimeGroup.ThisWeek
var selected = listOf<HistoryItem>() lastMonthRange.contains(item.visitedAt) -> HistoryItemTimeGroup.ThisMonth
else -> HistoryItemTimeGroup.Older
fun updateData(items: List<HistoryItem>, mode: HistoryState.Mode) {
val diffUtil = DiffUtil.calculateDiff(
HistoryDiffUtil(
this.historyList,
HistoryList(items),
HistoryList(selected),
HistoryList((mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()),
this.mode,
mode
)
)
this.historyList = HistoryList(items)
this.mode = mode
this.selected = if (mode is HistoryState.Mode.Editing) mode.selectedItems else listOf()
diffUtil.dispatchUpdatesTo(this)
}
private class HistoryDiffUtil(
val old: HistoryList,
val new: HistoryList,
val oldSelected: HistoryList,
val newSelected: HistoryList,
val oldMode: HistoryState.Mode,
val newMode: HistoryState.Mode
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
old.items[oldItemPosition] == new.items[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val modesEqual = oldMode::class == newMode::class
val isStillSelected =
oldSelected.items.contains(old.items[oldItemPosition]) &&
newSelected.items.contains(new.items[newItemPosition])
val isStillNotSelected =
!oldSelected.items.contains(old.items[oldItemPosition]) &&
!newSelected.items.contains(new.items[newItemPosition])
return modesEqual && (isStillSelected || isStillNotSelected)
}
override fun getOldListSize(): Int = old.items.size
override fun getNewListSize(): Int = new.items.size
}
override fun getItemCount(): Int = historyList.items.size
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.ID
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == HistoryListItemViewHolder.ID) {
val view = LibrarySiteItemView(parent.context).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
HistoryListItemViewHolder(view, historyInteractor)
} else {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
when (viewType) {
HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(view, historyInteractor)
HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view)
else -> throw IllegalStateException()
} }
} }
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { private val historyDiffCallback = object : DiffUtil.ItemCallback<HistoryItem>() {
when (holder) { override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean {
is HistoryDeleteButtonViewHolder -> holder.bind(mode) return oldItem == newItem
is HistoryHeaderViewHolder -> historyList.items[position].also {
if (it is AdapterItem.SectionHeader) {
holder.bind(it.range.humanReadable(holder.itemView.context))
}
} }
is HistoryListItemViewHolder -> (historyList.items[position] as AdapterItem.Item).also {
holder.bind(it.item, mode) override fun areContentsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: HistoryItem, newItem: HistoryItem): Any? {
return newItem
} }
} }
} }

View File

@ -0,0 +1,50 @@
/* 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
import androidx.paging.ItemKeyedDataSource
import mozilla.components.concept.storage.VisitInfo
import org.mozilla.fenix.components.history.PagedHistoryProvider
import org.mozilla.fenix.ext.getHostFromUrl
class HistoryDataSource(
private val historyProvider: PagedHistoryProvider
) : ItemKeyedDataSource<Int, HistoryItem>() {
override fun getKey(item: HistoryItem): Int = item.id
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<HistoryItem>
) {
historyProvider.getHistory(INITIAL_OFFSET, params.requestedLoadSize.toLong()) { history ->
val items = history.mapIndexed(transformVisitInfoToHistoryItem(INITIAL_OFFSET.toInt()))
callback.onResult(items)
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<HistoryItem>) {
historyProvider.getHistory(params.key.toLong(), params.requestedLoadSize.toLong()) { history ->
val items = history.mapIndexed(transformVisitInfoToHistoryItem(params.key))
callback.onResult(items)
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<HistoryItem>) {}
companion object {
private const val INITIAL_OFFSET = 0L
fun transformVisitInfoToHistoryItem(offset: Int): (id: Int, visit: VisitInfo) -> HistoryItem {
return { id, visit ->
val title = visit.title
?.takeIf(String::isNotEmpty)
?: visit.url.getHostFromUrl()
?: visit.url
HistoryItem(offset + id, title, visit.url, visit.visitTime)
}
}
}
}

View File

@ -0,0 +1,18 @@
package org.mozilla.fenix.library.history
import androidx.lifecycle.MutableLiveData
import androidx.paging.DataSource
import org.mozilla.fenix.components.history.PagedHistoryProvider
class HistoryDataSourceFactory(
private val historyProvider: PagedHistoryProvider
) : DataSource.Factory<Int, HistoryItem>() {
val datasourceLiveData = MutableLiveData<HistoryDataSource>()
override fun create(): DataSource<Int, HistoryItem> {
val datasource = HistoryDataSource(historyProvider)
datasourceLiveData.postValue(datasource)
return datasource
}
}

View File

@ -18,14 +18,12 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.Navigation import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_history.view.* import kotlinx.android.synthetic.main.fragment_history.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.concept.storage.VisitType
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
@ -34,20 +32,19 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.history.createSynchronousPagedHistoryProvider
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getHostFromUrl
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.simplifiedUrl
import org.mozilla.fenix.share.ShareTab import org.mozilla.fenix.share.ShareTab
import java.util.concurrent.TimeUnit
@SuppressWarnings("TooManyFunctions") @SuppressWarnings("TooManyFunctions")
class HistoryFragment : Fragment(), BackHandler { class HistoryFragment : Fragment(), BackHandler {
private lateinit var historyStore: HistoryStore private lateinit var historyStore: HistoryStore
private lateinit var historyView: HistoryView private lateinit var historyView: HistoryView
private lateinit var historyInteractor: HistoryInteractor private lateinit var historyInteractor: HistoryInteractor
private lateinit var viewModel: HistoryViewModel
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -70,6 +67,7 @@ class HistoryFragment : Fragment(), BackHandler {
::deleteHistoryItems ::deleteHistoryItems
) )
historyView = HistoryView(view.history_layout, historyInteractor) historyView = HistoryView(view.history_layout, historyInteractor)
return view return view
} }
@ -80,18 +78,24 @@ class HistoryFragment : Fragment(), BackHandler {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel = HistoryViewModel(
requireComponents.core.historyStorage.createSynchronousPagedHistoryProvider()
)
requireComponents.analytics.metrics.track(Event.HistoryOpened) requireComponents.analytics.metrics.track(Event.HistoryOpened)
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
fun deleteHistoryItems(items: List<HistoryItem>) { fun deleteHistoryItems(items: Set<HistoryItem>) {
lifecycleScope.launch { lifecycleScope.launch {
val storage = context?.components?.core?.historyStorage val storage = context?.components?.core?.historyStorage
for (item in items) { for (item in items) {
context?.components?.analytics?.metrics?.track(Event.HistoryItemRemoved) context?.components?.analytics?.metrics?.track(Event.HistoryItemRemoved)
storage?.deleteVisit(item.url, item.visitedAt) storage?.deleteVisit(item.url, item.visitedAt)
} }
reloadData() viewModel.invalidate()
historyStore.dispatch(HistoryAction.ExitDeletionMode)
} }
} }
@ -102,7 +106,9 @@ class HistoryFragment : Fragment(), BackHandler {
historyView.update(it) historyView.update(it)
} }
lifecycleScope.launch { reloadData() } viewModel.history.observe(this, Observer {
historyView.historyAdapter.submitList(it)
})
} }
override fun onResume() { override fun onResume() {
@ -137,7 +143,7 @@ class HistoryFragment : Fragment(), BackHandler {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.share_history_multi_select -> { R.id.share_history_multi_select -> {
val selectedHistory = val selectedHistory =
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf() (historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: setOf()
when { when {
selectedHistory.size == 1 -> selectedHistory.size == 1 ->
share(selectedHistory.first().url) share(selectedHistory.first().url)
@ -156,17 +162,18 @@ class HistoryFragment : Fragment(), BackHandler {
R.id.delete_history_multi_select -> { R.id.delete_history_multi_select -> {
val components = context?.applicationContext?.components!! val components = context?.applicationContext?.components!!
val selectedHistory = val selectedHistory =
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf() (historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: setOf()
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {
deleteSelectedHistory(selectedHistory, components) deleteSelectedHistory(selectedHistory, components)
reloadData() viewModel.invalidate()
historyStore.dispatch(HistoryAction.ExitDeletionMode)
} }
true true
} }
R.id.open_history_in_new_tabs_multi_select -> { R.id.open_history_in_new_tabs_multi_select -> {
val selectedHistory = val selectedHistory =
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf() (historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: setOf()
requireComponents.useCases.tabsUseCases.addTab.let { useCase -> requireComponents.useCases.tabsUseCases.addTab.let { useCase ->
for (selectedItem in selectedHistory) { for (selectedItem in selectedHistory) {
requireComponents.analytics.metrics.track(Event.HistoryItemOpened) requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
@ -186,7 +193,7 @@ class HistoryFragment : Fragment(), BackHandler {
} }
R.id.open_history_in_private_tabs_multi_select -> { R.id.open_history_in_private_tabs_multi_select -> {
val selectedHistory = val selectedHistory =
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf() (historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: setOf()
requireComponents.useCases.tabsUseCases.addPrivateTab.let { useCase -> requireComponents.useCases.tabsUseCases.addPrivateTab.let { useCase ->
for (selectedItem in selectedHistory) { for (selectedItem in selectedHistory) {
requireComponents.analytics.metrics.track(Event.HistoryItemOpened) requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
@ -230,8 +237,8 @@ class HistoryFragment : Fragment(), BackHandler {
lifecycleScope.launch { lifecycleScope.launch {
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved) requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
requireComponents.core.historyStorage.deleteEverything() requireComponents.core.historyStorage.deleteEverything()
reloadData() launch(Main) {
launch(Dispatchers.Main) { viewModel.invalidate()
historyStore.dispatch(HistoryAction.ExitDeletionMode) historyStore.dispatch(HistoryAction.ExitDeletionMode)
} }
} }
@ -243,51 +250,8 @@ class HistoryFragment : Fragment(), BackHandler {
} }
} }
private suspend fun reloadData() {
val excludeTypes = listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
)
// Until we have proper pagination, only display a limited set of history to avoid blowing up the UI.
// See https://github.com/mozilla-mobile/fenix/issues/1393
val sinceTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(HISTORY_TIME_DAYS)
var previous: String? = null
val items = requireComponents.core.historyStorage
.getDetailedVisits(sinceTimeMs, excludeTypes = excludeTypes)
// We potentially have a large amount of visits, and multiple processing steps.
// Wrapping iterator in a sequence should make this a little memory-more efficient.
.asSequence()
.sortedByDescending { it.visitTime }
.filter {
val current = it.url.simplifiedUrl()
val isNotDuplicate = current != previous
previous = current
isNotDuplicate
}
.mapIndexed { id, item ->
val title = item.title
?.takeIf(String::isNotEmpty)
?: item.url.getHostFromUrl()
?: item.url
HistoryItem(id, title, item.url, item.visitTime)
}
.toList()
withContext(Main) {
historyStore.dispatch(HistoryAction.Change(items))
}
}
private suspend fun deleteSelectedHistory( private suspend fun deleteSelectedHistory(
selected: List<HistoryItem>, selected: Set<HistoryItem>,
components: Components = requireComponents components: Components = requireComponents
) { ) {
requireComponents.analytics.metrics.track(Event.HistoryItemRemoved) requireComponents.analytics.metrics.track(Event.HistoryItemRemoved)
@ -306,8 +270,4 @@ class HistoryFragment : Fragment(), BackHandler {
) )
nav(R.id.historyFragment, directions) nav(R.id.historyFragment, directions)
} }
companion object {
private const val HISTORY_TIME_DAYS = 3L
}
} }

View File

@ -13,26 +13,43 @@ class HistoryInteractor(
private val openToBrowser: (item: HistoryItem) -> Unit, private val openToBrowser: (item: HistoryItem) -> Unit,
private val displayDeleteAll: () -> Unit, private val displayDeleteAll: () -> Unit,
private val invalidateOptionsMenu: () -> Unit, private val invalidateOptionsMenu: () -> Unit,
private val deleteHistoryItems: (List<HistoryItem>) -> Unit private val deleteHistoryItems: (Set<HistoryItem>) -> Unit
) : HistoryViewInteractor { ) : HistoryViewInteractor {
override fun onHistoryItemOpened(item: HistoryItem) { override fun onItemPress(item: HistoryItem) {
openToBrowser(item) val mode = store.state.mode
when (mode) {
is HistoryState.Mode.Normal -> openToBrowser(item)
is HistoryState.Mode.Editing -> {
val isSelected = mode.selectedItems.contains(item)
if (isSelected) {
store.dispatch(HistoryAction.RemoveItemForRemoval(item))
} else {
store.dispatch(HistoryAction.AddItemForRemoval(item))
}
}
}
} }
override fun onEnterEditMode(selectedItem: HistoryItem) { override fun onItemLongPress(item: HistoryItem) {
store.dispatch(HistoryAction.EnterEditMode(selectedItem)) val isSelected = (store.state.mode as? HistoryState.Mode.Editing)?.let {
it.selectedItems.contains(item)
} ?: false
if (isSelected) {
store.dispatch(HistoryAction.RemoveItemForRemoval(item))
} else {
store.dispatch(HistoryAction.AddItemForRemoval(item))
}
} }
override fun onBackPressed() { override fun onBackPressed(): Boolean {
store.dispatch(HistoryAction.ExitEditMode) return if (store.state.mode is HistoryState.Mode.Editing) {
} store.dispatch(HistoryAction.ExitEditMode)
true
override fun onItemAddedForRemoval(item: HistoryItem) { } else {
store.dispatch(HistoryAction.AddItemForRemoval(item)) false
} }
override fun onItemRemovedForRemoval(item: HistoryItem) {
store.dispatch(HistoryAction.RemoveItemForRemoval(item))
} }
override fun onModeSwitched() { override fun onModeSwitched() {
@ -44,10 +61,10 @@ class HistoryInteractor(
} }
override fun onDeleteOne(item: HistoryItem) { override fun onDeleteOne(item: HistoryItem) {
deleteHistoryItems.invoke(listOf(item)) deleteHistoryItems.invoke(setOf(item))
} }
override fun onDeleteSome(items: List<HistoryItem>) { override fun onDeleteSome(items: Set<HistoryItem>) {
deleteHistoryItems.invoke(items) deleteHistoryItems.invoke(items)
} }
} }

View File

@ -28,7 +28,6 @@ class HistoryStore(initialState: HistoryState) :
*/ */
sealed class HistoryAction : Action { sealed class HistoryAction : Action {
data class Change(val list: List<HistoryItem>) : HistoryAction() data class Change(val list: List<HistoryItem>) : HistoryAction()
data class EnterEditMode(val item: HistoryItem) : HistoryAction()
object ExitEditMode : HistoryAction() object ExitEditMode : HistoryAction()
data class AddItemForRemoval(val item: HistoryItem) : HistoryAction() data class AddItemForRemoval(val item: HistoryItem) : HistoryAction()
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction() data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction()
@ -44,7 +43,7 @@ sealed class HistoryAction : Action {
data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : State { data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : State {
sealed class Mode { sealed class Mode {
object Normal : Mode() object Normal : Mode()
data class Editing(val selectedItems: List<HistoryItem>) : Mode() data class Editing(val selectedItems: Set<HistoryItem>) : Mode()
object Deleting : Mode() object Deleting : Mode()
} }
} }
@ -55,28 +54,21 @@ data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : State {
fun historyStateReducer(state: HistoryState, action: HistoryAction): HistoryState { fun historyStateReducer(state: HistoryState, action: HistoryAction): HistoryState {
return when (action) { return when (action) {
is HistoryAction.Change -> state.copy(mode = HistoryState.Mode.Normal, items = action.list) is HistoryAction.Change -> state.copy(mode = HistoryState.Mode.Normal, items = action.list)
is HistoryAction.EnterEditMode -> state.copy(
mode = HistoryState.Mode.Editing(listOf(action.item))
)
is HistoryAction.AddItemForRemoval -> { is HistoryAction.AddItemForRemoval -> {
val mode = state.mode val mode = state.mode
if (mode is HistoryState.Mode.Editing) { if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems + listOf(action.item) val items = mode.selectedItems + setOf(action.item)
state.copy(mode = HistoryState.Mode.Editing(items)) state.copy(mode = HistoryState.Mode.Editing(items))
} else { } else {
state state.copy(mode = HistoryState.Mode.Editing(setOf(action.item)))
} }
} }
is HistoryAction.RemoveItemForRemoval -> { is HistoryAction.RemoveItemForRemoval -> {
var mode = state.mode var mode = state.mode
if (mode is HistoryState.Mode.Editing) { if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems.filter { it.id != action.item.id } val items = mode.selectedItems.minus(action.item)
mode = if (items.isEmpty()) HistoryState.Mode.Normal else HistoryState.Mode.Editing( state.copy(mode = HistoryState.Mode.Editing(items))
items
)
state.copy(mode = mode)
} else { } else {
state state
} }

View File

@ -14,12 +14,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_history.*
import kotlinx.android.synthetic.main.component_history.view.* import kotlinx.android.synthetic.main.component_history.view.*
import kotlinx.android.synthetic.main.delete_history_button.*
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.asActivity import org.mozilla.fenix.ext.asActivity
@ -31,33 +29,19 @@ import org.mozilla.fenix.ext.getColorResFromAttr
*/ */
interface HistoryViewInteractor { interface HistoryViewInteractor {
/** /**
* Called whenever a history item is tapped to open that history entry in the browser * Called when a user taps a history item
* @param item the history item to open in browser
*/ */
fun onHistoryItemOpened(item: HistoryItem) fun onItemPress(item: HistoryItem)
/** /**
* Called when a history item is long pressed and edit mode is launched * Called when a user long clicks a user
* @param selectedItem the history item to start selected for deletion in edit mode
*/ */
fun onEnterEditMode(selectedItem: HistoryItem) fun onItemLongPress(item: HistoryItem)
/** /**
* Called on backpressed to exit edit mode * Called on backpressed to exit edit mode
*/ */
fun onBackPressed() fun onBackPressed(): Boolean
/**
* Called when a history item is tapped in edit mode and added for removal
* @param item the history item to add to selected items for deletion in edit mode
*/
fun onItemAddedForRemoval(item: HistoryItem)
/**
* Called when a selected history item is tapped in edit mode and removed from removal
* @param item the history item to remove from the selected items for deletion in edit mode
*/
fun onItemRemovedForRemoval(item: HistoryItem)
/** /**
* Called when the mode is switched so we can invalidate the menu * Called when the mode is switched so we can invalidate the menu
@ -79,7 +63,7 @@ interface HistoryViewInteractor {
* Called when multiple history items are deleted * Called when multiple history items are deleted
* @param items the history items to delete * @param items the history items to delete
*/ */
fun onDeleteSome(items: List<HistoryItem>) fun onDeleteSome(items: Set<HistoryItem>)
} }
/** /**
@ -97,18 +81,20 @@ class HistoryView(
override val containerView: View? override val containerView: View?
get() = container get() = container
private val historyAdapter: HistoryAdapter val historyAdapter: HistoryAdapter
private var items: List<HistoryItem> = listOf() private var items: List<HistoryItem> = listOf()
private val context = container.context private val context = container.context
var mode: HistoryState.Mode = HistoryState.Mode.Normal var mode: HistoryState.Mode = HistoryState.Mode.Normal
private set private set
private val activity = context?.asActivity() private val activity = context?.asActivity()
private val layoutManager = LinearLayoutManager(container.context)
init { init {
historyAdapter = HistoryAdapter(interactor)
view.history_list.apply { view.history_list.apply {
historyAdapter = HistoryAdapter(interactor) layoutManager = this@HistoryView.layoutManager
adapter = historyAdapter adapter = historyAdapter
layoutManager = LinearLayoutManager(container.context) (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
} }
@ -117,17 +103,33 @@ class HistoryView(
if (state.mode is HistoryState.Mode.Deleting) View.VISIBLE else View.GONE if (state.mode is HistoryState.Mode.Deleting) View.VISIBLE else View.GONE
if (state.mode != mode) { if (state.mode != mode) {
mode = state.mode
interactor.onModeSwitched() interactor.onModeSwitched()
historyAdapter.updateMode(state.mode)
val oldMode = mode
if (oldMode is HistoryState.Mode.Editing) {
oldMode.selectedItems.forEach {
historyAdapter.notifyItemChanged(it.id)
}
}
} }
(view.history_list.adapter as HistoryAdapter).updateData(state.items, state.mode) (state.mode as? HistoryState.Mode.Editing)?.also {
val oldMode = (mode as? HistoryState.Mode.Editing)
val unselectedItems = oldMode?.selectedItems?.minus(it.selectedItems) ?: setOf()
it.selectedItems.union(unselectedItems).forEach { item ->
historyAdapter.notifyItemChanged(item.id)
}
}
items = state.items items = state.items
when (val mode = mode) { when (val mode = state.mode) {
is HistoryState.Mode.Normal -> setUIForNormalMode(items.isEmpty()) is HistoryState.Mode.Normal -> setUIForNormalMode()
is HistoryState.Mode.Editing -> setUIForSelectingMode(mode.selectedItems.size) is HistoryState.Mode.Editing -> setUIForSelectingMode(mode.selectedItems.size)
} }
mode = state.mode
} }
private fun setUIForSelectingMode(selectedItemSize: Int) { private fun setUIForSelectingMode(selectedItemSize: Int) {
@ -139,10 +141,10 @@ class HistoryView(
) )
} }
private fun setUIForNormalMode(isEmpty: Boolean) { private fun setUIForNormalMode() {
activity?.title = context.getString(R.string.library_history) activity?.title = context.getString(R.string.library_history)
delete_history_button?.isVisible = !isEmpty // history_list?.isVisible = !isEmpty
history_empty_view.isVisible = isEmpty // history_empty_view.isVisible = isEmpty
setToolbarColors( setToolbarColors(
context!!.getColorResFromAttr(R.attr.primaryText), context!!.getColorResFromAttr(R.attr.primaryText),
context.getColorResFromAttr(R.attr.foundation) context.getColorResFromAttr(R.attr.foundation)
@ -186,15 +188,6 @@ class HistoryView(
} }
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
return when (mode) { return interactor.onBackPressed()
is HistoryState.Mode.Editing -> {
mode = HistoryState.Mode.Normal
historyAdapter.updateData(items, mode)
setUIForNormalMode(items.isEmpty())
interactor.onBackPressed()
true
}
else -> false
}
} }
} }

View File

@ -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.history
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.paging.PagedList
import androidx.paging.LivePagedListBuilder
import org.mozilla.fenix.components.history.PagedHistoryProvider
class HistoryViewModel(historyProvider: PagedHistoryProvider) : ViewModel() {
var history: LiveData<PagedList<HistoryItem>>
private val datasource: LiveData<HistoryDataSource>
init {
val historyDataSourceFactory = HistoryDataSourceFactory(historyProvider)
datasource = historyDataSourceFactory.datasourceLiveData
history = LivePagedListBuilder(historyDataSourceFactory, PAGE_SIZE).build()
}
fun invalidate() {
datasource.value?.invalidate()
}
companion object {
private const val PAGE_SIZE = 25
}
}

View File

@ -1,51 +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.history.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.delete_history_button.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.HistoryInteractor
import org.mozilla.fenix.library.history.HistoryState
class HistoryDeleteButtonViewHolder(
view: View,
historyInteractor: HistoryInteractor
) : RecyclerView.ViewHolder(view) {
private var mode: HistoryState.Mode? = null
private val buttonView = view.delete_history_button
init {
buttonView.setOnClickListener {
mode?.also {
when (it) {
is HistoryState.Mode.Normal -> historyInteractor.onDeleteAll()
is HistoryState.Mode.Editing -> historyInteractor.onDeleteSome(it.selectedItems)
}
}
}
}
fun bind(mode: HistoryState.Mode) {
this.mode = mode
buttonView.run {
val isDeleting = mode is HistoryState.Mode.Deleting
if (isDeleting || mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
isEnabled = false
alpha = DISABLED_ALPHA
} else {
isEnabled = true
alpha = 1f
}
}
}
companion object {
const val DISABLED_ALPHA = 0.4f
const val LAYOUT_ID = R.layout.delete_history_button
}
}

View File

@ -1,24 +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.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
}
}

View File

@ -4,19 +4,36 @@
package org.mozilla.fenix.library.history.viewholders package org.mozilla.fenix.library.history.viewholders
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.*
import mozilla.components.browser.menu.BrowserMenu import mozilla.components.browser.menu.BrowserMenu
import org.mozilla.fenix.library.LibrarySiteItemView import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.library.history.HistoryInteractor import org.mozilla.fenix.library.history.HistoryInteractor
import org.mozilla.fenix.library.history.HistoryItem import org.mozilla.fenix.library.history.HistoryItem
import org.mozilla.fenix.library.history.HistoryItemMenu import org.mozilla.fenix.library.history.HistoryItemMenu
import org.mozilla.fenix.library.history.HistoryItemTimeGroup
import org.mozilla.fenix.library.history.HistoryState import org.mozilla.fenix.library.history.HistoryState
class HistoryListItemViewHolder( class HistoryListItemViewHolder(
val view: LibrarySiteItemView, private val view: View,
private val historyInteractor: HistoryInteractor private val historyInteractor: HistoryInteractor
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
private val layout = view.history_layout
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 val headerWrapper = view.header_wrapper
private val headerTitle = view.header_title
private val deleteButtonWrapper = view.delete_button_wrapper
private val deleteButton = view.delete_button
private var item: HistoryItem? = null private var item: HistoryItem? = null
private lateinit var historyMenu: HistoryItemMenu private lateinit var historyMenu: HistoryItemMenu
private var mode: HistoryState.Mode = HistoryState.Mode.Normal private var mode: HistoryState.Mode = HistoryState.Mode.Normal
@ -24,41 +41,122 @@ class HistoryListItemViewHolder(
init { init {
setupMenu() setupMenu()
view.setOnLongClickListener { layout.setOnLongClickListener {
item?.apply { item?.apply {
historyInteractor.onEnterEditMode(this) historyInteractor.onItemLongPress(this)
} }
true true
} }
view.overflowView.setOnClickListener { menuButton.setOnClickListener {
historyMenu.menuBuilder.build(view.context).show( historyMenu.menuBuilder.build(view.context).show(
anchor = it, anchor = it,
orientation = BrowserMenu.Orientation.DOWN orientation = BrowserMenu.Orientation.DOWN
) )
} }
view.displayAs(LibrarySiteItemView.ItemType.SITE) itemView.history_layout.setOnClickListener {
item?.also(historyInteractor::onItemPress)
}
deleteButton.setOnClickListener {
mode?.also {
when (it) {
is HistoryState.Mode.Normal -> historyInteractor.onDeleteAll()
is HistoryState.Mode.Editing -> historyInteractor.onDeleteSome(it.selectedItems)
}
}
}
} }
fun bind(item: HistoryItem, mode: HistoryState.Mode) { fun bind(
item: HistoryItem,
timeGroup: HistoryItemTimeGroup?,
showDeletebutton: Boolean,
mode: HistoryState.Mode
) {
this.item = item this.item = item
this.mode = mode this.mode = mode
view.titleView.text = item.title title.text = item.title
view.urlView.text = item.url url.text = item.url
val selected = mode is HistoryState.Mode.Editing && mode.selectedItems.contains(item) toggleDeleteButton(showDeletebutton, mode)
setClickListeners(item, selected) val headerText = timeGroup?.let { it.humanReadable(view.context) }
toggleHeader(headerText)
view.changeSelected(selected) val selected = toggleSelected(mode, item)
view.loadFavicon(item.url)
if (mode is HistoryState.Mode.Editing) {
val backgroundTint =
if (selected) {
ThemeManager.resolveAttribute(R.attr.accentHighContrast, itemView.context)
} else {
ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
}
val backgroundTintList =
ContextCompat.getColorStateList(itemView.context, backgroundTint)
favicon.backgroundTintList = backgroundTintList
if (selected) {
favicon.setImageResource(R.drawable.mozac_ic_check)
} else {
updateFavIcon(item.url)
}
} else {
val backgroundTint = ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
val backgroundTintList =
ContextCompat.getColorStateList(itemView.context, backgroundTint)
favicon.backgroundTintList = backgroundTintList
updateFavIcon(item.url)
}
}
private fun toggleSelected(
mode: HistoryState.Mode,
item: HistoryItem
): Boolean {
return when (mode) {
is HistoryState.Mode.Editing -> mode.selectedItems.contains(item)
else -> false
}
}
private fun toggleHeader(text: String?) {
text?.also {
headerWrapper.visibility = View.VISIBLE
headerTitle.text = it
} ?: run {
headerWrapper.visibility = View.GONE
}
}
private fun toggleDeleteButton(
showDeletebutton: Boolean,
mode: HistoryState.Mode
) {
if (showDeletebutton) {
deleteButtonWrapper.visibility = View.VISIBLE
deleteButton.run {
val isDeleting = mode is HistoryState.Mode.Deleting
if (isDeleting || mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
isEnabled = false
alpha = DELETE_BUTTON_DISABLED_ALPHA
} else {
isEnabled = true
alpha = 1f
}
}
} else {
deleteButtonWrapper.visibility = View.GONE
}
} }
private fun setupMenu() { private fun setupMenu() {
historyMenu = HistoryItemMenu(view.context) { this.historyMenu = HistoryItemMenu(itemView.context) {
when (it) { when (it) {
is HistoryItemMenu.Item.Delete -> { is HistoryItemMenu.Item.Delete -> {
item?.apply { historyInteractor.onDeleteOne(this) } item?.apply { historyInteractor.onDeleteOne(this) }
@ -67,20 +165,12 @@ class HistoryListItemViewHolder(
} }
} }
private fun setClickListeners( private fun updateFavIcon(url: String) {
item: HistoryItem, favicon.context.components.core.icons.loadIntoView(favicon, url)
selected: Boolean
) {
view.setOnClickListener {
when {
mode == HistoryState.Mode.Normal -> historyInteractor.onHistoryItemOpened(item)
selected -> historyInteractor.onItemRemovedForRemoval(item)
else -> historyInteractor.onItemAddedForRemoval(item)
}
}
} }
companion object { companion object {
val ID = LibrarySiteItemView.ItemType.SITE.ordinal const val DELETE_BUTTON_DISABLED_ALPHA = 0.4f
const val LAYOUT_ID = R.layout.history_list_item
} }
} }

View File

@ -1,17 +0,0 @@
<?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:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_history_button"
style="@style/ThemeIndependentMaterialGreyButtonDestructive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/history_delete_all"
app:rippleColor="?secondaryText" />
</FrameLayout>

View File

@ -2,14 +2,9 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public <!-- 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 - 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/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout
xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/history_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="org.mozilla.fenix.library.history.HistoryFragment"> android:orientation="vertical"/>
<LinearLayout
android:id="@+id/history_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"/>
</androidx.core.widget.NestedScrollView>

View File

@ -1,19 +0,0 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/history_header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="17sp"
android:textColor="?primaryText"/>
</FrameLayout>

View File

@ -0,0 +1,105 @@
<?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"
android:orientation="vertical">
<FrameLayout
android:id="@+id/delete_button_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_button"
style="@style/ThemeIndependentMaterialGreyButtonDestructive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/history_delete_all"
app:rippleColor="?secondaryText" />
</FrameLayout>
<FrameLayout
android:id="@+id/header_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone">
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="17sp"
android:textColor="?primaryText"/>
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/history_layout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="4dp"
android:paddingStart="20dp"
android:paddingEnd="0dp">
<ImageButton
android:id="@+id/history_item_overflow"
android:layout_width="@dimen/glyph_button_width"
android:layout_height="@dimen/glyph_button_height"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/content_description_history_menu"
android:src="@drawable/ic_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/history_favicon"
android:layout_width="@dimen/history_favicon_width_height"
android:layout_height="@dimen/history_favicon_width_height"
android:background="@drawable/favicon_background"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/history_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@id/history_item_overflow"
app:layout_constraintStart_toEndOf="@id/history_favicon"
app:layout_constraintTop_toBottomOf="@id/history_title" />
<TextView
android:id="@+id/history_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?primaryText"
android:textSize="18sp"
android:layout_marginTop="2dp"
app:layout_constraintEnd_toStartOf="@id/history_item_overflow"
app:layout_constraintStart_toEndOf="@id/history_favicon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -28,7 +28,7 @@ object Versions {
const val androidx_testing = "1.1.0-alpha08" const val androidx_testing = "1.1.0-alpha08"
const val androidx_test_ext = "1.0.0" const val androidx_test_ext = "1.0.0"
const val androidx_core = "1.1.0-rc02" const val androidx_core = "1.1.0-rc02"
const val androidx_paging = "2.0.0" const val androidx_paging = "2.1.0"
const val androidx_transition = "1.1.0" const val androidx_transition = "1.1.0"
const val androidx_work = "2.0.1" const val androidx_work = "2.0.1"
const val google_material = "1.1.0-alpha07" const val google_material = "1.1.0-alpha07"
@ -159,7 +159,7 @@ object Deps {
const val androidx_lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}" const val androidx_lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}"
const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidx_lifecycle}" const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidx_lifecycle}"
const val androidx_lifecycle_viewmodel_ss = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.androidx_lifecycle_savedstate}" const val androidx_lifecycle_viewmodel_ss = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.androidx_lifecycle_savedstate}"
const val androidx_paging = "androidx.paging:paging-runtime:${Versions.androidx_paging}" const val androidx_paging = "androidx.paging:paging-runtime-ktx:${Versions.androidx_paging}"
const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}" const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}"
const val androidx_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}" const val androidx_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}"
const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.androidx_navigation}" const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.androidx_navigation}"