diff --git a/app/build.gradle b/app/build.gradle
index b8daca97e..a0160de33 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -106,7 +106,7 @@ ext {
androidxWorkVersion = '2.5.0'
icepickVersion = '3.2.0'
- exoPlayerVersion = '2.12.3'
+ exoPlayerVersion = '2.14.2'
googleAutoServiceVersion = '1.0'
groupieVersion = '2.10.0'
markwonVersion = '4.6.2'
@@ -190,7 +190,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
- implementation 'com.github.TeamNewPipe:NewPipeExtractor:4f60225ddc'
+ implementation 'com.github.TeamNewPipe:NewPipeExtractor:7e7b78f1b3'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6a2700596..cc631af7a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -340,8 +340,12 @@
+
+
+
+
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 689f1ead6..72692a9f5 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
@@ -7,6 +7,7 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState
@@ -37,7 +38,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
- abstract fun getAllStreams(): Flowable>
+ abstract fun getAllStreams(): Maybe>
@Query(
"""
@@ -62,7 +63,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
- abstract fun getAllStreamsForGroup(groupId: Long): Flowable>
+ abstract fun getAllStreamsForGroup(groupId: Long): Maybe>
/**
* @see StreamStateEntity.isFinished()
@@ -97,7 +98,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
- abstract fun getLiveOrNotPlayedStreams(): Flowable>
+ abstract fun getLiveOrNotPlayedStreams(): Maybe>
/**
* @see StreamStateEntity.isFinished()
@@ -137,7 +138,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
- abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable>
+ abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe>
@Query(
"""
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
index 037eb8f94..b9065c969 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
@@ -378,6 +378,13 @@ public abstract class BaseListFragment extends BaseStateFragment
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
+
+ // show "mark as watched" only when watch history is enabled
+ if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
+ entries.add(
+ StreamDialogEntry.mark_as_watched
+ );
+ }
if (!isNullOrEmpty(item.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
index f3aa2e306..a8763af73 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
@@ -176,6 +176,12 @@ public class PlaylistFragment extends BaseListInfoFragment {
entries.add(StreamDialogEntry.play_with_kodi);
}
+ // show "mark as watched" only when watch history is enabled
+ if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
+ entries.add(
+ StreamDialogEntry.mark_as_watched
+ );
+ }
if (!isNullOrEmpty(item.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
diff --git a/app/src/main/java/org/schabi/newpipe/ktx/View.kt b/app/src/main/java/org/schabi/newpipe/ktx/View.kt
index 8f2249493..a1a96b20d 100644
--- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt
+++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt
@@ -299,18 +299,36 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
}
}
-fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float) {
+fun View.slideUp(
+ duration: Long,
+ delay: Long,
+ @FloatRange(from = 0.0, to = 1.0) translationPercent: Float
+) {
+ slideUp(duration, delay, translationPercent, null)
+}
+
+fun View.slideUp(
+ duration: Long,
+ delay: Long = 0L,
+ @FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F,
+ execOnEnd: Runnable? = null
+) {
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
animate().setListener(null).cancel()
alpha = 0f
translationY = newTranslationY.toFloat()
- visibility = View.VISIBLE
+ isVisible = true
animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
+ .setListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ execOnEnd?.run()
+ }
+ })
.start()
}
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 996293225..2acf002c0 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
@@ -42,7 +42,7 @@ class FeedDatabaseManager(context: Context) {
fun getStreams(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
getPlayedStreams: Boolean = true
- ): Flowable> {
+ ): Maybe> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> {
if (getPlayedStreams) feedTable.getAllStreams()
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 965075bf3..01ff0b1c1 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
@@ -21,8 +21,12 @@ package org.schabi.newpipe.local.feed
import android.annotation.SuppressLint
import android.app.Activity
+import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@@ -31,6 +35,8 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import android.widget.Button
+import androidx.annotation.AttrRes
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
@@ -40,8 +46,10 @@ import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Item
+import com.xwray.groupie.OnAsyncUpdateListener
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
@@ -65,10 +73,12 @@ 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.ktx.slideUp
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.DeviceUtils
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.StreamDialogEntry
@@ -76,6 +86,7 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.ArrayList
+import java.util.function.Consumer
class FeedFragment : BaseStateFragment() {
private var _feedBinding: FragmentFeedBinding? = null
@@ -97,6 +108,8 @@ class FeedFragment : BaseStateFragment() {
private var updateListViewModeOnResume = false
private var isRefreshing = false
+ private var lastNewItemsCount = 0
+
init {
setHasOptionsMenu(true)
}
@@ -126,8 +139,9 @@ class FeedFragment : BaseStateFragment() {
_feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState)
- val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems)
+ val factory = FeedViewModel.Factory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
+ showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
groupAdapter = GroupieAdapter().apply {
@@ -135,6 +149,20 @@ class FeedFragment : BaseStateFragment() {
setOnItemLongClickListener(listenerStreamItem)
}
+ feedBinding.itemsList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ // Check if we scrolled to the top
+ if (newState == RecyclerView.SCROLL_STATE_IDLE &&
+ !recyclerView.canScrollVertically(-1)
+ ) {
+
+ if (tryGetNewItemsLoadedButton()?.isVisible == true) {
+ hideNewItemsLoaded(true)
+ }
+ }
+ }
+ })
+
feedBinding.itemsList.adapter = groupAdapter
setupListViewMode()
}
@@ -158,7 +186,7 @@ class FeedFragment : BaseStateFragment() {
}
}
- fun setupListViewMode() {
+ private fun setupListViewMode() {
// does everything needed to setup the layouts for grid or list modes
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
@@ -170,6 +198,10 @@ class FeedFragment : BaseStateFragment() {
super.initListeners()
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
+ feedBinding.newItemsLoadedButton.setOnClickListener {
+ hideNewItemsLoaded(true)
+ feedBinding.itemsList.scrollToPosition(0)
+ }
}
// /////////////////////////////////////////////////////////////////////////
@@ -213,6 +245,7 @@ class FeedFragment : BaseStateFragment() {
showPlayedItems = !item.isChecked
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems)
+ viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
}
return super.onOptionsItemSelected(item)
@@ -236,6 +269,9 @@ class FeedFragment : BaseStateFragment() {
}
override fun onDestroyView() {
+ // Ensure that all animations are canceled
+ feedBinding.newItemsLoadedButton?.clearAnimation()
+
feedBinding.itemsList.adapter = null
_feedBinding = null
super.onDestroyView()
@@ -355,13 +391,7 @@ class FeedFragment : BaseStateFragment() {
}
// show "mark as watched" only when watch history is enabled
- val isWatchHistoryEnabled = PreferenceManager
- .getDefaultSharedPreferences(context)
- .getBoolean(getString(R.string.enable_watch_history_key), false)
- if (item.streamType != StreamType.AUDIO_LIVE_STREAM &&
- item.streamType != StreamType.LIVE_STREAM &&
- isWatchHistoryEnabled
- ) {
+ if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) {
entries.add(
StreamDialogEntry.mark_as_watched
)
@@ -404,7 +434,17 @@ class FeedFragment : BaseStateFragment() {
}
loadedState.items.forEach { it.itemVersion = itemVersion }
- groupAdapter.updateAsync(loadedState.items, false, null)
+ // This need to be saved in a variable as the update occurs async
+ val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
+
+ groupAdapter.updateAsync(
+ loadedState.items, false,
+ OnAsyncUpdateListener {
+ oldOldestSubscriptionUpdate?.run {
+ highlightNewItemsAfter(oldOldestSubscriptionUpdate)
+ }
+ }
+ )
listState?.run {
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
@@ -526,6 +566,125 @@ class FeedFragment : BaseStateFragment() {
)
}
+ /**
+ * Highlights all items that are after the specified time
+ */
+ private fun highlightNewItemsAfter(updateTime: OffsetDateTime) {
+ var highlightCount = 0
+
+ var doCheck = true
+
+ for (i in 0 until groupAdapter.itemCount) {
+ val item = groupAdapter.getItem(i) as StreamItem
+
+ var typeface = Typeface.DEFAULT
+ var backgroundSupplier = { ctx: Context ->
+ resolveDrawable(ctx, R.attr.selectableItemBackground)
+ }
+ if (doCheck) {
+ // If the uploadDate is null or true we should highlight the item
+ if (item.streamWithState.stream.uploadDate?.isAfter(updateTime) != false) {
+ highlightCount++
+
+ typeface = Typeface.DEFAULT_BOLD
+ backgroundSupplier = { ctx: Context ->
+ // Merge the drawables together. Otherwise we would lose the "select" effect
+ LayerDrawable(
+ arrayOf(
+ resolveDrawable(ctx, R.attr.dashed_border),
+ resolveDrawable(ctx, R.attr.selectableItemBackground)
+ )
+ )
+ }
+ } else {
+ // Decreases execution time due to the order of the items (newest always on top)
+ // Once a item is is before the updateTime we can skip all following items
+ doCheck = false
+ }
+ }
+
+ // The highlighter has to be always set
+ // When it's only set on items that are highlighted it will highlight all items
+ // due to the fact that itemRoot is getting recycled
+ item.execBindEnd = Consumer { viewBinding ->
+ val context = viewBinding.itemRoot.context
+ viewBinding.itemRoot.background = backgroundSupplier.invoke(context)
+ viewBinding.itemVideoTitleView.typeface = typeface
+ }
+ }
+
+ // Force updates all items so that the highlighting is correct
+ // If this isn't done visible items that are already highlighted will stay in a highlighted
+ // state until the user scrolls them out of the visible area which causes a update/bind-call
+ groupAdapter.notifyItemRangeChanged(
+ 0,
+ minOf(groupAdapter.itemCount, maxOf(highlightCount, lastNewItemsCount))
+ )
+
+ if (highlightCount > 0) {
+ showNewItemsLoaded()
+ }
+
+ lastNewItemsCount = highlightCount
+ }
+
+ private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
+ return androidx.core.content.ContextCompat.getDrawable(
+ context,
+ android.util.TypedValue().apply {
+ context.theme.resolveAttribute(
+ attrResId,
+ this,
+ true
+ )
+ }.resourceId
+ )
+ }
+
+ private fun showNewItemsLoaded() {
+ tryGetNewItemsLoadedButton()?.clearAnimation()
+ tryGetNewItemsLoadedButton()
+ ?.slideUp(
+ 250L,
+ delay = 100,
+ execOnEnd = {
+ // Disabled animations would result in immediately hiding the button
+ // after it showed up
+ if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
+ // Hide the new items-"popup" after 10s
+ hideNewItemsLoaded(true, 10000)
+ }
+ }
+ )
+ }
+
+ private fun hideNewItemsLoaded(animate: Boolean, delay: Long = 0) {
+ tryGetNewItemsLoadedButton()?.clearAnimation()
+ if (animate) {
+ tryGetNewItemsLoadedButton()?.animate(
+ false,
+ 200,
+ delay = delay,
+ execOnEnd = {
+ // Make the layout invisible so that the onScroll toTop method
+ // only does necessary work
+ tryGetNewItemsLoadedButton()?.isVisible = false
+ }
+ )
+ } else {
+ tryGetNewItemsLoadedButton()?.isVisible = false
+ }
+ }
+
+ /**
+ * The view/button can be disposed/set to null under certain circumstances.
+ * E.g. when the animation is still in progress but the view got destroyed.
+ * This method is a helper for such states and can be used in affected code blocks.
+ */
+ private fun tryGetNewItemsLoadedButton(): Button? {
+ return _feedBinding?.newItemsLoadedButton
+ }
+
// /////////////////////////////////////////////////////////////////////////
// Load Service Handling
// /////////////////////////////////////////////////////////////////////////
@@ -533,6 +692,8 @@ class FeedFragment : BaseStateFragment() {
override fun doInitialLoadLogic() {}
override fun reloadContent() {
+ hideNewItemsLoaded(false)
+
getActivity()?.startService(
Intent(requireContext(), FeedLoadService::class.java).apply {
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
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 8bdf412b5..2cbf9ad05 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
@@ -1,15 +1,18 @@
package org.schabi.newpipe.local.feed
import android.content.Context
+import androidx.core.content.edit
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.preference.PreferenceManager
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.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.local.feed.item.StreamItem
@@ -23,19 +26,16 @@ import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel(
- applicationContext: Context,
+ private val 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
+ private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
.startWithItem(initialShowPlayedItems)
.distinctUntilChanged()
- .switchMap { showPlayedItems ->
- feedDatabaseManager.getStreams(groupId, showPlayedItems)
- }
private val mutableStateLiveData = MutableLiveData()
val stateLiveData: LiveData = mutableStateLiveData
@@ -43,17 +43,28 @@ class FeedViewModel(
private var combineDisposable = Flowable
.combineLatest(
FeedEventManager.events(),
- streamItems,
+ toggleShowPlayedItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
- Function4 { t1: FeedEventManager.Event, t2: List,
+ Function4 { t1: FeedEventManager.Event, t2: Boolean,
t3: Long, t4: List ->
- return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
+ return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
}
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
+ .observeOn(Schedulers.io())
+ .map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
+ var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
+ feedDatabaseManager
+ .getStreams(groupId, showPlayedItems)
+ .blockingGet(arrayListOf())
+ else
+ arrayListOf()
+
+ CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
+ }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
mutableStateLiveData.postValue(
@@ -75,20 +86,50 @@ class FeedViewModel(
combineDisposable.dispose()
}
- private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List, val t3: Long, val t4: OffsetDateTime?)
+ private data class CombineResultEventHolder(
+ val t1: FeedEventManager.Event,
+ val t2: Boolean,
+ val t3: Long,
+ val t4: OffsetDateTime?
+ )
+
+ private data class CombineResultDataHolder(
+ val t1: FeedEventManager.Event,
+ val t2: List,
+ val t3: Long,
+ val t4: OffsetDateTime?
+ )
fun togglePlayedItems(showPlayedItems: Boolean) {
toggleShowPlayedItems.onNext(showPlayedItems)
}
+ fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
+ PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
+ this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
+ this.apply()
+ }
+
+ fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
+
+ companion object {
+ private fun getShowPlayedItemsFromPreferences(context: Context) =
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(context.getString(R.string.feed_show_played_items_key), true)
+ }
+
class Factory(
private val context: Context,
- private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
- private val showPlayedItems: Boolean
+ private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun create(modelClass: Class): T {
- return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
+ return FeedViewModel(
+ context.applicationContext,
+ groupId,
+ // Read initial value from preferences
+ getShowPlayedItemsFromPreferences(context.applicationContext)
+ ) 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
index 0d2caf126..217e3f3e3 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
@@ -19,6 +19,7 @@ import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper
import org.schabi.newpipe.util.StreamTypeUtil
import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
data class StreamItem(
val streamWithState: StreamWithState,
@@ -31,6 +32,12 @@ data class StreamItem(
private val stream: StreamEntity = streamWithState.stream
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
+ /**
+ * Will be executed at the end of the [StreamItem.bind] (with (ListStreamItemBinding,Int)).
+ * Can be used e.g. for highlighting a item.
+ */
+ var execBindEnd: Consumer? = null
+
override fun getId(): Long = stream.uid
enum class ItemVersion { NORMAL, MINI, GRID }
@@ -97,6 +104,8 @@ data class StreamItem(
viewBinding.itemAdditionalDetails.text =
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
}
+
+ execBindEnd?.accept(viewBinding)
}
override fun isLongClickable() = when (stream.streamType) {
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 d8c8440e2..d94088cd0 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
@@ -120,19 +120,11 @@ public class HistoryRecordManager {
}
// Update the stream progress to the full duration of the video
- final List states = streamStateTable.getState(streamId)
- .blockingFirst();
- if (!states.isEmpty()) {
- final StreamStateEntity entity = states.get(0);
- entity.setProgressMillis(duration * 1000);
- streamStateTable.update(entity);
- } else {
- final StreamStateEntity entity = new StreamStateEntity(
- streamId,
- duration * 1000
- );
- streamStateTable.insert(entity);
- }
+ final StreamStateEntity entity = new StreamStateEntity(
+ streamId,
+ duration * 1000
+ );
+ streamStateTable.upsert(entity);
// Add a history entry
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
index 376f18bc0..43a5fcf3c 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
@@ -366,6 +366,16 @@ public class StatisticsPlaylistFragment
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
+
+ // show "mark as watched" only when watch history is enabled
+ if (StreamDialogEntry.shouldAddMarkAsWatched(
+ item.getStreamEntity().getStreamType(),
+ context
+ )) {
+ entries.add(
+ StreamDialogEntry.mark_as_watched
+ );
+ }
entries.add(StreamDialogEntry.show_channel_details);
StreamDialogEntry.setEnabledEntries(entries);
diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
index 3965cbbd8..87d913b3b 100644
--- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
@@ -782,6 +782,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment 0 && windowPos <= simpleExoPlayer.getDuration()) {
- setRecovery(queuePos, windowPos);
- }
+ // No checks due to https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380
+ setRecovery(queuePos, Math.max(0, Math.min(windowPos, duration)));
}
private void setRecovery(final int queuePos, final long windowPos) {
@@ -896,7 +895,7 @@ public final class Player implements
public void smoothStopPlayer() {
// Pausing would make transition from one stream to a new stream not smooth, so only stop
- simpleExoPlayer.stop(false);
+ simpleExoPlayer.stop();
}
//endregion
@@ -2435,7 +2434,9 @@ public final class Player implements
}
@Override
- public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuityReason) {
+ public void onPositionDiscontinuity(
+ final PositionInfo oldPosition, final PositionInfo newPosition,
+ @DiscontinuityReason final int discontinuityReason) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
+ "discontinuityReason = [" + discontinuityReason + "]");
@@ -2447,7 +2448,8 @@ public final class Player implements
// Refresh the playback if there is a transition to the next video
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
switch (discontinuityReason) {
- case DISCONTINUITY_REASON_PERIOD_TRANSITION:
+ case DISCONTINUITY_REASON_AUTO_TRANSITION:
+ case DISCONTINUITY_REASON_REMOVE:
// When player is in single repeat mode and a period transition occurs,
// we need to register a view count here since no metadata has changed
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) {
@@ -2468,7 +2470,7 @@ public final class Player implements
playQueue.setIndex(newWindowIndex);
}
break;
- case DISCONTINUITY_REASON_AD_INSERTION:
+ case DISCONTINUITY_REASON_SKIP:
break; // only makes Android Studio linter happy, as there are no ads
}
@@ -2480,6 +2482,11 @@ public final class Player implements
//TODO check if this causes black screen when switching to fullscreen
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
}
+
+ @Override
+ public void onCues(final List cues) {
+ binding.subtitleView.onCues(cues);
+ }
//endregion
@@ -2501,7 +2508,7 @@ public final class Player implements
*
*
* @see #processSourceError(IOException)
- * @see com.google.android.exoplayer2.Player.EventListener#onPlayerError(ExoPlaybackException)
+ * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException)
*/
@Override
public void onPlayerError(@NonNull final ExoPlaybackException error) {
@@ -3865,19 +3872,17 @@ public final class Player implements
}
@Override // exoplayer listener
- public void onVideoSizeChanged(final int width, final int height,
- final int unappliedRotationDegrees,
- final float pixelWidthHeightRatio) {
+ public void onVideoSizeChanged(final VideoSize videoSize) {
if (DEBUG) {
Log.d(TAG, "onVideoSizeChanged() called with: "
- + "width / height = [" + width + " / " + height
- + " = " + (((float) width) / height) + "], "
- + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], "
- + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
+ + "width / height = [" + videoSize.width + " / " + videoSize.height
+ + " = " + (((float) videoSize.width) / videoSize.height) + "], "
+ + "unappliedRotationDegrees = [" + videoSize.unappliedRotationDegrees + "], "
+ + "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]");
}
- binding.surfaceView.setAspectRatio(((float) width) / height);
- isVerticalVideo = width < height;
+ binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
+ isVerticalVideo = videoSize.width < videoSize.height;
if (globalScreenOrientationLocked(context)
&& isFullscreen
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
index 2e2fda86c..b36f9f234 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
@@ -16,7 +16,6 @@ import androidx.media.AudioManagerCompat;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
-import com.google.android.exoplayer2.decoder.DecoderCounters;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
@@ -150,15 +149,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
//////////////////////////////////////////////////////////////////////////*/
@Override
- public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
+ public void onAudioSessionIdChanged(final EventTime eventTime, final int audioSessionId) {
notifyAudioSessionUpdate(true, audioSessionId);
}
-
- @Override
- public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) {
- notifyAudioSessionUpdate(false, player.getAudioSessionId());
- }
-
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
if (!PlayerHelper.isUsingDSP()) {
return;
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
index 71cfcc818..ec0e4e4a7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
@@ -1,81 +1,28 @@
package org.schabi.newpipe.player.helper;
import com.google.android.exoplayer2.DefaultLoadControl;
-import com.google.android.exoplayer2.LoadControl;
-import com.google.android.exoplayer2.Renderer;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.upstream.Allocator;
-public class LoadController implements LoadControl {
+public class LoadController extends DefaultLoadControl {
public static final String TAG = "LoadController";
-
- private final long initialPlaybackBufferUs;
- private final LoadControl internalLoadControl;
private boolean preloadingEnabled = true;
- /*//////////////////////////////////////////////////////////////////////////
- // Default Load Control
- //////////////////////////////////////////////////////////////////////////*/
-
- public LoadController() {
- this(PlayerHelper.getPlaybackStartBufferMs());
- }
-
- private LoadController(final int initialPlaybackBufferMs) {
- this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
-
- final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
- builder.setBufferDurationsMs(
- DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
- DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
- initialPlaybackBufferMs,
- DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
- internalLoadControl = builder.build();
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Custom behaviours
- //////////////////////////////////////////////////////////////////////////*/
-
@Override
public void onPrepared() {
preloadingEnabled = true;
- internalLoadControl.onPrepared();
- }
-
- @Override
- public void onTracksSelected(final Renderer[] renderers, final TrackGroupArray trackGroups,
- final TrackSelectionArray trackSelections) {
- internalLoadControl.onTracksSelected(renderers, trackGroups, trackSelections);
+ super.onPrepared();
}
@Override
public void onStopped() {
preloadingEnabled = true;
- internalLoadControl.onStopped();
+ super.onStopped();
}
@Override
public void onReleased() {
preloadingEnabled = true;
- internalLoadControl.onReleased();
- }
-
- @Override
- public Allocator getAllocator() {
- return internalLoadControl.getAllocator();
- }
-
- @Override
- public long getBackBufferDurationUs() {
- return internalLoadControl.getBackBufferDurationUs();
- }
-
- @Override
- public boolean retainBackBufferFromKeyframe() {
- return internalLoadControl.retainBackBufferFromKeyframe();
+ super.onReleased();
}
@Override
@@ -85,20 +32,10 @@ public class LoadController implements LoadControl {
if (!preloadingEnabled) {
return false;
}
- return internalLoadControl.shouldContinueLoading(
+ return super.shouldContinueLoading(
playbackPositionUs, bufferedDurationUs, playbackSpeed);
}
- @Override
- public boolean shouldStartPlayback(final long bufferedDurationUs, final float playbackSpeed,
- final boolean rebuffering) {
- final boolean isInitialPlaybackBufferFilled
- = bufferedDurationUs >= this.initialPlaybackBufferUs * playbackSpeed;
- final boolean isInternalStartingPlayback = internalLoadControl
- .shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering);
- return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
- }
-
public void disablePreloadingOfCurrentTrack() {
preloadingEnabled = false;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index bbe281921..5139ef9cd 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -1,5 +1,8 @@
package org.schabi.newpipe.player.helper;
+import static org.schabi.newpipe.player.Player.DEBUG;
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
@@ -18,9 +21,6 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy;
-import static org.schabi.newpipe.player.Player.DEBUG;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
public class PlaybackParameterDialog extends DialogFragment {
// Minimum allowable range in ExoPlayer
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
@@ -157,7 +157,6 @@ public class PlaybackParameterDialog extends DialogFragment {
setupControlViews(view);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
- .setTitle(R.string.playback_speed_control)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index 5fea4761b..b7584151d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -1,14 +1,18 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
+import android.os.Build;
import androidx.annotation.NonNull;
+import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
+import com.google.android.exoplayer2.source.chunk.MediaParserChunkExtractor;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.hls.MediaParserHlsMediaChunkExtractor;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
@@ -19,7 +23,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
public class PlayerDataSource {
private static final int MANIFEST_MINIMUM_RETRY = 5;
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
- private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
+ public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
private final DataSource.Factory cacheDataSourceFactory;
private final DataSource.Factory cachelessDataSourceFactory;
@@ -32,51 +36,83 @@ public class PlayerDataSource {
}
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
- return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
- cachelessDataSourceFactory), cachelessDataSourceFactory)
+ return new SsMediaSource.Factory(
+ new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
+ cachelessDataSourceFactory
+ )
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
- return new HlsMediaSource.Factory(cachelessDataSourceFactory)
- .setAllowChunklessPreparation(true)
+ final HlsMediaSource.Factory factory =
+ new HlsMediaSource.Factory(cachelessDataSourceFactory)
+ .setAllowChunklessPreparation(true)
+ .setLoadErrorHandlingPolicy(
+ new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ factory.setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY);
+ }
+
+ return factory;
+ }
+
+ public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
+ return new DashMediaSource.Factory(
+ getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
+ cachelessDataSourceFactory
+ )
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
}
- public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
- return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
- cachelessDataSourceFactory), cachelessDataSourceFactory)
- .setLoadErrorHandlingPolicy(
- new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
- .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true);
- }
+ private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
+ final DataSource.Factory dataSourceFactory
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return new DefaultDashChunkSource.Factory(
+ MediaParserChunkExtractor.FACTORY,
+ dataSourceFactory,
+ 1
+ );
+ }
- public SsMediaSource.Factory getSsMediaSourceFactory() {
- return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
- cacheDataSourceFactory), cacheDataSourceFactory);
+ return new DefaultDashChunkSource.Factory(dataSourceFactory);
}
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
- return new HlsMediaSource.Factory(cacheDataSourceFactory);
+ final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(cacheDataSourceFactory);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return factory;
+ }
+
+ // *** >= Android 11 / R / API 30 ***
+ return factory.setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY);
}
public DashMediaSource.Factory getDashMediaSourceFactory() {
- return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
- cacheDataSourceFactory), cacheDataSourceFactory);
+ return new DashMediaSource.Factory(
+ getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
+ cacheDataSourceFactory
+ );
}
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
- return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
- .setLoadErrorHandlingPolicy(
- new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
- }
+ final ProgressiveMediaSource.Factory factory;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ factory = new ProgressiveMediaSource.Factory(
+ cacheDataSourceFactory,
+ MediaParserExtractorAdapter.FACTORY
+ );
+ } else {
+ factory = new ProgressiveMediaSource.Factory(cacheDataSourceFactory);
+ }
- public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory(
- @NonNull final String key) {
- return getExtractorMediaSourceFactory().setCustomCacheKey(key);
+ return factory.setLoadErrorHandlingPolicy(
+ new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
}
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index 828833a8d..c51b6d5dd 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -1,5 +1,18 @@
package org.schabi.newpipe.player.helper;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
+import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
+import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
+import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
+import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
+import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
@@ -21,11 +34,11 @@ import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.SeekParameters;
-import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
+import com.google.android.exoplayer2.ui.CaptionStyleCompat;
import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
@@ -57,19 +70,6 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
-import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
-import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
-import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
-import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
-
public final class PlayerHelper {
private static final StringBuilder STRING_BUILDER = new StringBuilder();
private static final Formatter STRING_FORMATTER
@@ -305,14 +305,7 @@ public final class PlayerHelper {
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
}
- /**
- * @return the number of milliseconds the player buffers for before starting playback
- */
- public static int getPlaybackStartBufferMs() {
- return 500;
- }
-
- public static TrackSelection.Factory getQualitySelector() {
+ public static ExoTrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory(
1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
index d70707fdb..389be7062 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
@@ -13,7 +13,7 @@ import com.google.android.exoplayer2.RendererCapabilities.Capabilities;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.util.Assertions;
/**
@@ -28,7 +28,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
private String preferredTextLanguage;
public CustomTrackSelector(final Context context,
- final TrackSelection.Factory adaptiveTrackSelectionFactory) {
+ final ExoTrackSelection.Factory adaptiveTrackSelectionFactory) {
super(context, adaptiveTrackSelectionFactory);
}
@@ -50,7 +50,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
@Override
@Nullable
- protected Pair selectTextTrack(
+ protected Pair selectTextTrack(
final TrackGroupArray groups,
@NonNull final int[][] formatSupport,
@NonNull final Parameters params,
@@ -86,7 +86,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
}
}
return selectedGroup == null ? null
- : Pair.create(new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
+ : Pair.create(new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex),
Assertions.checkNotNull(selectedTrackScore));
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
index 014c13339..f2259b120 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
@@ -436,14 +436,16 @@ public abstract class PlayQueue implements Serializable {
* top, so shuffling a size-2 list does nothing)
*/
public synchronized void shuffle() {
- // Can't shuffle an list that's empty or only has one element
- if (size() <= 2) {
- return;
- }
// Create a backup if it doesn't already exist
+ // Note: The backup-list has to be created at all cost (even when size <= 2).
+ // Otherwise it's not possible to enter shuffle-mode!
if (backup == null) {
backup = new ArrayList<>(streams);
}
+ // Can't shuffle a list that's empty or only has one element
+ if (size() <= 2) {
+ return;
+ }
final int originalIndex = getIndex();
final PlayQueueItem currentItem = getItem();
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index 81e629c2f..cfe9dbb62 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.util.Util;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@@ -41,20 +42,28 @@ public interface PlaybackResolver extends Resolver {
@NonNull final String sourceUrl,
@C.ContentType final int type,
@NonNull final MediaSourceTag metadata) {
- final Uri uri = Uri.parse(sourceUrl);
+ final MediaSourceFactory factory;
switch (type) {
case C.TYPE_SS:
- return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getLiveSsMediaSourceFactory();
+ break;
case C.TYPE_DASH:
- return dataSource.getLiveDashMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getLiveDashMediaSourceFactory();
+ break;
case C.TYPE_HLS:
- return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getLiveHlsMediaSourceFactory();
+ break;
default:
throw new IllegalStateException("Unsupported type: " + type);
}
+
+ return factory.createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(sourceUrl))
+ .setLiveTargetOffsetMs(PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS)
+ .build()
+ );
}
@NonNull
@@ -67,21 +76,30 @@ public interface PlaybackResolver extends Resolver {
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
+ final MediaSourceFactory factory;
switch (type) {
case C.TYPE_SS:
- return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getLiveSsMediaSourceFactory();
+ break;
case C.TYPE_DASH:
- return dataSource.getDashMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getDashMediaSourceFactory();
+ break;
case C.TYPE_HLS:
- return dataSource.getHlsMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getHlsMediaSourceFactory();
+ break;
case C.TYPE_OTHER:
- return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getExtractorMediaSourceFactory();
+ break;
default:
throw new IllegalStateException("Unsupported type: " + type);
}
+
+ return factory.createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(uri)
+ .setCustomCacheKey(cacheKey)
+ .build()
+ );
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java
index bdf5e8ce4..bbe9a7edb 100644
--- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java
@@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.BatteryManager;
import android.os.Build;
+import android.provider.Settings;
import android.util.TypedValue;
import android.view.KeyEvent;
@@ -144,4 +145,11 @@ public final class DeviceUtils {
public static boolean isInMultiWindow(final AppCompatActivity activity) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
}
+
+ public static boolean hasAnimationsAnimatorDurationEnabled(final Context context) {
+ return Settings.System.getFloat(
+ context.getContentResolver(),
+ Settings.Global.ANIMATOR_DURATION_SCALE,
+ 1F) != 0F;
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
index e33c4da87..0ffbe4137 100644
--- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
+++ b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
@@ -5,11 +5,13 @@ import android.net.Uri;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
+import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
@@ -194,6 +196,16 @@ public enum StreamDialogEntry {
void onClick(Fragment fragment, StreamInfoItem infoItem);
}
+ public static boolean shouldAddMarkAsWatched(final StreamType streamType,
+ final Context context) {
+ final boolean isWatchHistoryEnabled = PreferenceManager
+ .getDefaultSharedPreferences(context)
+ .getBoolean(context.getString(R.string.enable_watch_history_key), false);
+ return streamType != StreamType.AUDIO_LIVE_STREAM
+ && streamType != StreamType.LIVE_STREAM
+ && isWatchHistoryEnabled;
+ }
+
/////////////////////////////////////////////
// private method to open channel fragment //
/////////////////////////////////////////////
diff --git a/app/src/main/res/layout/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml
index e4eacb0e2..24e062932 100644
--- a/app/src/main/res/layout/activity_player_queue_control.xml
+++ b/app/src/main/res/layout/activity_player_queue_control.xml
@@ -31,84 +31,78 @@
android:id="@+id/play_queue"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_above="@id/center"
+ android:layout_above="@id/metadata"
android:layout_below="@id/appbar"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/play_queue_item" />
-
+
+
-
-
-
-
-
-
-
+ android:layout_above="@id/progress_bar"
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:padding="8dp"
+ tools:ignore="RtlHardcoded,RtlSymmetry">
-
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textSize="14sp"
+ tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec aliquam augue, eget cursus est. Ut id tristique enim, ut scelerisque tellus. Sed ultricies ipsum non mauris ultricies, commodo malesuada velit porta." />
+
+
+
+ android:paddingRight="12dp"
+ android:layout_above="@+id/playback_controls">
-
diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml
index 546602d0c..40db90675 100644
--- a/app/src/main/res/layout/dialog_playback_parameter.xml
+++ b/app/src/main/res/layout/dialog_playback_parameter.xml
@@ -4,9 +4,9 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
- android:paddingLeft="@dimen/video_item_search_padding"
- android:paddingTop="@dimen/video_item_search_padding"
- android:paddingRight="@dimen/video_item_search_padding">
+ android:paddingStart="6dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="6dp">
@@ -344,32 +350,37 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/stepSizeSelector"
- android:layout_margin="@dimen/video_item_search_padding"
+ android:layout_marginStart="12dp"
+ android:layout_marginTop="6dp"
+ android:layout_marginEnd="12dp"
+ android:layout_marginBottom="6dp"
android:background="?attr/separator_color" />
-
+ android:orientation="vertical">
-
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml
index d5ba0e8e3..ebe76af0c 100644
--- a/app/src/main/res/layout/fragment_feed.xml
+++ b/app/src/main/res/layout/fragment_feed.xml
@@ -87,6 +87,19 @@
+
+
feed_update_threshold_key
300
+ feed_show_played_items
show_thumbnail_key
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ff2f8bb71..628879055 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -657,6 +657,7 @@
Not loaded: %d
Loading feed…
Processing feed…
+ New feed items
Select subscriptions
No subscription selected
diff --git a/build.gradle b/build.gradle
index 1bcddd7cc..145515c1e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,6 @@ buildscript {
repositories {
google()
mavenCentral()
- jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
@@ -20,7 +19,6 @@ allprojects {
repositories {
google()
mavenCentral()
- jcenter()
maven { url "https://jitpack.io" }
maven { url "https://clojars.org/repo" }
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index af7be50b1..2d849b60a 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip
+distributionSha256Sum=33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle b/settings.gradle
index b564e3700..0338fde6c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,6 +6,6 @@ include ':app'
//includeBuild('../NewPipeExtractor') {
// dependencySubstitution {
-// substitute module('com.github.TeamNewPipe:NewPipeExtractor') with project(':extractor')
+// substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor')
// }
//}