From e846f69e38c6f66bdbbab05200ca56d39f4edb94 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Sun, 5 Apr 2020 11:11:03 -0300 Subject: [PATCH 1/9] Add ability to hide played items in a feed - Use components from the new Groupie list library for displaying the feed list. --- .../newpipe/database/feed/dao/FeedDAO.kt | 82 +++++++- .../database/stream/StreamWithState.kt | 17 ++ .../newpipe/local/feed/FeedDatabaseManager.kt | 45 +++-- .../schabi/newpipe/local/feed/FeedFragment.kt | 182 ++++++++++++++++-- .../schabi/newpipe/local/feed/FeedState.kt | 4 +- .../newpipe/local/feed/FeedViewModel.kt | 52 +++-- .../newpipe/local/feed/item/StreamItem.kt | 157 +++++++++++++++ .../res/drawable-night/ic_visibility_off.xml | 9 + .../res/drawable-night/ic_visibility_on.xml | 9 + .../main/res/drawable/ic_visibility_off.xml | 9 + .../main/res/drawable/ic_visibility_on.xml | 9 + .../item_in_history_indicator_background.xml | 7 + .../main/res/layout/list_stream_grid_item.xml | 26 +++ app/src/main/res/layout/list_stream_item.xml | 26 +++ .../main/res/layout/list_stream_mini_item.xml | 26 +++ .../layout/list_stream_playlist_grid_item.xml | 26 +++ .../res/layout/list_stream_playlist_item.xml | 26 +++ app/src/main/res/menu/menu_feed_fragment.xml | 10 +- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/strings.xml | 3 +- 21 files changed, 667 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt create mode 100644 app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt create mode 100644 app/src/main/res/drawable-night/ic_visibility_off.xml create mode 100644 app/src/main/res/drawable-night/ic_visibility_on.xml create mode 100644 app/src/main/res/drawable/ic_visibility_off.xml create mode 100644 app/src/main/res/drawable/ic_visibility_on.xml create mode 100644 app/src/main/res/drawable/item_in_history_indicator_background.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index f216ba1d8d0..f3a4a13b5f6 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -9,7 +9,7 @@ import androidx.room.Update import io.reactivex.rxjava3.core.Flowable import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity -import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.StreamWithState import org.schabi.newpipe.database.subscription.SubscriptionEntity import java.time.OffsetDateTime @@ -20,21 +20,34 @@ abstract class FeedDAO { @Query( """ - SELECT s.* FROM streams s + SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history + FROM streams s + + LEFT JOIN stream_state sst + ON s.uid = sst.stream_id + + LEFT JOIN stream_history sh + ON s.uid = sh.stream_id INNER JOIN feed f ON s.uid = f.stream_id ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC - LIMIT 500 """ ) - abstract fun getAllStreams(): Flowable> + abstract fun getAllStreams(): Flowable> @Query( """ - SELECT s.* FROM streams s + SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history + FROM streams s + + LEFT JOIN stream_state sst + ON s.uid = sst.stream_id + + LEFT JOIN stream_history sh + ON s.uid = sh.stream_id INNER JOIN feed f ON s.uid = f.stream_id @@ -42,16 +55,69 @@ abstract class FeedDAO { INNER JOIN feed_group_subscription_join fgs ON fgs.subscription_id = f.subscription_id - INNER JOIN feed_group fg - ON fg.uid = fgs.group_id + WHERE fgs.group_id = :groupId + + ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC + LIMIT 500 + """ + ) + abstract fun getAllStreamsForGroup(groupId: Long): Flowable> + + @Query( + """ + SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history + FROM streams s + + LEFT JOIN stream_state sst + ON s.uid = sst.stream_id + + LEFT JOIN stream_history sh + ON s.uid = sh.stream_id + + INNER JOIN feed f + ON s.uid = f.stream_id + + WHERE ( + sh.stream_id IS NULL + OR s.stream_type = 'LIVE_STREAM' + OR s.stream_type = 'AUDIO_LIVE_STREAM' + ) + + ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC + LIMIT 500 + """ + ) + abstract fun getLiveOrNotPlayedStreams(): Flowable> + + @Query( + """ + SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history + FROM streams s + + LEFT JOIN stream_state sst + ON s.uid = sst.stream_id + + LEFT JOIN stream_history sh + ON s.uid = sh.stream_id + + INNER JOIN feed f + ON s.uid = f.stream_id + + INNER JOIN feed_group_subscription_join fgs + ON fgs.subscription_id = f.subscription_id WHERE fgs.group_id = :groupId + AND ( + sh.stream_id IS NULL + OR s.stream_type = 'LIVE_STREAM' + OR s.stream_type = 'AUDIO_LIVE_STREAM' + ) ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC LIMIT 500 """ ) - abstract fun getAllStreamsFromGroup(groupId: Long): Flowable> + abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable> @Query( """ diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt new file mode 100644 index 00000000000..40c7862460e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt @@ -0,0 +1,17 @@ +package org.schabi.newpipe.database.stream + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamStateEntity + +data class StreamWithState( + @Embedded + val stream: StreamEntity, + + @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME) + val stateProgressTime: Long?, + + @ColumnInfo(name = "is_stream_in_history") + val isInHistory: Boolean = false +) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index 9a4832c81f5..ff7c2848e6a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt @@ -12,6 +12,7 @@ import org.schabi.newpipe.NewPipeDatabase 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.StreamWithState import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamType @@ -38,16 +39,19 @@ class FeedDatabaseManager(context: Context) { fun database() = database - fun asStreamItems(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable> { - val streams = when (groupId) { - FeedGroupEntity.GROUP_ALL_ID -> feedTable.getAllStreams() - else -> feedTable.getAllStreamsFromGroup(groupId) - } - - return streams.map { - val items = ArrayList(it.size) - it.mapTo(items) { stream -> stream.toStreamInfoItem() } - return@map items + fun getStreams( + groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + getPlayedStreams: Boolean = true + ): Flowable> { + return when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> { + if (getPlayedStreams) feedTable.getAllStreams() + else feedTable.getLiveOrNotPlayedStreams() + } + else -> { + if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId) + else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId) + } } } @@ -60,8 +64,10 @@ class FeedDatabaseManager(context: Context) { } } - fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) = - feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold) + fun outdatedSubscriptionsForGroup( + groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + outdatedThreshold: OffsetDateTime + ) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold) fun markAsOutdated(subscriptionId: Long) = feedTable .setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null)) @@ -93,10 +99,7 @@ class FeedDatabaseManager(context: Context) { } feedTable.setLastUpdatedForSubscription( - FeedLastUpdatedEntity( - subscriptionId, - OffsetDateTime.now(ZoneOffset.UTC) - ) + FeedLastUpdatedEntity(subscriptionId, OffsetDateTime.now(ZoneOffset.UTC)) ) } @@ -108,7 +111,12 @@ class FeedDatabaseManager(context: Context) { fun clear() { feedTable.deleteAll() val deletedOrphans = streamTable.deleteOrphans() - if (DEBUG) Log.d(this::class.java.simpleName, "clear() → streamTable.deleteOrphans() → $deletedOrphans") + if (DEBUG) { + Log.d( + this::class.java.simpleName, + "clear() → streamTable.deleteOrphans() → $deletedOrphans" + ) + } } // ///////////////////////////////////////////////////////////////////////// @@ -122,7 +130,8 @@ class FeedDatabaseManager(context: Context) { } fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List): Completable { - return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) } + return Completable + .fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index 0d7a9a11f67..d6d75fec5d9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -19,7 +19,10 @@ package org.schabi.newpipe.local.feed +import android.annotation.SuppressLint +import android.app.Activity import android.content.Intent +import android.content.res.Configuration import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -30,11 +33,18 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.Nullable import androidx.appcompat.app.AlertDialog +import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.edit import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.GridLayoutManager +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.GroupieViewHolder +import com.xwray.groupie.Item +import com.xwray.groupie.OnItemClickListener +import com.xwray.groupie.OnItemLongClickListener import icepick.State import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Single @@ -49,33 +59,43 @@ import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.error.UserAction import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty -import org.schabi.newpipe.fragments.list.BaseListFragment +import org.schabi.newpipe.fragments.BaseStateFragment +import org.schabi.newpipe.info_list.InfoItemDialog import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling +import org.schabi.newpipe.local.feed.item.StreamItem import org.schabi.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.local.subscription.SubscriptionManager +import org.schabi.newpipe.player.helper.PlayerHolder import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.NavigationHelper +import org.schabi.newpipe.util.StreamDialogEntry import java.time.OffsetDateTime +import java.util.ArrayList +import kotlin.math.floor +import kotlin.math.max -class FeedFragment : BaseListFragment() { +class FeedFragment : BaseStateFragment() { private var _feedBinding: FragmentFeedBinding? = null private val feedBinding get() = _feedBinding!! private val disposables = CompositeDisposable() private lateinit var viewModel: FeedViewModel - @State - @JvmField - var listState: Parcelable? = null + @State @JvmField var listState: Parcelable? = null private var groupId = FeedGroupEntity.GROUP_ALL_ID private var groupName = "" private var oldestSubscriptionUpdate: OffsetDateTime? = null + private lateinit var groupAdapter: GroupAdapter + @State @JvmField var showPlayedItems: Boolean = true + init { setHasOptionsMenu(true) - setUseDefaultStateSaving(false) } override fun onCreate(savedInstanceState: Bundle?) { @@ -95,8 +115,22 @@ class FeedFragment : BaseListFragment() { _feedBinding = FragmentFeedBinding.bind(rootView) super.onViewCreated(rootView, savedInstanceState) - viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java) - viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) } + val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems) + viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java) + viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) }) + + groupAdapter = GroupAdapter().apply { + setOnItemClickListener(listenerStreamItem) + setOnItemLongClickListener(listenerStreamItem) + spanCount = if (shouldUseGridLayout()) getGridSpanCount() else 1 + } + + feedBinding.itemsList.apply { + layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply { + spanSizeLookup = groupAdapter.spanSizeLookup + } + adapter = groupAdapter + } } override fun onPause() { @@ -129,13 +163,18 @@ class FeedFragment : BaseListFragment() { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) + + activity.supportActionBar?.setDisplayShowTitleEnabled(true) activity.supportActionBar?.setTitle(R.string.fragment_feed_title) activity.supportActionBar?.subtitle = groupName inflater.inflate(R.menu.menu_feed_fragment, menu) - if (useAsFrontPage) { - menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) + menu.findItem(R.id.menu_item_feed_toggle_played_items).apply { + updateTogglePlayedItemsButton(this) + if (useAsFrontPage) { + setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) + } } } @@ -143,7 +182,8 @@ class FeedFragment : BaseListFragment() { if (item.itemId == R.id.menu_item_feed_help) { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) - val usingDedicatedMethod = sharedPreferences.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) + val usingDedicatedMethod = sharedPreferences + .getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) val enableDisableButtonText = when { usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button else -> R.string.feed_use_dedicated_fetch_method_enable_button @@ -160,6 +200,10 @@ class FeedFragment : BaseListFragment() { .create() .show() return true + } else if (item.itemId == R.id.menu_item_feed_toggle_played_items) { + showPlayedItems = !item.isChecked + updateTogglePlayedItemsButton(item) + viewModel.togglePlayedItems(showPlayedItems) } return super.onOptionsItemSelected(item) @@ -177,13 +221,22 @@ class FeedFragment : BaseListFragment() { } override fun onDestroyView() { + feedBinding.itemsList.adapter = null _feedBinding = null super.onDestroyView() } - // ///////////////////////////////////////////////////////////////////////// + private fun updateTogglePlayedItemsButton(menuItem: MenuItem) { + menuItem.isChecked = showPlayedItems + menuItem.icon = AppCompatResources.getDrawable( + requireContext(), + if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off + ) + } + + // ////////////////////////////////////////////////////////////////////////// // Handling - // ///////////////////////////////////////////////////////////////////////// + // ////////////////////////////////////////////////////////////////////////// override fun showLoading() { super.showLoading() @@ -195,6 +248,7 @@ class FeedFragment : BaseListFragment() { override fun hideLoading() { super.hideLoading() + feedBinding.itemsList.animate(true, 0) feedBinding.refreshRootView.animate(true, 200) feedBinding.loadingProgressText.animate(false, 0) feedBinding.swipeRefreshLayout.isRefreshing = false @@ -220,7 +274,6 @@ class FeedFragment : BaseListFragment() { override fun handleError() { super.handleError() - infoListAdapter.clearStreamItemList() feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling() feedBinding.refreshRootView.animate(false, 0) feedBinding.loadingProgressText.animate(false, 0) @@ -248,8 +301,71 @@ class FeedFragment : BaseListFragment() { feedBinding.loadingProgressBar.max = progressState.maxProgress } + private fun showStreamDialog(item: StreamInfoItem) { + val context = context + val activity: Activity? = getActivity() + if (context == null || context.resources == null || activity == null) return + + val entries = ArrayList() + if (PlayerHolder.getType() != null) { + entries.add(StreamDialogEntry.enqueue) + } + if (item.streamType == StreamType.AUDIO_STREAM) { + entries.addAll( + listOf( + StreamDialogEntry.start_here_on_background, + StreamDialogEntry.append_playlist, + StreamDialogEntry.share + ) + ) + } else { + entries.addAll( + listOf( + StreamDialogEntry.start_here_on_background, + StreamDialogEntry.start_here_on_popup, + StreamDialogEntry.append_playlist, + StreamDialogEntry.share + ) + ) + } + + InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which -> + StreamDialogEntry.clickOn(which, this, item) + }.show() + } + + private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener { + override fun onItemClick(item: Item<*>, view: View) { + if (item is StreamItem) { + val stream = item.streamWithState.stream + NavigationHelper.openVideoDetailFragment( + requireContext(), fm, + stream.serviceId, stream.url, stream.title, null, false + ) + } + } + + override fun onItemLongClick(item: Item<*>, view: View): Boolean { + if (item is StreamItem) { + showStreamDialog(item.streamWithState.stream.toStreamInfoItem()) + return true + } + return false + } + } + + @SuppressLint("StringFormatMatches") private fun handleLoadedState(loadedState: FeedState.LoadedState) { - infoListAdapter.setInfoItemList(loadedState.items) + + val itemVersion = if (shouldUseGridLayout()) { + StreamItem.ItemVersion.GRID + } else { + StreamItem.ItemVersion.NORMAL + } + loadedState.items.forEach { it.itemVersion = itemVersion } + + groupAdapter.updateAsync(loadedState.items, false, null) + listState?.run { feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState) listState = null @@ -357,7 +473,10 @@ class FeedFragment : BaseListFragment() { private fun updateRelativeTimeViews() { updateRefreshViewState() - infoListAdapter.notifyDataSetChanged() + groupAdapter.notifyItemRangeChanged( + 0, groupAdapter.itemCount, + StreamItem.UPDATE_RELATIVE_TIME + ) } private fun updateRefreshViewState() { @@ -372,8 +491,6 @@ class FeedFragment : BaseListFragment() { // ///////////////////////////////////////////////////////////////////////// override fun doInitialLoadLogic() {} - override fun loadMoreItems() {} - override fun hasMoreItems() = false override fun reloadContent() { getActivity()?.startService( @@ -384,6 +501,35 @@ class FeedFragment : BaseListFragment() { listState = null } + // ///////////////////////////////////////////////////////////////////////// + // Grid Mode + // ///////////////////////////////////////////////////////////////////////// + + // TODO: Move these out of this class, as it can be reused + + private fun shouldUseGridLayout(): Boolean { + val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value)) + + return when (listMode) { + getString(R.string.list_view_mode_auto_key) -> { + val configuration = resources.configuration + + ( + configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && + configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE) + ) + } + getString(R.string.list_view_mode_grid_key) -> true + else -> false + } + } + + private fun getGridSpanCount(): Int { + val minWidth = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width) + return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt()) + } + companion object { const val KEY_GROUP_ID = "ARG_GROUP_ID" const val KEY_GROUP_NAME = "ARG_GROUP_NAME" diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt index dec2773e124..27613e83e9c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt @@ -1,7 +1,7 @@ package org.schabi.newpipe.local.feed import androidx.annotation.StringRes -import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.local.feed.item.StreamItem import java.time.OffsetDateTime sealed class FeedState { @@ -12,7 +12,7 @@ sealed class FeedState { ) : FeedState() data class LoadedState( - val items: List, + val items: List, val oldestUpdate: OffsetDateTime? = null, val notLoadedCount: Long, val itemsErrors: List = emptyList() diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index e516cdacaf8..8bdf412b5a2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -8,9 +8,11 @@ import androidx.lifecycle.ViewModelProvider import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.functions.Function4 +import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.schedulers.Schedulers import org.schabi.newpipe.database.feed.model.FeedGroupEntity -import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.database.stream.StreamWithState +import org.schabi.newpipe.local.feed.item.StreamItem import org.schabi.newpipe.local.feed.service.FeedEventManager import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent @@ -20,26 +22,33 @@ import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT import java.time.OffsetDateTime import java.util.concurrent.TimeUnit -class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() { - class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return FeedViewModel(context.applicationContext, groupId) as T - } - } - +class FeedViewModel( + applicationContext: Context, + groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + initialShowPlayedItems: Boolean = true +) : ViewModel() { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) + private val toggleShowPlayedItems = BehaviorProcessor.create() + private val streamItems = toggleShowPlayedItems + .startWithItem(initialShowPlayedItems) + .distinctUntilChanged() + .switchMap { showPlayedItems -> + feedDatabaseManager.getStreams(groupId, showPlayedItems) + } + private val mutableStateLiveData = MutableLiveData() val stateLiveData: LiveData = mutableStateLiveData private var combineDisposable = Flowable .combineLatest( FeedEventManager.events(), - feedDatabaseManager.asStreamItems(groupId), + streamItems, feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId), - Function4 { t1: FeedEventManager.Event, t2: List, t3: Long, t4: List -> + + Function4 { t1: FeedEventManager.Event, t2: List, + t3: Long, t4: List -> return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull()) } ) @@ -49,9 +58,9 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn .subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) -> mutableStateLiveData.postValue( when (event) { - is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount) + is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount) is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage) - is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount, event.itemsErrors) + is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors) is ErrorResultEvent -> FeedState.ErrorState(event.error) } ) @@ -66,5 +75,20 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn combineDisposable.dispose() } - private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List, val t3: Long, val t4: OffsetDateTime?) + private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List, val t3: Long, val t4: OffsetDateTime?) + + fun togglePlayedItems(showPlayedItems: Boolean) { + toggleShowPlayedItems.onNext(showPlayedItems) + } + + class Factory( + private val context: Context, + private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + private val showPlayedItems: Boolean + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt new file mode 100644 index 00000000000..88c5d809d2f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt @@ -0,0 +1,157 @@ +package org.schabi.newpipe.local.feed.item + +import android.content.Context +import android.text.TextUtils +import android.view.View +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import com.nostra13.universalimageloader.core.ImageLoader +import com.xwray.groupie.viewbinding.BindableItem +import org.schabi.newpipe.MainActivity +import org.schabi.newpipe.R +import org.schabi.newpipe.database.stream.StreamWithState +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.databinding.ListStreamItemBinding +import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM +import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM +import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM +import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM +import org.schabi.newpipe.util.ImageDisplayConstants +import org.schabi.newpipe.util.Localization +import java.util.concurrent.TimeUnit + +data class StreamItem( + val streamWithState: StreamWithState, + var itemVersion: ItemVersion = ItemVersion.NORMAL +) : BindableItem() { + companion object { + const val UPDATE_RELATIVE_TIME = 1 + } + + private val stream: StreamEntity = streamWithState.stream + private val stateProgressTime: Long? = streamWithState.stateProgressTime + private val isInHistory: Boolean = streamWithState.isInHistory + + override fun getId(): Long = stream.uid + + enum class ItemVersion { NORMAL, MINI, GRID } + + override fun getLayout(): Int = when (itemVersion) { + ItemVersion.NORMAL -> R.layout.list_stream_item + ItemVersion.MINI -> R.layout.list_stream_mini_item + ItemVersion.GRID -> R.layout.list_stream_grid_item + } + + override fun initializeViewBinding(view: View) = ListStreamItemBinding.bind(view) + + override fun bind(viewBinding: ListStreamItemBinding, position: Int, payloads: MutableList) { + if (payloads.contains(UPDATE_RELATIVE_TIME)) { + if (itemVersion != ItemVersion.MINI) { + viewBinding.itemAdditionalDetails.text = + getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context) + } + return + } + + super.bind(viewBinding, position, payloads) + } + + override fun bind(viewBinding: ListStreamItemBinding, position: Int) { + viewBinding.itemVideoTitleView.text = stream.title + viewBinding.itemUploaderView.text = stream.uploader + + val isLiveStream = stream.streamType == LIVE_STREAM || stream.streamType == AUDIO_LIVE_STREAM + + if (stream.duration > 0) { + viewBinding.itemDurationView.text = Localization.getDurationString(stream.duration) + viewBinding.itemDurationView.setBackgroundColor( + ContextCompat.getColor( + viewBinding.itemDurationView.context, + R.color.duration_background_color + ) + ) + viewBinding.itemDurationView.visibility = View.VISIBLE + + if (stateProgressTime != null) { + viewBinding.itemProgressView.visibility = View.VISIBLE + viewBinding.itemProgressView.max = stream.duration.toInt() + viewBinding.itemProgressView.progress = TimeUnit.MILLISECONDS.toSeconds(stateProgressTime).toInt() + } else { + viewBinding.itemProgressView.visibility = View.GONE + } + } else if (isLiveStream) { + viewBinding.itemDurationView.setText(R.string.duration_live) + viewBinding.itemDurationView.setBackgroundColor( + ContextCompat.getColor( + viewBinding.itemDurationView.context, + R.color.live_duration_background_color + ) + ) + viewBinding.itemDurationView.visibility = View.VISIBLE + viewBinding.itemProgressView.visibility = View.GONE + } else { + viewBinding.itemDurationView.visibility = View.GONE + viewBinding.itemProgressView.visibility = View.GONE + } + + viewBinding.itemInHistoryIndicatorView.visibility = + if (isInHistory && !isLiveStream) View.VISIBLE else View.GONE + + ImageLoader.getInstance().displayImage( + stream.thumbnailUrl, viewBinding.itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS + ) + + if (itemVersion != ItemVersion.MINI) { + viewBinding.itemAdditionalDetails.text = + getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context) + } + } + + override fun isLongClickable() = when (stream.streamType) { + AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true + else -> false + } + + private fun getStreamInfoDetailLine(context: Context): String { + var viewsAndDate = "" + val viewCount = stream.viewCount + if (viewCount != null && viewCount >= 0) { + viewsAndDate = when (stream.streamType) { + AUDIO_LIVE_STREAM -> Localization.listeningCount(context, viewCount) + LIVE_STREAM -> Localization.shortWatchingCount(context, viewCount) + else -> Localization.shortViewCount(context, viewCount) + } + } + val uploadDate = getFormattedRelativeUploadDate(context) + return when { + !TextUtils.isEmpty(uploadDate) -> when { + viewsAndDate.isEmpty() -> uploadDate!! + else -> Localization.concatenateStrings(viewsAndDate, uploadDate) + } + else -> viewsAndDate + } + } + + private fun getFormattedRelativeUploadDate(context: Context): String? { + val uploadDate = stream.uploadDate + return if (uploadDate != null) { + var formattedRelativeTime = Localization.relativeTime(uploadDate) + + if (MainActivity.DEBUG) { + val key = context.getString(R.string.show_original_time_ago_key) + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, false)) { + formattedRelativeTime += " (" + stream.textualUploadDate + ")" + } + } + + formattedRelativeTime + } else { + stream.textualUploadDate + } + } + + override fun getSpanSize(spanCount: Int, position: Int): Int { + return if (itemVersion == ItemVersion.GRID) 1 else spanCount + } +} diff --git a/app/src/main/res/drawable-night/ic_visibility_off.xml b/app/src/main/res/drawable-night/ic_visibility_off.xml new file mode 100644 index 00000000000..689f3f47c14 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_visibility_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-night/ic_visibility_on.xml b/app/src/main/res/drawable-night/ic_visibility_on.xml new file mode 100644 index 00000000000..e02f1d19120 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_visibility_on.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_visibility_off.xml b/app/src/main/res/drawable/ic_visibility_off.xml new file mode 100644 index 00000000000..e0b170300a0 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_visibility_on.xml b/app/src/main/res/drawable/ic_visibility_on.xml new file mode 100644 index 00000000000..6c95a5d2921 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_on.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/item_in_history_indicator_background.xml b/app/src/main/res/drawable/item_in_history_indicator_background.xml new file mode 100644 index 00000000000..1c3a9a56b3f --- /dev/null +++ b/app/src/main/res/drawable/item_in_history_indicator_background.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/layout/list_stream_grid_item.xml b/app/src/main/res/layout/list_stream_grid_item.xml index 4e3d6edfb3b..1a65b41a1b9 100644 --- a/app/src/main/res/layout/list_stream_grid_item.xml +++ b/app/src/main/res/layout/list_stream_grid_item.xml @@ -20,6 +20,32 @@ android:src="@drawable/dummy_thumbnail" tools:ignore="RtlHardcoded" /> + + + + + + + + + + + + + app:showAsAction="never" /> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1a1b3a9bb44..a35590b6a28 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -56,6 +56,7 @@ #EEFFFFFF #ffffff #64000000 + #E6FFFFFF #323232 #ffffff diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b55ad781cd6..51da9b29916 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -20,6 +20,7 @@ 11sp 12sp 16sp + 12sp 124dp @@ -52,6 +53,8 @@ 40dp 200dp + 2dp + 8dp 180dp 150dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 756e12eafac..f263208006d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -228,6 +228,7 @@ Delete entire search history? Search history deleted. Help + In History Error External storage unavailable @@ -702,7 +703,7 @@ Enable fast mode Disable fast mode Do you think feed loading is too slow? If so, try enabling fast loading (you can change it in settings or by pressing the button below).\n\nNewPipe offers two feed loading strategies:\n• Fetching the whole subscription channel, which is slow but complete.\n• Using a dedicated service endpoint, which is fast but usually not complete.\n\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.\n\nYouTube is an example of a service that offers this fast method with its RSS feed.\n\nSo the choice boils down to what you prefer: speed or precise information. - + Show played items This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version. Channel\'s avatar thumbnail Created by %s From 360f5ac6f79cf07955364a163f16cea06d1ce183 Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 26 Mar 2021 11:27:25 +0100 Subject: [PATCH 2/9] Save playback state even if stream is finished and add isFinished() --- .../newpipe/database/feed/dao/FeedDAO.kt | 4 +++ .../stream/model/StreamStateEntity.java | 35 ++++++++++++++----- .../local/history/HistoryRecordManager.java | 6 ++-- .../org/schabi/newpipe/player/Player.java | 6 +++- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index f3a4a13b5f6..4b3bcdd7ee9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -10,6 +10,7 @@ import io.reactivex.rxjava3.core.Flowable import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipe.database.stream.StreamWithState +import org.schabi.newpipe.database.stream.model.StreamStateEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity import java.time.OffsetDateTime @@ -79,6 +80,9 @@ abstract class FeedDAO { WHERE ( sh.stream_id IS NULL + OR sst.stream_id IS NULL + OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS} + OR sst.progress_time < s.duration * 1000 * 3 / 4 OR s.stream_type = 'LIVE_STREAM' OR s.stream_type = 'AUDIO_LIVE_STREAM' ) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index 1ce834a8249..fd8dc0a42f7 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -5,8 +5,6 @@ import androidx.room.Entity; import androidx.room.ForeignKey; -import java.util.concurrent.TimeUnit; - import static androidx.room.ForeignKey.CASCADE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @@ -30,11 +28,13 @@ public class StreamStateEntity { /** * Playback state will not be saved, if playback time is less than this threshold. */ - private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5; + private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000; // 5000ms = 5s + /** - * Playback state will not be saved, if time left is less than this threshold. + * @see #isFinished(long) + * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams() */ - private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10; + public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000; // 60000ms = 60s @ColumnInfo(name = JOIN_STREAM_ID) private long streamUid; @@ -63,10 +63,27 @@ public void setProgressTime(final long progressTime) { this.progressTime = progressTime; } - public boolean isValid(final int durationInSeconds) { - final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime); - return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS - && seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS; + /** + * The state will be considered valid, and thus be saved, if the progress is more than {@link + * #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS}. + * @return whether this stream state entity should be saved or not + */ + public boolean isValid() { + return progressTime > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS; + } + + /** + * The video will be considered as finished, if the time left is less than {@link + * #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length. + * The state will be saved anyway, so that it can be shown under stream info items, but the + * player will not resume if a state is considered as finished. Finished streams are also the + * ones that can be filtered out in the feed fragment. + * @param durationInSeconds the duration of the stream connected with this state, in seconds + * @return whether the stream is finished or not + */ + public boolean isFinished(final long durationInSeconds) { + return progressTime >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS + && progressTime >= durationInSeconds * 1000 * 3 / 4; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 66f1bda0e4c..a0bd2f479d1 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -215,7 +215,7 @@ public Maybe loadStreamState(final PlayQueueItem queueItem) { .flatMapPublisher(streamStateTable::getState) .firstElement() .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0))) - .filter(state -> state.isValid((int) queueItem.getDuration())) + .filter(StreamStateEntity::isValid) .subscribeOn(Schedulers.io()); } @@ -224,7 +224,7 @@ public Maybe loadStreamState(final StreamInfo info) { .flatMapPublisher(streamStateTable::getState) .firstElement() .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0))) - .filter(state -> state.isValid((int) info.getDuration())) + .filter(StreamStateEntity::isValid) .subscribeOn(Schedulers.io()); } @@ -232,7 +232,7 @@ public Completable saveStreamState(@NonNull final StreamInfo info, final long pr return Completable.fromAction(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); final StreamStateEntity state = new StreamStateEntity(streamId, progressTime); - if (state.isValid((int) info.getDuration())) { + if (state.isValid()) { streamStateTable.upsert(state); } else { streamStateTable.deleteState(streamId); diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 0c5dbbb6fe7..55e2e2dd733 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -671,7 +671,11 @@ && isPlaybackResumeEnabled(this) //.doFinally() .subscribe( state -> { - newQueue.setRecovery(newQueue.getIndex(), state.getProgressTime()); + if (!state.isFinished(newQueue.getItem().getDuration())) { + // resume playback only if the stream was not played to the end + newQueue.setRecovery(newQueue.getIndex(), + state.getProgressTime()); + } initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, playWhenReady, isMuted); }, From e58feadba9e38b54f871568f2fa7f3e3dd3eaca7 Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 26 Mar 2021 11:35:54 +0100 Subject: [PATCH 3/9] Remove IN HISTORY label on stream info items --- .../newpipe/database/feed/dao/FeedDAO.kt | 8 +++--- .../database/stream/StreamWithState.kt | 5 +--- .../newpipe/local/feed/item/StreamItem.kt | 4 --- .../main/res/layout/list_stream_grid_item.xml | 26 ------------------- app/src/main/res/layout/list_stream_item.xml | 26 ------------------- .../main/res/layout/list_stream_mini_item.xml | 26 ------------------- .../layout/list_stream_playlist_grid_item.xml | 26 ------------------- .../res/layout/list_stream_playlist_item.xml | 26 ------------------- 8 files changed, 5 insertions(+), 142 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index 4b3bcdd7ee9..769c728328d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -21,7 +21,7 @@ abstract class FeedDAO { @Query( """ - SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history + SELECT s.*, sst.progress_time FROM streams s LEFT JOIN stream_state sst @@ -41,7 +41,7 @@ abstract class FeedDAO { @Query( """ - SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history + SELECT s.*, sst.progress_time FROM streams s LEFT JOIN stream_state sst @@ -66,7 +66,7 @@ abstract class FeedDAO { @Query( """ - SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history + SELECT s.*, sst.progress_time FROM streams s LEFT JOIN stream_state sst @@ -95,7 +95,7 @@ abstract class FeedDAO { @Query( """ - SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history + SELECT s.*, sst.progress_time FROM streams s LEFT JOIN stream_state sst diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt index 40c7862460e..759dbae29b3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt @@ -10,8 +10,5 @@ data class StreamWithState( val stream: StreamEntity, @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME) - val stateProgressTime: Long?, - - @ColumnInfo(name = "is_stream_in_history") - val isInHistory: Boolean = false + val stateProgressTime: Long? ) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt index 88c5d809d2f..dbd675161dc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt @@ -30,7 +30,6 @@ data class StreamItem( private val stream: StreamEntity = streamWithState.stream private val stateProgressTime: Long? = streamWithState.stateProgressTime - private val isInHistory: Boolean = streamWithState.isInHistory override fun getId(): Long = stream.uid @@ -94,9 +93,6 @@ data class StreamItem( viewBinding.itemProgressView.visibility = View.GONE } - viewBinding.itemInHistoryIndicatorView.visibility = - if (isInHistory && !isLiveStream) View.VISIBLE else View.GONE - ImageLoader.getInstance().displayImage( stream.thumbnailUrl, viewBinding.itemThumbnailView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS diff --git a/app/src/main/res/layout/list_stream_grid_item.xml b/app/src/main/res/layout/list_stream_grid_item.xml index 1a65b41a1b9..4e3d6edfb3b 100644 --- a/app/src/main/res/layout/list_stream_grid_item.xml +++ b/app/src/main/res/layout/list_stream_grid_item.xml @@ -20,32 +20,6 @@ android:src="@drawable/dummy_thumbnail" tools:ignore="RtlHardcoded" /> - - - - - - - - - - Date: Mon, 7 Jun 2021 09:35:40 +0200 Subject: [PATCH 4/9] Correctly save stream progress at the end of a video --- .../history/dao/StreamHistoryDAO.java | 4 +- .../database/playlist/PlaylistStreamEntry.kt | 4 +- .../playlist/dao/PlaylistStreamDAO.java | 4 +- .../database/stream/StreamStatisticsEntry.kt | 6 +- .../database/stream/StreamWithState.kt | 4 +- .../stream/model/StreamStateEntity.java | 26 +++--- .../fragments/detail/VideoDetailFragment.java | 2 +- .../holder/StreamMiniInfoItemHolder.java | 6 +- .../newpipe/local/feed/item/StreamItem.kt | 2 +- .../local/history/HistoryRecordManager.java | 6 +- .../holder/LocalPlaylistStreamItemHolder.java | 10 +-- .../LocalStatisticStreamItemHolder.java | 10 +-- .../org/schabi/newpipe/player/Player.java | 82 ++++++++----------- 13 files changed, 74 insertions(+), 92 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 535f2d2d05d..0a765ed4eec 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -21,7 +21,7 @@ import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao @@ -80,7 +80,7 @@ public Flowable> listByService(final int serviceId) { + " LEFT JOIN " + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " - + STREAM_PROGRESS_TIME + + STREAM_PROGRESS_MILLIS + " FROM " + STREAM_STATE_TABLE + " )" + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS) public abstract Flowable> getStatistics(); diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt index 0d46f20bfb8..81409ecf057 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -12,8 +12,8 @@ data class PlaylistStreamEntry( @Embedded val streamEntity: StreamEntity, - @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0") - val progressTime: Long, + @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS, defaultValue = "0") + val progressMillis: Long, @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) val streamId: Long, diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index b13dbf717e1..f4a0a758085 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -25,7 +25,7 @@ import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao @@ -64,7 +64,7 @@ default Flowable> listByService(final int serviceId) + " LEFT JOIN " + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " - + STREAM_PROGRESS_TIME + + STREAM_PROGRESS_MILLIS + " FROM " + STREAM_STATE_TABLE + " )" + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt index eca12f58400..9a622f6437a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -5,7 +5,7 @@ import androidx.room.Embedded import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.history.model.StreamHistoryEntity import org.schabi.newpipe.database.stream.model.StreamEntity -import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME +import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS import org.schabi.newpipe.extractor.stream.StreamInfoItem import java.time.OffsetDateTime @@ -13,8 +13,8 @@ class StreamStatisticsEntry( @Embedded val streamEntity: StreamEntity, - @ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0") - val progressTime: Long, + @ColumnInfo(name = STREAM_PROGRESS_MILLIS, defaultValue = "0") + val progressMillis: Long, @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) val streamId: Long, diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt index 759dbae29b3..abeabf888c9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamWithState.kt @@ -9,6 +9,6 @@ data class StreamWithState( @Embedded val stream: StreamEntity, - @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME) - val stateProgressTime: Long? + @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS) + val stateProgressMillis: Long? ) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index fd8dc0a42f7..63a21fa9a20 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -23,7 +23,7 @@ public class StreamStateEntity { // This additional field is required for the SQL query because 'stream_id' is used // for some other joins already public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias"; - public static final String STREAM_PROGRESS_TIME = "progress_time"; + public static final String STREAM_PROGRESS_MILLIS = "progress_time"; /** * Playback state will not be saved, if playback time is less than this threshold. @@ -39,12 +39,12 @@ public class StreamStateEntity { @ColumnInfo(name = JOIN_STREAM_ID) private long streamUid; - @ColumnInfo(name = STREAM_PROGRESS_TIME) - private long progressTime; + @ColumnInfo(name = STREAM_PROGRESS_MILLIS) + private long progressMillis; - public StreamStateEntity(final long streamUid, final long progressTime) { + public StreamStateEntity(final long streamUid, final long progressMillis) { this.streamUid = streamUid; - this.progressTime = progressTime; + this.progressMillis = progressMillis; } public long getStreamUid() { @@ -55,12 +55,12 @@ public void setStreamUid(final long streamUid) { this.streamUid = streamUid; } - public long getProgressTime() { - return progressTime; + public long getProgressMillis() { + return progressMillis; } - public void setProgressTime(final long progressTime) { - this.progressTime = progressTime; + public void setProgressMillis(final long progressMillis) { + this.progressMillis = progressMillis; } /** @@ -69,7 +69,7 @@ public void setProgressTime(final long progressTime) { * @return whether this stream state entity should be saved or not */ public boolean isValid() { - return progressTime > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS; + return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS; } /** @@ -82,15 +82,15 @@ public boolean isValid() { * @return whether the stream is finished or not */ public boolean isFinished(final long durationInSeconds) { - return progressTime >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS - && progressTime >= durationInSeconds * 1000 * 3 / 4; + return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS + && progressMillis >= durationInSeconds * 1000 * 3 / 4; } @Override public boolean equals(@Nullable final Object obj) { if (obj instanceof StreamStateEntity) { return ((StreamStateEntity) obj).streamUid == streamUid - && ((StreamStateEntity) obj).progressTime == progressTime; + && ((StreamStateEntity) obj).progressMillis == progressMillis; } else { return false; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 784a1c3be7d..ee9037537e0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1669,7 +1669,7 @@ private void updateProgressInfo(@NonNull final StreamInfo info) { .onErrorComplete() .observeOn(AndroidSchedulers.mainThread()) .subscribe(state -> { - showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000); + showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000); animate(binding.positionView, true, 500); animate(binding.detailPositionView, true, 500); }, e -> { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 227c11f91a5..98699eb95e7 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -66,7 +66,7 @@ public void updateFromItem(final InfoItem infoItem, itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setMax((int) item.getDuration()); itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(state2.getProgressTime())); + .toSeconds(state2.getProgressMillis())); } else { itemProgressView.setVisibility(View.GONE); } @@ -121,10 +121,10 @@ public void updateState(final InfoItem infoItem, itemProgressView.setMax((int) item.getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS - .toSeconds(state.getProgressTime())); + .toSeconds(state.getProgressMillis())); } else { itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(state.getProgressTime())); + .toSeconds(state.getProgressMillis())); ViewUtils.animate(itemProgressView, true, 500); } } else if (itemProgressView.getVisibility() == View.VISIBLE) { diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt index dbd675161dc..13ba7592b1f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt @@ -29,7 +29,7 @@ data class StreamItem( } private val stream: StreamEntity = streamWithState.stream - private val stateProgressTime: Long? = streamWithState.stateProgressTime + private val stateProgressTime: Long? = streamWithState.stateProgressMillis override fun getId(): Long = stream.uid diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index a0bd2f479d1..d2ffbfc277b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -228,14 +228,12 @@ public Maybe loadStreamState(final StreamInfo info) { .subscribeOn(Schedulers.io()); } - public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) { + public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) { return Completable.fromAction(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); - final StreamStateEntity state = new StreamStateEntity(streamId, progressTime); + final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis); if (state.isValid()) { streamStateTable.upsert(state); - } else { - streamStateTable.deleteState(streamId); } })).subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index fd6c8d1d1e5..903f104405e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -68,11 +68,11 @@ public void updateFromItem(final LocalItem localItem, R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - if (item.getProgressTime() > 0) { + if (item.getProgressMillis() > 0) { itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressTime())); + .toSeconds(item.getProgressMillis())); } else { itemProgressView.setVisibility(View.GONE); } @@ -109,14 +109,14 @@ public void updateState(final LocalItem localItem, } final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) { + if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) { itemProgressView.setMax((int) item.getStreamEntity().getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressTime())); + .toSeconds(item.getProgressMillis())); } else { itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressTime())); + .toSeconds(item.getProgressMillis())); ViewUtils.animate(itemProgressView, true, 500); } } else if (itemProgressView.getVisibility() == View.VISIBLE) { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index 7c4e47c3654..adf6bd5c262 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -96,11 +96,11 @@ public void updateFromItem(final LocalItem localItem, R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - if (item.getProgressTime() > 0) { + if (item.getProgressMillis() > 0) { itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressTime())); + .toSeconds(item.getProgressMillis())); } else { itemProgressView.setVisibility(View.GONE); } @@ -140,14 +140,14 @@ public void updateState(final LocalItem localItem, } final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) { + if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) { itemProgressView.setMax((int) item.getStreamEntity().getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressTime())); + .toSeconds(item.getProgressMillis())); } else { itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressTime())); + .toSeconds(item.getProgressMillis())); ViewUtils.animate(itemProgressView, true, 500); } } else if (itemProgressView.getVisibility() == View.VISIBLE) { diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 55e2e2dd733..a6be078beca 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -674,7 +674,7 @@ && isPlaybackResumeEnabled(this) if (!state.isFinished(newQueue.getItem().getDuration())) { // resume playback only if the stream was not played to the end newQueue.setRecovery(newQueue.getIndex(), - state.getProgressTime()); + state.getProgressMillis()); } initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, playWhenReady, isMuted); @@ -1939,9 +1939,7 @@ public void onPlayerStateChanged(final boolean playWhenReady, final int playback break; case com.google.android.exoplayer2.Player.STATE_ENDED: // 4 changeState(STATE_COMPLETED); - if (currentMetadata != null) { - resetStreamProgressState(currentMetadata.getMetadata()); - } + saveStreamProgressStateCompleted(); isPrepared = false; break; } @@ -2402,7 +2400,7 @@ public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuity case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: if (playQueue.getIndex() != newWindowIndex) { - resetStreamProgressState(playQueue.getItem()); + saveStreamProgressStateCompleted(); // current stream has ended playQueue.setIndex(newWindowIndex); } break; @@ -2793,61 +2791,47 @@ private void registerStreamViewed() { } } - private void saveStreamProgressState(final StreamInfo info, final long progress) { - if (info == null) { + private void saveStreamProgressState(final long progressMillis) { + if (currentMetadata == null + || !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { return; } if (DEBUG) { - Log.d(TAG, "saveStreamProgressState() called"); + Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis + + ", currentMetadata=[" + currentMetadata.getMetadata().getName() + "]"); } - if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { - final Disposable stateSaver = recordManager.saveStreamState(info, progress) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError((e) -> { - if (DEBUG) { - e.printStackTrace(); - } - }) - .onErrorComplete() - .subscribe(); - databaseUpdateDisposable.add(stateSaver); - } - } - private void resetStreamProgressState(final PlayQueueItem queueItem) { - if (queueItem == null) { - return; - } - if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { - final Disposable stateSaver = queueItem.getStream() - .flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError((e) -> { - if (DEBUG) { - e.printStackTrace(); - } - }) - .onErrorComplete() - .subscribe(); - databaseUpdateDisposable.add(stateSaver); - } - } - - private void resetStreamProgressState(final StreamInfo info) { - saveStreamProgressState(info, 0); + databaseUpdateDisposable + .add(recordManager.saveStreamState(currentMetadata.getMetadata(), progressMillis) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) { + e.printStackTrace(); + } + }) + .onErrorComplete() + .subscribe()); } public void saveStreamProgressState() { - if (exoPlayerIsNull() || currentMetadata == null) { + if (exoPlayerIsNull() || currentMetadata == null || playQueue == null + || playQueue.getIndex() != simpleExoPlayer.getCurrentWindowIndex()) { + // Make sure play queue and current window index are equal, to prevent saving state for + // the wrong stream on discontinuity (e.g. when the stream just changed but the + // playQueue index and currentMetadata still haven't updated) return; } - final StreamInfo currentInfo = currentMetadata.getMetadata(); - if (playQueue != null) { - // Save current position. It will help to restore this position once a user - // wants to play prev or next stream from the queue - playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition()); + // Save current position. It will help to restore this position once a user + // wants to play prev or next stream from the queue + playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition()); + saveStreamProgressState(simpleExoPlayer.getCurrentPosition()); + } + + public void saveStreamProgressStateCompleted() { + if (currentMetadata != null) { + // current stream has ended, so the progress is its duration (+1 to overcome rounding) + saveStreamProgressState((currentMetadata.getMetadata().getDuration() + 1) * 1000); } - saveStreamProgressState(currentInfo, simpleExoPlayer.getCurrentPosition()); } //endregion From 2142f05a88f2848586debcf1501cb71dd4abc522 Mon Sep 17 00:00:00 2001 From: Stypox Date: Mon, 7 Jun 2021 09:43:08 +0200 Subject: [PATCH 5/9] Fix hiding finished streams in groups; new stream state validity condition Consider stream state valid also if >1/4 of video was watched --- .../newpipe/database/feed/dao/FeedDAO.kt | 16 ++++++++++++ .../stream/model/StreamStateEntity.java | 26 ++++++++++++++----- .../local/history/HistoryRecordManager.java | 8 +++--- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index 769c728328d..689f1ead67d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -64,6 +64,12 @@ abstract class FeedDAO { ) abstract fun getAllStreamsForGroup(groupId: Long): Flowable> + /** + * @see StreamStateEntity.isFinished() + * @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS + * @return all of the non-live, never-played and non-finished streams in the feed + * (all of the cited conditions must hold for a stream to be in the returned list) + */ @Query( """ SELECT s.*, sst.progress_time @@ -93,6 +99,13 @@ abstract class FeedDAO { ) abstract fun getLiveOrNotPlayedStreams(): Flowable> + /** + * @see StreamStateEntity.isFinished() + * @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS + * @param groupId the group id to get streams of + * @return all of the non-live, never-played and non-finished streams for the given feed group + * (all of the cited conditions must hold for a stream to be in the returned list) + */ @Query( """ SELECT s.*, sst.progress_time @@ -113,6 +126,9 @@ abstract class FeedDAO { WHERE fgs.group_id = :groupId AND ( sh.stream_id IS NULL + OR sst.stream_id IS NULL + OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS} + OR sst.progress_time < s.duration * 1000 * 3 / 4 OR s.stream_type = 'LIVE_STREAM' OR s.stream_type = 'AUDIO_LIVE_STREAM' ) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index 63a21fa9a20..75766850ff5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -5,6 +5,8 @@ import androidx.room.Entity; import androidx.room.ForeignKey; +import java.util.Objects; + import static androidx.room.ForeignKey.CASCADE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @@ -26,15 +28,18 @@ public class StreamStateEntity { public static final String STREAM_PROGRESS_MILLIS = "progress_time"; /** - * Playback state will not be saved, if playback time is less than this threshold. + * Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s). */ - private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000; // 5000ms = 5s + private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000; /** + * Stream will be considered finished if the playback time left exceeds this threshold + * (60000ms = 60s). * @see #isFinished(long) * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams() + * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long) */ - public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000; // 60000ms = 60s + public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000; @ColumnInfo(name = JOIN_STREAM_ID) private long streamUid; @@ -65,11 +70,13 @@ public void setProgressMillis(final long progressMillis) { /** * The state will be considered valid, and thus be saved, if the progress is more than {@link - * #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS}. + * #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length. + * @param durationInSeconds the duration of the stream connected with this state, in seconds * @return whether this stream state entity should be saved or not */ - public boolean isValid() { - return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS; + public boolean isValid(final long durationInSeconds) { + return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS + || progressMillis > durationInSeconds * 1000 / 4; } /** @@ -78,6 +85,8 @@ public boolean isValid() { * The state will be saved anyway, so that it can be shown under stream info items, but the * player will not resume if a state is considered as finished. Finished streams are also the * ones that can be filtered out in the feed fragment. + * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams() + * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long) * @param durationInSeconds the duration of the stream connected with this state, in seconds * @return whether the stream is finished or not */ @@ -95,4 +104,9 @@ public boolean equals(@Nullable final Object obj) { return false; } } + + @Override + public int hashCode() { + return Objects.hash(streamUid, progressMillis); + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index d2ffbfc277b..38ebe504ec1 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -211,11 +211,11 @@ public Maybe getStreamHistory(final StreamInfo info) { public Maybe loadStreamState(final PlayQueueItem queueItem) { return queueItem.getStream() - .map((info) -> streamTable.upsert(new StreamEntity(info))) + .map(info -> streamTable.upsert(new StreamEntity(info))) .flatMapPublisher(streamStateTable::getState) .firstElement() .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0))) - .filter(StreamStateEntity::isValid) + .filter(state -> state.isValid(queueItem.getDuration())) .subscribeOn(Schedulers.io()); } @@ -224,7 +224,7 @@ public Maybe loadStreamState(final StreamInfo info) { .flatMapPublisher(streamStateTable::getState) .firstElement() .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0))) - .filter(StreamStateEntity::isValid) + .filter(state -> state.isValid(info.getDuration())) .subscribeOn(Schedulers.io()); } @@ -232,7 +232,7 @@ public Completable saveStreamState(@NonNull final StreamInfo info, final long pr return Completable.fromAction(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis); - if (state.isValid()) { + if (state.isValid(info.getDuration())) { streamStateTable.upsert(state); } })).subscribeOn(Schedulers.io()); From 4698d07323c2dc0e1c2b4cbbafafa4783548f07f Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 9 Jun 2021 16:14:03 +0200 Subject: [PATCH 6/9] Do not hide feed buttons (show/hide & help) behind three-dots menu --- .../java/org/schabi/newpipe/fragments/MainFragment.java | 2 +- .../java/org/schabi/newpipe/local/feed/FeedFragment.kt | 8 +------- app/src/main/res/menu/menu_feed_fragment.xml | 4 +++- .../{main_fragment_menu.xml => menu_main_fragment.xml} | 0 4 files changed, 5 insertions(+), 9 deletions(-) rename app/src/main/res/menu/{main_fragment_menu.xml => menu_main_fragment.xml} (100%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 88d7e757e1b..7e0186e1cc3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -130,7 +130,7 @@ public void onCreateOptionsMenu(@NonNull final Menu menu, Log.d(TAG, "onCreateOptionsMenu() called with: " + "menu = [" + menu + "], inflater = [" + inflater + "]"); } - inflater.inflate(R.menu.main_fragment_menu, menu); + inflater.inflate(R.menu.menu_main_fragment, menu); final ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index d6d75fec5d9..4665ebb7f65 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -169,13 +169,7 @@ class FeedFragment : BaseStateFragment() { activity.supportActionBar?.subtitle = groupName inflater.inflate(R.menu.menu_feed_fragment, menu) - - menu.findItem(R.id.menu_item_feed_toggle_played_items).apply { - updateTogglePlayedItemsButton(this) - if (useAsFrontPage) { - setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) - } - } + updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items)) } override fun onOptionsItemSelected(item: MenuItem): Boolean { diff --git a/app/src/main/res/menu/menu_feed_fragment.xml b/app/src/main/res/menu/menu_feed_fragment.xml index cd6e1696c23..7a948ea8abb 100644 --- a/app/src/main/res/menu/menu_feed_fragment.xml +++ b/app/src/main/res/menu/menu_feed_fragment.xml @@ -4,6 +4,7 @@ + android:orderInCategory="3" + app:showAsAction="ifRoom" /> diff --git a/app/src/main/res/menu/main_fragment_menu.xml b/app/src/main/res/menu/menu_main_fragment.xml similarity index 100% rename from app/src/main/res/menu/main_fragment_menu.xml rename to app/src/main/res/menu/menu_main_fragment.xml From 7145b117cccbf286de8006b4559e4fdd5dd1599b Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 9 Jun 2021 16:15:04 +0200 Subject: [PATCH 7/9] Fix long press menu in feed --- .../java/org/schabi/newpipe/local/feed/FeedFragment.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index 4665ebb7f65..bafe8b0f2fd 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -309,7 +309,8 @@ class FeedFragment : BaseStateFragment() { listOf( StreamDialogEntry.start_here_on_background, StreamDialogEntry.append_playlist, - StreamDialogEntry.share + StreamDialogEntry.share, + StreamDialogEntry.open_in_browser ) ) } else { @@ -318,11 +319,13 @@ class FeedFragment : BaseStateFragment() { StreamDialogEntry.start_here_on_background, StreamDialogEntry.start_here_on_popup, StreamDialogEntry.append_playlist, - StreamDialogEntry.share + StreamDialogEntry.share, + StreamDialogEntry.open_in_browser ) ) } + StreamDialogEntry.setEnabledEntries(entries) InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which -> StreamDialogEntry.clickOn(which, this, item) }.show() @@ -400,7 +403,7 @@ class FeedFragment : BaseStateFragment() { } private fun handleItemsErrors(errors: List) { - errors.forEachIndexed() { i, t -> + errors.forEachIndexed { i, t -> if (t is FeedLoadService.RequestException && t.cause is ContentNotAvailableException ) { From fdb6679d2d8eada1a28cb2ab5c6547b15e39538a Mon Sep 17 00:00:00 2001 From: Stypox Date: Mon, 14 Jun 2021 19:21:42 +0200 Subject: [PATCH 8/9] Make list stream item ConstraintLayouts and use chain --- .../main/res/layout/list_stream_grid_item.xml | 62 ++++++++++--------- app/src/main/res/layout/list_stream_item.xml | 34 ++++------ 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/app/src/main/res/layout/list_stream_grid_item.xml b/app/src/main/res/layout/list_stream_grid_item.xml index 4e3d6edfb3b..5bf6bed68f8 100644 --- a/app/src/main/res/layout/list_stream_grid_item.xml +++ b/app/src/main/res/layout/list_stream_grid_item.xml @@ -1,5 +1,6 @@ - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintBottom_toTopOf="@+id/itemUploaderView" + app:layout_constraintEnd_toEndOf="@+id/itemThumbnailView" + app:layout_constraintStart_toStartOf="@+id/itemThumbnailView" + app:layout_constraintTop_toBottomOf="@+id/itemProgressView" + tools:text="@tools:sample/lorem[10]" /> + app:layout_constraintBottom_toTopOf="@+id/itemAdditionalDetails" + app:layout_constraintEnd_toEndOf="@+id/itemThumbnailView" + app:layout_constraintStart_toStartOf="@+id/itemThumbnailView" + app:layout_constraintTop_toBottomOf="@+id/itemVideoTitleView" + tools:text="Uploader name long very very long long" /> + android:progressDrawable="?progress_horizontal_drawable" + app:layout_constraintEnd_toEndOf="@+id/itemThumbnailView" + app:layout_constraintStart_toStartOf="@+id/itemThumbnailView" + app:layout_constraintTop_toBottomOf="@+id/itemThumbnailView" /> - + diff --git a/app/src/main/res/layout/list_stream_item.xml b/app/src/main/res/layout/list_stream_item.xml index f30337fec9f..f1936e70467 100644 --- a/app/src/main/res/layout/list_stream_item.xml +++ b/app/src/main/res/layout/list_stream_item.xml @@ -14,47 +14,41 @@ android:id="@+id/itemThumbnailView" android:layout_width="@dimen/video_item_search_thumbnail_image_width" android:layout_height="@dimen/video_item_search_thumbnail_image_height" - android:layout_marginRight="@dimen/video_item_search_image_right_margin" android:contentDescription="@string/list_thumbnail_view_description" android:scaleType="centerCrop" android:src="@drawable/dummy_thumbnail" - app:layout_constraintEnd_toStartOf="@+id/itemVideoTitleView" - app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintBottom_toTopOf="@+id/itemProgressView" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - tools:ignore="RtlHardcoded" /> + app:layout_constraintTop_toTopOf="parent" /> @@ -67,8 +61,9 @@ android:lines="1" android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_uploader_text_size" - app:layout_constraintLeft_toLeftOf="@+id/itemVideoTitleView" - app:layout_constraintRight_toRightOf="parent" + app:layout_constraintBottom_toTopOf="@+id/itemAdditionalDetails" + app:layout_constraintEnd_toEndOf="@+id/itemVideoTitleView" + app:layout_constraintStart_toStartOf="@+id/itemVideoTitleView" app:layout_constraintTop_toBottomOf="@+id/itemVideoTitleView" tools:text="Uploader" /> @@ -76,15 +71,13 @@ android:id="@+id/itemAdditionalDetails" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_toEndOf="@+id/itemThumbnailView" - android:layout_toRightOf="@+id/itemThumbnailView" android:ellipsize="end" android:lines="1" android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_upload_date_text_size" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintLeft_toLeftOf="@+id/itemVideoTitleView" - app:layout_constraintRight_toRightOf="parent" + app:layout_constraintEnd_toEndOf="@+id/itemVideoTitleView" + app:layout_constraintStart_toStartOf="@+id/itemVideoTitleView" app:layout_constraintTop_toBottomOf="@+id/itemUploaderView" tools:text="2 years ago • 10M views" /> @@ -93,9 +86,8 @@ style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="0dp" android:layout_height="4dp" - android:layout_below="@id/itemThumbnailView" - android:layout_marginTop="-2dp" android:progressDrawable="?progress_horizontal_drawable" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/itemThumbnailView" app:layout_constraintStart_toStartOf="@+id/itemThumbnailView" app:layout_constraintTop_toBottomOf="@+id/itemThumbnailView" /> From 32df4d39a4b37603d891936817dd6ea0d0df61c5 Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 15 Jun 2021 18:40:25 +0200 Subject: [PATCH 9/9] Reshow feed if grid/list view mode changed --- .../schabi/newpipe/local/feed/FeedFragment.kt | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index bafe8b0f2fd..4c1bb073242 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -22,6 +22,7 @@ package org.schabi.newpipe.local.feed import android.annotation.SuppressLint import android.app.Activity import android.content.Intent +import android.content.SharedPreferences import android.content.res.Configuration import android.os.Bundle import android.os.Parcelable @@ -94,6 +95,9 @@ class FeedFragment : BaseStateFragment() { private lateinit var groupAdapter: GroupAdapter @State @JvmField var showPlayedItems: Boolean = true + private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null + private var updateListViewModeOnResume = false + init { setHasOptionsMenu(true) } @@ -104,6 +108,14 @@ class FeedFragment : BaseStateFragment() { groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) ?: FeedGroupEntity.GROUP_ALL_ID groupName = arguments?.getString(KEY_GROUP_NAME) ?: "" + + onSettingsChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key.equals(getString(R.string.list_view_mode_key))) { + updateListViewModeOnResume = true + } + } + PreferenceManager.getDefaultSharedPreferences(activity) + .registerOnSharedPreferenceChangeListener(onSettingsChangeListener) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -122,15 +134,10 @@ class FeedFragment : BaseStateFragment() { groupAdapter = GroupAdapter().apply { setOnItemClickListener(listenerStreamItem) setOnItemLongClickListener(listenerStreamItem) - spanCount = if (shouldUseGridLayout()) getGridSpanCount() else 1 } - feedBinding.itemsList.apply { - layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply { - spanSizeLookup = groupAdapter.spanSizeLookup - } - adapter = groupAdapter - } + feedBinding.itemsList.adapter = groupAdapter + setupListViewMode() } override fun onPause() { @@ -141,6 +148,23 @@ class FeedFragment : BaseStateFragment() { override fun onResume() { super.onResume() updateRelativeTimeViews() + + if (updateListViewModeOnResume) { + updateListViewModeOnResume = false + + setupListViewMode() + if (viewModel.stateLiveData.value != null) { + handleResult(viewModel.stateLiveData.value!!) + } + } + } + + fun setupListViewMode() { + // does everything needed to setup the layouts for grid or list modes + groupAdapter.spanCount = if (shouldUseGridLayout()) getGridSpanCount() else 1 + feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply { + spanSizeLookup = groupAdapter.spanSizeLookup + } } override fun setUserVisibleHint(isVisibleToUser: Boolean) { @@ -210,6 +234,12 @@ class FeedFragment : BaseStateFragment() { override fun onDestroy() { disposables.dispose() + if (onSettingsChangeListener != null) { + PreferenceManager.getDefaultSharedPreferences(activity) + .unregisterOnSharedPreferenceChangeListener(onSettingsChangeListener) + onSettingsChangeListener = null + } + super.onDestroy() activity?.supportActionBar?.subtitle = null }