Load only the selected group and customizable updated status timeout
Now only the subscriptions from the selected group by the user will be loaded. Also add an option to decide how much time have to pass since the last refresh before the subscription is deemed as not up to date. This helps when a subscription appear in multiple groups, since updating in one will not require to be fetched again in the others.
This commit is contained in:
parent
2948e4190b
commit
b2f317ab7c
20 changed files with 412 additions and 123 deletions
|
|
@ -1,7 +1,6 @@
|
|||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
|
|
@ -10,9 +9,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers
|
|||
import io.reactivex.schedulers.Schedulers
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
|
|
@ -55,6 +54,22 @@ class FeedDatabaseManager(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun outdatedSubscriptions(outdatedThreshold: Date) = feedTable.getAllOutdated(outdatedThreshold)
|
||||
|
||||
fun notLoadedCount(groupId: Long = -1): Flowable<Long> {
|
||||
return if (groupId != -1L) {
|
||||
feedTable.notLoadedCountForGroup(groupId)
|
||||
} else {
|
||||
feedTable.notLoadedCount()
|
||||
}
|
||||
}
|
||||
|
||||
fun outdatedSubscriptionsForGroup(groupId: Long = -1, outdatedThreshold: Date) =
|
||||
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
|
||||
|
||||
fun markAsOutdated(subscriptionId: Long) = feedTable
|
||||
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
|
||||
|
||||
fun upsertAll(subscriptionId: Long, items: List<StreamInfoItem>,
|
||||
oldestAllowedDate: Date = FEED_OLDEST_ALLOWED_DATE.time) {
|
||||
val itemsToInsert = ArrayList<StreamInfoItem>()
|
||||
|
|
@ -77,24 +92,8 @@ class FeedDatabaseManager(context: Context) {
|
|||
|
||||
feedTable.insertAll(feedEntities)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLastUpdated(context: Context): Calendar? {
|
||||
val lastUpdatedMillis = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getLong(context.getString(R.string.feed_last_updated_key), -1)
|
||||
|
||||
val calendar = Calendar.getInstance()
|
||||
if (lastUpdatedMillis > 0) {
|
||||
calendar.timeInMillis = lastUpdatedMillis
|
||||
return calendar
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun setLastUpdated(context: Context, lastUpdated: Calendar?) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||
.putLong(context.getString(R.string.feed_last_updated_key), lastUpdated?.timeInMillis ?: -1).apply()
|
||||
feedTable.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, Calendar.getInstance().time))
|
||||
}
|
||||
|
||||
fun removeOrphansOrOlderStreams(oldestAllowedDate: Date = FEED_OLDEST_ALLOWED_DATE.time) {
|
||||
|
|
@ -147,4 +146,13 @@ class FeedDatabaseManager(context: Context) {
|
|||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>> {
|
||||
return if (groupId == -1L) {
|
||||
feedTable.oldestSubscriptionUpdateFromAll()
|
||||
} else {
|
||||
feedTable.oldestSubscriptionUpdate(groupId)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,14 +35,15 @@ import org.schabi.newpipe.local.feed.service.FeedLoadService
|
|||
import org.schabi.newpipe.report.UserAction
|
||||
import org.schabi.newpipe.util.AnimationUtils.animateView
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import java.util.*
|
||||
|
||||
class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
private lateinit var viewModel: FeedViewModel
|
||||
private lateinit var feedDatabaseManager: FeedDatabaseManager
|
||||
@State @JvmField var listState: Parcelable? = null
|
||||
|
||||
private var groupId = -1L
|
||||
private var groupName = ""
|
||||
private var oldestSubscriptionUpdate: Calendar? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
|
|
@ -54,11 +55,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
|
||||
groupId = arguments?.getLong(KEY_GROUP_ID, -1) ?: -1
|
||||
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
|
||||
|
||||
feedDatabaseManager = FeedDatabaseManager(requireContext())
|
||||
if (feedDatabaseManager.getLastUpdated(requireContext()) == null) {
|
||||
triggerUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
|
@ -193,11 +189,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
|
||||
loading_progress_bar.isIndeterminate = isIndeterminate ||
|
||||
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
loading_progress_bar?.setProgress(progressState.currentProgress, true)
|
||||
} else {
|
||||
loading_progress_bar.progress = progressState.currentProgress
|
||||
}
|
||||
loading_progress_bar.progress = progressState.currentProgress
|
||||
|
||||
loading_progress_bar.max = progressState.maxProgress
|
||||
}
|
||||
|
|
@ -209,9 +201,18 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
listState = null
|
||||
}
|
||||
|
||||
if (!loadedState.itemsErrors.isEmpty()) {
|
||||
oldestSubscriptionUpdate = loadedState.oldestUpdate
|
||||
|
||||
if (loadedState.notLoadedCount > 0) {
|
||||
refresh_subtitle_text.visibility = View.VISIBLE
|
||||
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
|
||||
} else {
|
||||
refresh_subtitle_text.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (loadedState.itemsErrors.isNotEmpty()) {
|
||||
showSnackBarError(loadedState.itemsErrors, UserAction.REQUESTED_FEED,
|
||||
"none", "Loading feed", R.string.general_error);
|
||||
"none", "Loading feed", R.string.general_error)
|
||||
}
|
||||
|
||||
if (loadedState.items.isEmpty()) {
|
||||
|
|
@ -237,13 +238,12 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
}
|
||||
|
||||
private fun updateRefreshViewState() {
|
||||
val lastUpdated = feedDatabaseManager.getLastUpdated(requireContext())
|
||||
val updatedAt = when {
|
||||
lastUpdated != null -> Localization.relativeTime(lastUpdated)
|
||||
val oldestSubscriptionUpdateText = when {
|
||||
oldestSubscriptionUpdate != null -> Localization.relativeTime(oldestSubscriptionUpdate!!)
|
||||
else -> "—"
|
||||
}
|
||||
|
||||
refresh_text?.text = getString(R.string.feed_last_updated, updatedAt)
|
||||
refresh_text?.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -256,7 +256,9 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
override fun hasMoreItems() = false
|
||||
|
||||
private fun triggerUpdate() {
|
||||
getActivity()?.startService(Intent(requireContext(), FeedLoadService::class.java))
|
||||
getActivity()?.startService(Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||
})
|
||||
listState = null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,20 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
|||
import java.util.*
|
||||
|
||||
sealed class FeedState {
|
||||
data class ProgressState(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : FeedState()
|
||||
data class LoadedState(val lastUpdated: Calendar? = null, val items: List<StreamInfoItem>, var itemsErrors: List<Throwable> = emptyList()) : FeedState()
|
||||
data class ErrorState(val error: Throwable? = null) : FeedState()
|
||||
data class ProgressState(
|
||||
val currentProgress: Int = -1,
|
||||
val maxProgress: Int = -1,
|
||||
@StringRes val progressMessage: Int = 0
|
||||
) : FeedState()
|
||||
|
||||
data class LoadedState(
|
||||
val items: List<StreamInfoItem>,
|
||||
val oldestUpdate: Calendar? = null,
|
||||
val notLoadedCount: Long,
|
||||
val itemsErrors: List<Throwable> = emptyList()
|
||||
) : FeedState()
|
||||
|
||||
data class ErrorState(
|
||||
val error: Throwable? = null
|
||||
) : FeedState()
|
||||
}
|
||||
|
|
@ -6,12 +6,13 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.functions.Function4
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.*
|
||||
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(applicationContext: Context, val groupId: Long = -1) : ViewModel() {
|
||||
|
|
@ -23,7 +24,6 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = -1) : ViewM
|
|||
}
|
||||
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
||||
private var subscriptionManager: SubscriptionManager = SubscriptionManager(applicationContext)
|
||||
|
||||
val stateLiveData = MutableLiveData<FeedState>()
|
||||
|
||||
|
|
@ -31,30 +31,30 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = -1) : ViewM
|
|||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
feedDatabaseManager.asStreamItems(groupId),
|
||||
subscriptionManager.subscriptionTable().rowCount(),
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
|
||||
Function3 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long -> return@Function3 Triple(first = t1, second = t2, third = t3) }
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<Date> ->
|
||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
val (event, listFromDB, subsCount) = it
|
||||
val (event, listFromDB, notLoadedCount, oldestUpdate) = it
|
||||
|
||||
var lastUpdated = feedDatabaseManager.getLastUpdated(applicationContext)
|
||||
if (subsCount == 0L && lastUpdated != null) {
|
||||
feedDatabaseManager.setLastUpdated(applicationContext, null)
|
||||
lastUpdated = null
|
||||
}
|
||||
val oldestUpdateCalendar =
|
||||
oldestUpdate?.let { Calendar.getInstance().apply { time = it } }
|
||||
|
||||
stateLiveData.postValue(when (event) {
|
||||
is FeedEventManager.Event.IdleEvent -> FeedState.LoadedState(lastUpdated, listFromDB)
|
||||
is FeedEventManager.Event.ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
||||
is FeedEventManager.Event.SuccessResultEvent -> FeedState.LoadedState(lastUpdated, listFromDB, event.itemsErrors)
|
||||
is FeedEventManager.Event.ErrorResultEvent -> throw event.error
|
||||
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount)
|
||||
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors)
|
||||
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
||||
})
|
||||
|
||||
if (event is FeedEventManager.Event.ErrorResultEvent || event is FeedEventManager.Event.SuccessResultEvent) {
|
||||
if (event is ErrorResultEvent || event is SuccessResultEvent) {
|
||||
FeedEventManager.reset()
|
||||
}
|
||||
}
|
||||
|
|
@ -63,4 +63,6 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = -1) : ViewM
|
|||
super.onCleared()
|
||||
combineDisposable.dispose()
|
||||
}
|
||||
|
||||
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamInfoItem>, val t3: Long, val t4: Date?)
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import android.app.Service
|
|||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
|
@ -71,6 +72,8 @@ class FeedLoadService : Service() {
|
|||
* Number of items to buffer to mass-insert in the database.
|
||||
*/
|
||||
private const val BUFFER_COUNT_BEFORE_INSERT = 20
|
||||
|
||||
const val EXTRA_GROUP_ID: String = "FeedLoadService.EXTRA_GROUP_ID"
|
||||
}
|
||||
|
||||
private var loadingSubscription: Subscription? = null
|
||||
|
|
@ -103,7 +106,15 @@ class FeedLoadService : Service() {
|
|||
}
|
||||
|
||||
setupNotification()
|
||||
startLoading()
|
||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
val groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1)
|
||||
val thresholdOutdatedMinutesString = defaultSharedPreferences
|
||||
.getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
|
||||
val thresholdOutdatedMinutes = thresholdOutdatedMinutesString!!.toInt()
|
||||
|
||||
startLoading(groupId, thresholdOutdatedMinutes)
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
|
|
@ -129,23 +140,31 @@ class FeedLoadService : Service() {
|
|||
// Loading & Handling
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private class RequestException(message: String, cause: Throwable) : Exception(message, cause) {
|
||||
private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
|
||||
companion object {
|
||||
fun wrapList(info: ChannelInfo): List<Throwable> {
|
||||
fun wrapList(subscriptionId: Long, info: ChannelInfo): List<Throwable> {
|
||||
val toReturn = ArrayList<Throwable>(info.errors.size)
|
||||
for (error in info.errors) {
|
||||
toReturn.add(RequestException(info.serviceId.toString() + ":" + info.url, error))
|
||||
toReturn.add(RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, error))
|
||||
}
|
||||
return toReturn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLoading() {
|
||||
private fun startLoading(groupId: Long = -1, thresholdOutdatedMinutes: Int) {
|
||||
feedResultsHolder = ResultsHolder()
|
||||
|
||||
subscriptionManager
|
||||
.subscriptions()
|
||||
val outdatedThreshold = Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, -thresholdOutdatedMinutes)
|
||||
}.time
|
||||
|
||||
val subscriptions = when (groupId) {
|
||||
-1L -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)
|
||||
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
|
||||
}
|
||||
|
||||
subscriptions
|
||||
.limit(1)
|
||||
|
||||
.doOnNext {
|
||||
|
|
@ -174,7 +193,7 @@ class FeedLoadService : Service() {
|
|||
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, channelInfo))
|
||||
} catch (e: Throwable) {
|
||||
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
||||
val wrapper = RequestException(request, e)
|
||||
val wrapper = RequestException(subscriptionEntity.uid, request, e)
|
||||
return@map Notification.createOnError<Pair<Long, ChannelInfo>>(wrapper)
|
||||
}
|
||||
}
|
||||
|
|
@ -235,7 +254,6 @@ class FeedLoadService : Service() {
|
|||
|
||||
postEvent(ProgressEvent(R.string.feed_processing_message))
|
||||
feedDatabaseManager.removeOrphansOrOlderStreams()
|
||||
feedDatabaseManager.setLastUpdated(this@FeedLoadService, feedResultsHolder.lastUpdated)
|
||||
|
||||
postEvent(SuccessResultEvent(feedResultsHolder.itemsErrors))
|
||||
true
|
||||
|
|
@ -266,11 +284,17 @@ class FeedLoadService : Service() {
|
|||
subscriptionManager.updateFromInfo(subscriptionId, info)
|
||||
|
||||
if (info.errors.isNotEmpty()) {
|
||||
feedResultsHolder.addErrors(RequestException.wrapList(info))
|
||||
feedResultsHolder.addErrors(RequestException.wrapList(subscriptionId, info))
|
||||
feedDatabaseManager.markAsOutdated(subscriptionId)
|
||||
}
|
||||
|
||||
} else if (notification.isOnError) {
|
||||
feedResultsHolder.addError(notification.error!!)
|
||||
val error = notification.error!!
|
||||
feedResultsHolder.addError(error)
|
||||
|
||||
if (error is RequestException) {
|
||||
feedDatabaseManager.markAsOutdated(error.subscriptionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -371,11 +395,6 @@ class FeedLoadService : Service() {
|
|||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class ResultsHolder {
|
||||
/**
|
||||
* The time the items have been loaded.
|
||||
*/
|
||||
internal lateinit var lastUpdated: Calendar
|
||||
|
||||
/**
|
||||
* List of errors that may have happen during loading.
|
||||
*/
|
||||
|
|
@ -393,7 +412,6 @@ class FeedLoadService : Service() {
|
|||
|
||||
fun ready() {
|
||||
itemsErrors = itemsErrorsHolder.toList()
|
||||
lastUpdated = Calendar.getInstance()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue