mirror of https://github.com/tuskyapp/Tusky.git
13 changed files with 162 additions and 874 deletions
@ -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 |
||||
} |
||||
|
||||
) |
||||
} |
||||
} |
||||
@ -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 new issue