[fenix] For https://github.com/mozilla-mobile/fenix/issues/4137 - Adds pagination to the history view
parent
ef25fff429
commit
cfda0676e7
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue