migrate reporting to paging 3 (#2205)
* migrate reporting to paging 3 * apply PR feedback
This commit is contained in:
parent
920c71560b
commit
554820de5f
13 changed files with 162 additions and 874 deletions
|
@ -51,7 +51,6 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID))
|
||||
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
|
|
@ -17,25 +17,35 @@ package com.keylesspalace.tusky.components.report
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.PagedList
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||
import com.keylesspalace.tusky.components.report.adapter.StatusesRepository
|
||||
import com.keylesspalace.tusky.components.report.adapter.StatusesPagingSource
|
||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReportViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub,
|
||||
private val statusesRepository: StatusesRepository) : RxAwareViewModel() {
|
||||
private val eventHub: EventHub
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
private val navigationMutable = MutableLiveData<Screen?>()
|
||||
val navigation: LiveData<Screen?> = navigationMutable
|
||||
|
@ -52,11 +62,19 @@ class ReportViewModel @Inject constructor(
|
|||
private val checkUrlMutable = MutableLiveData<String?>()
|
||||
val checkUrl: LiveData<String?> = checkUrlMutable
|
||||
|
||||
private val repoResult = MutableLiveData<BiListing<Status>>()
|
||||
val statuses: LiveData<PagedList<Status>> = Transformations.switchMap(repoResult) { it.pagedList }
|
||||
val networkStateAfter: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkStateAfter }
|
||||
val networkStateBefore: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkStateBefore }
|
||||
val networkStateRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
|
||||
private val accountIdFlow = MutableSharedFlow<String>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
val statusesFlow = accountIdFlow.flatMapLatest { accountId ->
|
||||
Pager(
|
||||
initialKey = statusId,
|
||||
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
||||
pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) }
|
||||
).flow
|
||||
}
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
private val selectedIds = HashSet<String>()
|
||||
val statusViewState = StatusViewState()
|
||||
|
@ -84,7 +102,10 @@ class ReportViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
obtainRelationship()
|
||||
repoResult.value = statusesRepository.getStatuses(accountId, statusId, disposables)
|
||||
|
||||
viewModelScope.launch {
|
||||
accountIdFlow.emit(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateTo(screen: Screen) {
|
||||
|
@ -95,7 +116,6 @@ class ReportViewModel @Inject constructor(
|
|||
navigationMutable.value = null
|
||||
}
|
||||
|
||||
|
||||
private fun obtainRelationship() {
|
||||
val ids = listOf(accountId)
|
||||
muteStateMutable.value = Loading()
|
||||
|
@ -115,7 +135,6 @@ class ReportViewModel @Inject constructor(
|
|||
.autoDispose()
|
||||
}
|
||||
|
||||
|
||||
private fun updateRelationship(relationship: Relationship?) {
|
||||
if (relationship != null) {
|
||||
muteStateMutable.value = Success(relationship.muting)
|
||||
|
@ -194,14 +213,6 @@ class ReportViewModel @Inject constructor(
|
|||
|
||||
}
|
||||
|
||||
fun retryStatusLoad() {
|
||||
repoResult.value?.retry?.invoke()
|
||||
}
|
||||
|
||||
fun refreshStatuses() {
|
||||
repoResult.value?.refresh?.invoke()
|
||||
}
|
||||
|
||||
fun checkClickedUrl(url: String?) {
|
||||
checkUrlMutable.value = url
|
||||
}
|
||||
|
@ -221,5 +232,4 @@ class ReportViewModel @Inject constructor(
|
|||
fun isStatusChecked(id: String): Boolean {
|
||||
return selectedIds.contains(id)
|
||||
}
|
||||
|
||||
}
|
|
@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.report.adapter
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
|
@ -29,7 +29,7 @@ class StatusesAdapter(
|
|||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val statusViewState: StatusViewState,
|
||||
private val adapterHandler: AdapterHandler
|
||||
) : PagedListAdapter<Status, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||
) : PagingDataAdapter<Status, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||
|
||||
private val statusForPosition: (Int) -> Status? = { position: Int ->
|
||||
if (position != RecyclerView.NO_POSITION) getItem(position) else null
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.ItemKeyedDataSource
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.functions.BiFunction
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class StatusesDataSource(private val accountId: String,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor) : ItemKeyedDataSource<String, Status>() {
|
||||
|
||||
val networkStateAfter = MutableLiveData<NetworkState>()
|
||||
val networkStateBefore = MutableLiveData<NetworkState>()
|
||||
|
||||
private var retryAfter: (() -> Any)? = null
|
||||
private var retryBefore: (() -> Any)? = null
|
||||
private var retryInitial: (() -> Any)? = null
|
||||
|
||||
val initialLoad = MutableLiveData<NetworkState>()
|
||||
fun retryAllFailed() {
|
||||
var prevRetry = retryInitial
|
||||
retryInitial = null
|
||||
prevRetry?.let {
|
||||
retryExecutor.execute {
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
prevRetry = retryAfter
|
||||
retryAfter = null
|
||||
prevRetry?.let {
|
||||
retryExecutor.execute {
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
prevRetry = retryBefore
|
||||
retryBefore = null
|
||||
prevRetry?.let {
|
||||
retryExecutor.execute {
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<Status>) {
|
||||
networkStateAfter.postValue(NetworkState.LOADED)
|
||||
networkStateBefore.postValue(NetworkState.LOADED)
|
||||
retryAfter = null
|
||||
retryBefore = null
|
||||
retryInitial = null
|
||||
initialLoad.postValue(NetworkState.LOADING)
|
||||
val initialKey = params.requestedInitialKey
|
||||
if (initialKey == null) {
|
||||
mastodonApi.accountStatusesObservable(accountId, null, null, params.requestedLoadSize, true)
|
||||
} else {
|
||||
mastodonApi.statusObservable(initialKey).zipWith(
|
||||
mastodonApi.accountStatusesObservable(accountId, params.requestedInitialKey, null, params.requestedLoadSize - 1, true),
|
||||
BiFunction { status: Status, list: List<Status> ->
|
||||
val ret = ArrayList<Status>()
|
||||
ret.add(status)
|
||||
ret.addAll(list)
|
||||
return@BiFunction ret
|
||||
})
|
||||
}
|
||||
.doOnSubscribe {
|
||||
disposables.add(it)
|
||||
}
|
||||
.subscribe(
|
||||
{
|
||||
callback.onResult(it)
|
||||
initialLoad.postValue(NetworkState.LOADED)
|
||||
},
|
||||
{
|
||||
retryInitial = {
|
||||
loadInitial(params, callback)
|
||||
}
|
||||
initialLoad.postValue(NetworkState.error(it.message))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Status>) {
|
||||
networkStateAfter.postValue(NetworkState.LOADING)
|
||||
retryAfter = null
|
||||
mastodonApi.accountStatusesObservable(accountId, params.key, null, params.requestedLoadSize, true)
|
||||
.doOnSubscribe {
|
||||
disposables.add(it)
|
||||
}
|
||||
.subscribe(
|
||||
{
|
||||
callback.onResult(it)
|
||||
networkStateAfter.postValue(NetworkState.LOADED)
|
||||
},
|
||||
{
|
||||
retryAfter = {
|
||||
loadAfter(params, callback)
|
||||
}
|
||||
networkStateAfter.postValue(NetworkState.error(it.message))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<Status>) {
|
||||
networkStateBefore.postValue(NetworkState.LOADING)
|
||||
retryBefore = null
|
||||
mastodonApi.accountStatusesObservable(accountId, null, params.key, params.requestedLoadSize, true)
|
||||
.doOnSubscribe {
|
||||
disposables.add(it)
|
||||
}
|
||||
.subscribe(
|
||||
{
|
||||
callback.onResult(it)
|
||||
networkStateBefore.postValue(NetworkState.LOADED)
|
||||
},
|
||||
{
|
||||
retryBefore = {
|
||||
loadBefore(params, callback)
|
||||
}
|
||||
networkStateBefore.postValue(NetworkState.error(it.message))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun getKey(item: Status): String = item.id
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class StatusesDataSourceFactory(
|
||||
private val accountId: String,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor) : DataSource.Factory<String, Status>() {
|
||||
val sourceLiveData = MutableLiveData<StatusesDataSource>()
|
||||
override fun create(): DataSource<String, Status> {
|
||||
val source = StatusesDataSource(accountId, mastodonApi, disposables, retryExecutor)
|
||||
sourceLiveData.postValue(source)
|
||||
return source
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class StatusesPagingSource(
|
||||
private val accountId: String,
|
||||
private val mastodonApi: MastodonApi
|
||||
) : PagingSource<String, Status>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<String, Status>): String? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
state.closestItemToPosition(anchorPosition)?.id
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Status> {
|
||||
val key = params.key
|
||||
try {
|
||||
val result = if (params is LoadParams.Refresh && key != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val initialStatus = async { getSingleStatus(key) }
|
||||
val additionalStatuses = async { getStatusList(maxId = key, limit = params.loadSize - 1) }
|
||||
listOf(initialStatus.await()) + additionalStatuses.await()
|
||||
}
|
||||
} else {
|
||||
val maxId = if (params is LoadParams.Refresh || params is LoadParams.Append) {
|
||||
params.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val minId = if (params is LoadParams.Prepend) {
|
||||
params.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
getStatusList(minId = minId, maxId = maxId, limit = params.loadSize)
|
||||
}
|
||||
return LoadResult.Page(
|
||||
data = result,
|
||||
prevKey = result.firstOrNull()?.id,
|
||||
nextKey = result.lastOrNull()?.id
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.w("StatusesPagingSource", "failed to load statuses", e)
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSingleStatus(statusId: String): Status {
|
||||
return mastodonApi.statusObservable(statusId).await()
|
||||
}
|
||||
|
||||
private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List<Status> {
|
||||
return mastodonApi.accountStatusesObservable(
|
||||
accountId = accountId,
|
||||
maxId = maxId,
|
||||
sinceId = null,
|
||||
minId = minId,
|
||||
limit = limit,
|
||||
excludeReblogs = true
|
||||
).await()
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.Config
|
||||
import androidx.paging.toLiveData
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.BiListing
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class StatusesRepository @Inject constructor(private val mastodonApi: MastodonApi) {
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
fun getStatuses(accountId: String, initialStatus: String?, disposables: CompositeDisposable, pageSize: Int = 20): BiListing<Status> {
|
||||
val sourceFactory = StatusesDataSourceFactory(accountId, mastodonApi, disposables, executor)
|
||||
val livePagedList = sourceFactory.toLiveData(
|
||||
config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2),
|
||||
fetchExecutor = executor, initialLoadKey = initialStatus
|
||||
)
|
||||
return BiListing(
|
||||
pagedList = livePagedList,
|
||||
networkStateBefore = Transformations.switchMap(sourceFactory.sourceLiveData) {
|
||||
it.networkStateBefore
|
||||
},
|
||||
networkStateAfter = Transformations.switchMap(sourceFactory.sourceLiveData) {
|
||||
it.networkStateAfter
|
||||
},
|
||||
retry = {
|
||||
sourceFactory.sourceLiveData.value?.retryAllFailed()
|
||||
},
|
||||
refresh = {
|
||||
sourceFactory.sourceLiveData.value?.invalidate()
|
||||
},
|
||||
refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) {
|
||||
it.initialLoad
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
}
|
|
@ -27,7 +27,12 @@ import com.keylesspalace.tusky.components.report.Screen
|
|||
import com.keylesspalace.tusky.databinding.FragmentReportNoteBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -92,11 +97,9 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
|
|||
binding.progressBar.hide()
|
||||
|
||||
Snackbar.make(binding.buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.apply {
|
||||
setAction(R.string.action_retry) {
|
||||
.setAction(R.string.action_retry) {
|
||||
sendReport()
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ import androidx.core.app.ActivityOptionsCompat
|
|||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.LoadState
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -43,10 +45,11 @@ import com.keylesspalace.tusky.entity.Status
|
|||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler {
|
||||
|
@ -70,13 +73,11 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
|||
when (actionable.attachments[idx].type) {
|
||||
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
|
||||
val attachments = AttachmentViewData.list(actionable)
|
||||
val intent = ViewMediaActivity.newIntent(context, attachments,
|
||||
idx)
|
||||
val intent = ViewMediaActivity.newIntent(context, attachments, idx)
|
||||
if (v != null) {
|
||||
val url = actionable.attachments[idx].url
|
||||
ViewCompat.setTransitionName(v, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(),
|
||||
v, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), v, url)
|
||||
startActivity(intent, options.toBundle())
|
||||
} else {
|
||||
startActivity(intent)
|
||||
|
@ -85,7 +86,6 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
|||
Attachment.Type.UNKNOWN -> {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
|||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
snackbarErrorRetry?.dismiss()
|
||||
viewModel.refreshStatuses()
|
||||
adapter.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,62 +118,46 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
|||
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
)
|
||||
|
||||
adapter = StatusesAdapter(statusDisplayOptions,
|
||||
viewModel.statusViewState, this)
|
||||
adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this)
|
||||
|
||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
viewModel.statuses.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(it)
|
||||
lifecycleScope.launch {
|
||||
viewModel.statusesFlow.collectLatest { pagingData ->
|
||||
adapter.submitData(pagingData)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.networkStateAfter.observe(viewLifecycleOwner) {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
|
||||
binding.progressBarBottom.show()
|
||||
else
|
||||
binding.progressBarBottom.hide()
|
||||
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
if (loadState.refresh is LoadState.Error
|
||||
|| loadState.append is LoadState.Error
|
||||
|| loadState.prepend is LoadState.Error) {
|
||||
showError()
|
||||
}
|
||||
|
||||
viewModel.networkStateBefore.observe(viewLifecycleOwner) {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
|
||||
binding.progressBarTop.show()
|
||||
else
|
||||
binding.progressBarTop.hide()
|
||||
binding.progressBarBottom.visible(loadState.append == LoadState.Loading)
|
||||
binding.progressBarTop.visible(loadState.prepend == LoadState.Loading)
|
||||
binding.progressBarLoading.visible(loadState.refresh == LoadState.Loading && !binding.swipeRefreshLayout.isRefreshing)
|
||||
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
}
|
||||
|
||||
viewModel.networkStateRefresh.observe(viewLifecycleOwner) {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !binding.swipeRefreshLayout.isRefreshing)
|
||||
binding.progressBarLoading.show()
|
||||
else
|
||||
binding.progressBarLoading.hide()
|
||||
|
||||
if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING)
|
||||
if (loadState.refresh != LoadState.Loading) {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
|
||||
private fun showError() {
|
||||
if (snackbarErrorRetry?.isShown != true) {
|
||||
snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE)
|
||||
snackbarErrorRetry?.setAction(R.string.action_retry) {
|
||||
viewModel.retryStatusLoad()
|
||||
adapter.retry()
|
||||
}
|
||||
snackbarErrorRetry?.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleClicks() {
|
||||
binding.buttonCancel.setOnClickListener {
|
||||
viewModel.navigateTo(Screen.Back)
|
||||
|
|
|
@ -577,6 +577,7 @@ interface MastodonApi {
|
|||
@Path("id") accountId: String,
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("min_id") minId: String?,
|
||||
@Query("limit") limit: Int?,
|
||||
@Query("exclude_reblogs") excludeReblogs: Boolean?
|
||||
): Single<List<Status>>
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
|
||||
/**
|
||||
* Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
|
||||
*/
|
||||
data class BiListing<T: Any>(
|
||||
// the LiveData of paged lists for the UI to observe
|
||||
val pagedList: LiveData<PagedList<T>>,
|
||||
// represents the network request status for load data before first to show to the user
|
||||
val networkStateBefore: LiveData<NetworkState>,
|
||||
// represents the network request status for load data after last to show to the user
|
||||
val networkStateAfter: LiveData<NetworkState>,
|
||||
// represents the refresh status to show to the user. Separate from networkState, this
|
||||
// value is importantly only when refresh is requested.
|
||||
val refreshState: LiveData<NetworkState>,
|
||||
// refreshes the whole data and fetches it from scratch.
|
||||
val refresh: () -> Unit,
|
||||
// retries any failed requests.
|
||||
val retry: () -> Unit)
|
|
@ -1,491 +0,0 @@
|
|||
/*
|
||||
* Copyright 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
/**
|
||||
* A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and
|
||||
* {@link androidx.paging.DataSource}s to help with tracking network requests.
|
||||
* <p>
|
||||
* It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL},
|
||||
* {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request
|
||||
* for each of them via {@link #runIfNotRunning(RequestType, Request)}.
|
||||
* <p>
|
||||
* It tracks a {@link Status} and an {@code error} for each {@link RequestType}.
|
||||
* <p>
|
||||
* A sample usage of this class to limit requests looks like this:
|
||||
* <pre>
|
||||
* class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
|
||||
* // TODO replace with an executor from your application
|
||||
* Executor executor = Executors.newSingleThreadExecutor();
|
||||
* PagingRequestHelper helper = new PagingRequestHelper(executor);
|
||||
* // imaginary API service, using Retrofit
|
||||
* MyApi api;
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
|
||||
* helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
|
||||
* helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
|
||||
* new Callback<ApiResponse>() {
|
||||
* {@literal @}Override
|
||||
* public void onResponse(Call<ApiResponse> call,
|
||||
* Response<ApiResponse> response) {
|
||||
* // TODO insert new records into database
|
||||
* helperCallback.recordSuccess();
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onFailure(Call<ApiResponse> call, Throwable t) {
|
||||
* helperCallback.recordFailure(t);
|
||||
* }
|
||||
* }));
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
|
||||
* helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
|
||||
* helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
|
||||
* new Callback<ApiResponse>() {
|
||||
* {@literal @}Override
|
||||
* public void onResponse(Call<ApiResponse> call,
|
||||
* Response<ApiResponse> response) {
|
||||
* // TODO insert new records into database
|
||||
* helperCallback.recordSuccess();
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onFailure(Call<ApiResponse> call, Throwable t) {
|
||||
* helperCallback.recordFailure(t);
|
||||
* }
|
||||
* }));
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
* The helper provides an API to observe combined request status, which can be reported back to the
|
||||
* application based on your business rules.
|
||||
* <pre>
|
||||
* MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
|
||||
* helper.addListener(status -> {
|
||||
* // merge multiple states per request type into one, or dispatch separately depending on
|
||||
* // your application logic.
|
||||
* if (status.hasRunning()) {
|
||||
* combined.postValue(PagingRequestHelper.Status.RUNNING);
|
||||
* } else if (status.hasError()) {
|
||||
* // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
|
||||
* combined.postValue(PagingRequestHelper.Status.FAILED);
|
||||
* } else {
|
||||
* combined.postValue(PagingRequestHelper.Status.SUCCESS);
|
||||
* }
|
||||
* });
|
||||
* </pre>
|
||||
*/
|
||||
// THIS class is likely to be moved into the library in a future release. Feel free to copy it
|
||||
// from this sample.
|
||||
public class PagingRequestHelper {
|
||||
private final Object mLock = new Object();
|
||||
private final Executor mRetryService;
|
||||
@GuardedBy("mLock")
|
||||
private final RequestQueue[] mRequestQueues = new RequestQueue[]
|
||||
{new RequestQueue(RequestType.INITIAL),
|
||||
new RequestQueue(RequestType.BEFORE),
|
||||
new RequestQueue(RequestType.AFTER)};
|
||||
@NonNull
|
||||
final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>();
|
||||
/**
|
||||
* Creates a new PagingRequestHelper with the given {@link Executor} which is used to run
|
||||
* retry actions.
|
||||
*
|
||||
* @param retryService The {@link Executor} that can run the retry actions.
|
||||
*/
|
||||
public PagingRequestHelper(@NonNull Executor retryService) {
|
||||
mRetryService = retryService;
|
||||
}
|
||||
/**
|
||||
* Adds a new listener that will be notified when any request changes {@link Status state}.
|
||||
*
|
||||
* @param listener The listener that will be notified each time a request's status changes.
|
||||
* @return True if it is added, false otherwise (e.g. it already exists in the list).
|
||||
*/
|
||||
@AnyThread
|
||||
public boolean addListener(@NonNull Listener listener) {
|
||||
return mListeners.add(listener);
|
||||
}
|
||||
/**
|
||||
* Removes the given listener from the listeners list.
|
||||
*
|
||||
* @param listener The listener that will be removed.
|
||||
* @return True if the listener is removed, false otherwise (e.g. it never existed)
|
||||
*/
|
||||
public boolean removeListener(@NonNull Listener listener) {
|
||||
return mListeners.remove(listener);
|
||||
}
|
||||
/**
|
||||
* Runs the given {@link Request} if no other requests in the given request type is already
|
||||
* running.
|
||||
* <p>
|
||||
* If run, the request will be run in the current thread.
|
||||
*
|
||||
* @param type The type of the request.
|
||||
* @param request The request to run.
|
||||
* @return True if the request is run, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@AnyThread
|
||||
public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) {
|
||||
boolean hasListeners = !mListeners.isEmpty();
|
||||
StatusReport report = null;
|
||||
synchronized (mLock) {
|
||||
RequestQueue queue = mRequestQueues[type.ordinal()];
|
||||
if (queue.mRunning != null) {
|
||||
return false;
|
||||
}
|
||||
queue.mRunning = request;
|
||||
queue.mStatus = Status.RUNNING;
|
||||
queue.mFailed = null;
|
||||
queue.mLastError = null;
|
||||
if (hasListeners) {
|
||||
report = prepareStatusReportLocked();
|
||||
}
|
||||
}
|
||||
if (report != null) {
|
||||
dispatchReport(report);
|
||||
}
|
||||
final RequestWrapper wrapper = new RequestWrapper(request, this, type);
|
||||
wrapper.run();
|
||||
return true;
|
||||
}
|
||||
@GuardedBy("mLock")
|
||||
private StatusReport prepareStatusReportLocked() {
|
||||
Throwable[] errors = new Throwable[]{
|
||||
mRequestQueues[0].mLastError,
|
||||
mRequestQueues[1].mLastError,
|
||||
mRequestQueues[2].mLastError
|
||||
};
|
||||
return new StatusReport(
|
||||
getStatusForLocked(RequestType.INITIAL),
|
||||
getStatusForLocked(RequestType.BEFORE),
|
||||
getStatusForLocked(RequestType.AFTER),
|
||||
errors
|
||||
);
|
||||
}
|
||||
@GuardedBy("mLock")
|
||||
private Status getStatusForLocked(RequestType type) {
|
||||
return mRequestQueues[type.ordinal()].mStatus;
|
||||
}
|
||||
@AnyThread
|
||||
@VisibleForTesting
|
||||
void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) {
|
||||
StatusReport report = null;
|
||||
final boolean success = throwable == null;
|
||||
boolean hasListeners = !mListeners.isEmpty();
|
||||
synchronized (mLock) {
|
||||
RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()];
|
||||
queue.mRunning = null;
|
||||
queue.mLastError = throwable;
|
||||
if (success) {
|
||||
queue.mFailed = null;
|
||||
queue.mStatus = Status.SUCCESS;
|
||||
} else {
|
||||
queue.mFailed = wrapper;
|
||||
queue.mStatus = Status.FAILED;
|
||||
}
|
||||
if (hasListeners) {
|
||||
report = prepareStatusReportLocked();
|
||||
}
|
||||
}
|
||||
if (report != null) {
|
||||
dispatchReport(report);
|
||||
}
|
||||
}
|
||||
private void dispatchReport(StatusReport report) {
|
||||
for (Listener listener : mListeners) {
|
||||
listener.onStatusChange(report);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Retries all failed requests.
|
||||
*
|
||||
* @return True if any request is retried, false otherwise.
|
||||
*/
|
||||
public boolean retryAllFailed() {
|
||||
final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length];
|
||||
boolean retried = false;
|
||||
synchronized (mLock) {
|
||||
for (int i = 0; i < RequestType.values().length; i++) {
|
||||
toBeRetried[i] = mRequestQueues[i].mFailed;
|
||||
mRequestQueues[i].mFailed = null;
|
||||
}
|
||||
}
|
||||
for (RequestWrapper failed : toBeRetried) {
|
||||
if (failed != null) {
|
||||
failed.retry(mRetryService);
|
||||
retried = true;
|
||||
}
|
||||
}
|
||||
return retried;
|
||||
}
|
||||
static class RequestWrapper implements Runnable {
|
||||
@NonNull
|
||||
final Request mRequest;
|
||||
@NonNull
|
||||
final PagingRequestHelper mHelper;
|
||||
@NonNull
|
||||
final RequestType mType;
|
||||
RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper,
|
||||
@NonNull RequestType type) {
|
||||
mRequest = request;
|
||||
mHelper = helper;
|
||||
mType = type;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
mRequest.run(new Request.Callback(this, mHelper));
|
||||
}
|
||||
void retry(Executor service) {
|
||||
service.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mHelper.runIfNotRunning(mType, mRequest);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Runner class that runs a request tracked by the {@link PagingRequestHelper}.
|
||||
* <p>
|
||||
* When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)}
|
||||
* or {@link Callback#recordSuccess()} once and only once. This call
|
||||
* can be made any time. Until that method call is made, {@link PagingRequestHelper} will
|
||||
* consider the request is running.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Request {
|
||||
/**
|
||||
* Should run the request and call the given {@link Callback} with the result of the
|
||||
* request.
|
||||
*
|
||||
* @param callback The callback that should be invoked with the result.
|
||||
*/
|
||||
void run(Callback callback);
|
||||
/**
|
||||
* Callback class provided to the {@link #run(Callback)} method to report the result.
|
||||
*/
|
||||
class Callback {
|
||||
private final AtomicBoolean mCalled = new AtomicBoolean();
|
||||
private final RequestWrapper mWrapper;
|
||||
private final PagingRequestHelper mHelper;
|
||||
Callback(RequestWrapper wrapper, PagingRequestHelper helper) {
|
||||
mWrapper = wrapper;
|
||||
mHelper = helper;
|
||||
}
|
||||
/**
|
||||
* Call this method when the request succeeds and new data is fetched.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final void recordSuccess() {
|
||||
if (mCalled.compareAndSet(false, true)) {
|
||||
mHelper.recordResult(mWrapper, null);
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
"already called recordSuccess or recordFailure");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Call this method with the failure message and the request can be retried via
|
||||
* {@link #retryAllFailed()}.
|
||||
*
|
||||
* @param throwable The error that occured while carrying out the request.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final void recordFailure(@NonNull Throwable throwable) {
|
||||
//noinspection ConstantConditions
|
||||
if (throwable == null) {
|
||||
throw new IllegalArgumentException("You must provide a throwable describing"
|
||||
+ " the error to record the failure");
|
||||
}
|
||||
if (mCalled.compareAndSet(false, true)) {
|
||||
mHelper.recordResult(mWrapper, throwable);
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
"already called recordSuccess or recordFailure");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Data class that holds the information about the current status of the ongoing requests
|
||||
* using this helper.
|
||||
*/
|
||||
public static final class StatusReport {
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#INITIAL}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status initial;
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#BEFORE}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status before;
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#AFTER}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status after;
|
||||
@NonNull
|
||||
private final Throwable[] mErrors;
|
||||
StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
|
||||
@NonNull Throwable[] errors) {
|
||||
this.initial = initial;
|
||||
this.before = before;
|
||||
this.after = after;
|
||||
this.mErrors = errors;
|
||||
}
|
||||
/**
|
||||
* Convenience method to check if there are any running requests.
|
||||
*
|
||||
* @return True if there are any running requests, false otherwise.
|
||||
*/
|
||||
public boolean hasRunning() {
|
||||
return initial == Status.RUNNING
|
||||
|| before == Status.RUNNING
|
||||
|| after == Status.RUNNING;
|
||||
}
|
||||
/**
|
||||
* Convenience method to check if there are any requests that resulted in an error.
|
||||
*
|
||||
* @return True if there are any requests that finished with error, false otherwise.
|
||||
*/
|
||||
public boolean hasError() {
|
||||
return initial == Status.FAILED
|
||||
|| before == Status.FAILED
|
||||
|| after == Status.FAILED;
|
||||
}
|
||||
/**
|
||||
* Returns the error for the given request type.
|
||||
*
|
||||
* @param type The request type for which the error should be returned.
|
||||
* @return The {@link Throwable} returned by the failing request with the given type or
|
||||
* {@code null} if the request for the given type did not fail.
|
||||
*/
|
||||
@Nullable
|
||||
public Throwable getErrorFor(@NonNull RequestType type) {
|
||||
return mErrors[type.ordinal()];
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StatusReport{"
|
||||
+ "initial=" + initial
|
||||
+ ", before=" + before
|
||||
+ ", after=" + after
|
||||
+ ", mErrors=" + Arrays.toString(mErrors)
|
||||
+ '}';
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
StatusReport that = (StatusReport) o;
|
||||
if (initial != that.initial) return false;
|
||||
if (before != that.before) return false;
|
||||
if (after != that.after) return false;
|
||||
// Probably incorrect - comparing Object[] arrays with Arrays.equals
|
||||
return Arrays.equals(mErrors, that.mErrors);
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = initial.hashCode();
|
||||
result = 31 * result + before.hashCode();
|
||||
result = 31 * result + after.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(mErrors);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Listener interface to get notified by request status changes.
|
||||
*/
|
||||
public interface Listener {
|
||||
/**
|
||||
* Called when the status for any of the requests has changed.
|
||||
*
|
||||
* @param report The current status report that has all the information about the requests.
|
||||
*/
|
||||
void onStatusChange(@NonNull StatusReport report);
|
||||
}
|
||||
/**
|
||||
* Represents the status of a Request for each {@link RequestType}.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* There is current a running request.
|
||||
*/
|
||||
RUNNING,
|
||||
/**
|
||||
* The last request has succeeded or no such requests have ever been run.
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* The last request has failed.
|
||||
*/
|
||||
FAILED
|
||||
}
|
||||
/**
|
||||
* Available request types.
|
||||
*/
|
||||
public enum RequestType {
|
||||
/**
|
||||
* Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the empty state for
|
||||
* a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
INITIAL,
|
||||
/**
|
||||
* Corresponds to the {@code loadBefore} calls in {@link androidx.paging.DataSource} or
|
||||
* {@code onItemAtFrontLoaded} in
|
||||
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
BEFORE,
|
||||
/**
|
||||
* Corresponds to the {@code loadAfter} calls in {@link androidx.paging.DataSource} or
|
||||
* {@code onItemAtEndLoaded} in
|
||||
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
AFTER
|
||||
}
|
||||
class RequestQueue {
|
||||
@NonNull
|
||||
final RequestType mRequestType;
|
||||
@Nullable
|
||||
RequestWrapper mFailed;
|
||||
@Nullable
|
||||
Request mRunning;
|
||||
@Nullable
|
||||
Throwable mLastError;
|
||||
@NonNull
|
||||
Status mStatus = Status.SUCCESS;
|
||||
RequestQueue(@NonNull RequestType requestType) {
|
||||
mRequestType = requestType;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
private fun getErrorMessage(report: PagingRequestHelper.StatusReport): String {
|
||||
return PagingRequestHelper.RequestType.values().mapNotNull {
|
||||
report.getErrorFor(it)?.message
|
||||
}.first()
|
||||
}
|
||||
|
||||
fun PagingRequestHelper.createStatusLiveData(): LiveData<NetworkState> {
|
||||
val liveData = MutableLiveData<NetworkState>()
|
||||
addListener { report ->
|
||||
when {
|
||||
report.hasRunning() -> liveData.postValue(NetworkState.LOADING)
|
||||
report.hasError() -> liveData.postValue(
|
||||
NetworkState.error(getErrorMessage(report)))
|
||||
else -> liveData.postValue(NetworkState.LOADED)
|
||||
}
|
||||
}
|
||||
return liveData
|
||||
}
|
Loading…
Reference in a new issue