[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