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:
Mauricio Colli 2019-12-16 04:36:04 -03:00
parent 2948e4190b
commit b2f317ab7c
No known key found for this signature in database
GPG key ID: F200BFD6F29DDD85
20 changed files with 412 additions and 123 deletions

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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?)
}

View file

@ -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()
}
}
}