From 108af48b76486d682f8c0c39548e8251358359f2 Mon Sep 17 00:00:00 2001 From: Nathan Schulzke Date: Thu, 23 Sep 2021 21:39:47 -0600 Subject: [PATCH 01/41] Enable Mark as Watched in all the other playlist fragments. --- .../fragments/list/BaseListFragment.java | 13 +++++++++++++ .../list/playlist/PlaylistFragment.java | 13 +++++++++++++ .../local/history/HistoryRecordManager.java | 18 +++++------------- .../history/StatisticsPlaylistFragment.java | 14 ++++++++++++++ .../local/playlist/LocalPlaylistFragment.java | 14 ++++++++++++++ 5 files changed, 59 insertions(+), 13 deletions(-) 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 c30b6fc05..6a255b914 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,19 @@ 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 + final boolean isWatchHistoryEnabled = PreferenceManager + .getDefaultSharedPreferences(context) + .getBoolean(getString(R.string.enable_watch_history_key), false); + if (item.getStreamType() != StreamType.AUDIO_LIVE_STREAM + && item.getStreamType() != StreamType.LIVE_STREAM + && isWatchHistoryEnabled + ) { + 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..b03dddc20 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 @@ -15,6 +15,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; +import androidx.preference.PreferenceManager; import androidx.viewbinding.ViewBinding; import org.reactivestreams.Subscriber; @@ -176,6 +177,18 @@ public class PlaylistFragment extends BaseListInfoFragment { entries.add(StreamDialogEntry.play_with_kodi); } + // show "mark as watched" only when watch history is enabled + final boolean isWatchHistoryEnabled = PreferenceManager + .getDefaultSharedPreferences(context) + .getBoolean(getString(R.string.enable_watch_history_key), false); + if (item.getStreamType() != StreamType.AUDIO_LIVE_STREAM + && item.getStreamType() != StreamType.LIVE_STREAM + && isWatchHistoryEnabled + ) { + 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/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 823e56d9e..03f04235a 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 9632b47f7..4bb907abc 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 @@ -14,6 +14,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; import androidx.viewbinding.ViewBinding; import com.google.android.material.snackbar.Snackbar; @@ -366,6 +367,19 @@ 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 + final boolean isWatchHistoryEnabled = PreferenceManager + .getDefaultSharedPreferences(context) + .getBoolean(getString(R.string.enable_watch_history_key), false); + if (item.getStreamEntity().getStreamType() != StreamType.AUDIO_LIVE_STREAM + && item.getStreamEntity().getStreamType() != StreamType.LIVE_STREAM + && isWatchHistoryEnabled + ) { + 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 21da9e571..2e33f3db4 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 @@ -19,6 +19,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import androidx.viewbinding.ViewBinding; @@ -782,6 +783,19 @@ public class LocalPlaylistFragment extends BaseLocalListFragment Date: Wed, 4 Aug 2021 14:33:40 +0100 Subject: [PATCH 02/41] Fixed shuffle button opacity bug Parameterised shuffle state into initPlayback for potentially passing the shuffle state into the player in the future --- .../main/java/org/schabi/newpipe/player/Player.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 22e66e793..fa63b0345 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -635,6 +635,7 @@ public final class Player implements final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted()); + final boolean shuffleMode = false; //Set the default shuffle mode to disabled /* * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): @@ -691,7 +692,7 @@ public final class Player implements state.getProgressMillis()); } initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); + playbackSkipSilence, playWhenReady, isMuted, shuffleMode); }, error -> { if (DEBUG) { @@ -699,19 +700,19 @@ public final class Player implements } // In case any error we can start playback without history initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); + playbackSkipSilence, playWhenReady, isMuted, shuffleMode); }, () -> { // Completed but not found in history initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); + playbackSkipSilence, playWhenReady, isMuted, shuffleMode); } )); } else { // Good to go... // In a case of equal PlayQueues we can re-init old one but only when it is disposed initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed, - playbackPitch, playbackSkipSilence, playWhenReady, isMuted); + playbackPitch, playbackSkipSilence, playWhenReady, isMuted, shuffleMode); } if (oldPlayerType != playerType && playQueue != null) { @@ -770,10 +771,12 @@ public final class Player implements final float playbackPitch, final boolean playbackSkipSilence, final boolean playOnReady, - final boolean isMuted) { + final boolean isMuted, + final boolean shuffleEnabled) { destroyPlayer(); initPlayer(playOnReady); setRepeatMode(repeatMode); + onShuffleModeEnabledChanged(shuffleEnabled); setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); playQueue = queue; From cf81c3768354dcf0127192cf054ac65cc2503d02 Mon Sep 17 00:00:00 2001 From: 0x416c6578 <14030169+0x416c6578@users.noreply.github.com> Date: Wed, 4 Aug 2021 17:21:50 +0100 Subject: [PATCH 03/41] Removed changes to the intent handler --- .../java/org/schabi/newpipe/player/Player.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 fa63b0345..edfcb35e4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -635,7 +635,6 @@ public final class Player implements final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted()); - final boolean shuffleMode = false; //Set the default shuffle mode to disabled /* * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): @@ -692,7 +691,7 @@ public final class Player implements state.getProgressMillis()); } initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted, shuffleMode); + playbackSkipSilence, playWhenReady, isMuted); }, error -> { if (DEBUG) { @@ -700,19 +699,19 @@ public final class Player implements } // In case any error we can start playback without history initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted, shuffleMode); + playbackSkipSilence, playWhenReady, isMuted); }, () -> { // Completed but not found in history initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted, shuffleMode); + playbackSkipSilence, playWhenReady, isMuted); } )); } else { // Good to go... // In a case of equal PlayQueues we can re-init old one but only when it is disposed initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed, - playbackPitch, playbackSkipSilence, playWhenReady, isMuted, shuffleMode); + playbackPitch, playbackSkipSilence, playWhenReady, isMuted); } if (oldPlayerType != playerType && playQueue != null) { @@ -771,12 +770,11 @@ public final class Player implements final float playbackPitch, final boolean playbackSkipSilence, final boolean playOnReady, - final boolean isMuted, - final boolean shuffleEnabled) { + final boolean isMuted) { destroyPlayer(); initPlayer(playOnReady); setRepeatMode(repeatMode); - onShuffleModeEnabledChanged(shuffleEnabled); + onShuffleModeEnabledChanged(false); setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); playQueue = queue; From d66f933c69ff5064184e2502eac1c7dce520cef1 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 23 Oct 2021 16:46:56 +0200 Subject: [PATCH 04/41] Fixing the shuffle button on the UI is enough. No need for doing the heavier method ``onShuffleModeEnabledChanged(false);`` --- app/src/main/java/org/schabi/newpipe/player/Player.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 edfcb35e4..5435b9f81 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -774,7 +774,8 @@ public final class Player implements destroyPlayer(); initPlayer(playOnReady); setRepeatMode(repeatMode); - onShuffleModeEnabledChanged(false); + // #6825 - Ensure that the shuffle-button is in the correct state on the UI + setShuffleButton(binding.shuffleButton, simpleExoPlayer.getShuffleModeEnabled()); setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); playQueue = queue; From af936bc64602126f1ed06e477bc748ef617f6370 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 23 Oct 2021 17:35:42 +0200 Subject: [PATCH 05/41] Always create a backup list when shuffling The backup-list has to be created at all cost (even when current list size <= 2). Otherwise it's not possible to enter shuffle-mode (as ``isShuffled()`` always returns false)! --- .../org/schabi/newpipe/player/playqueue/PlayQueue.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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..76fed8e9e 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() { + // 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 an list that's empty or only has one element if (size() <= 2) { return; } - // Create a backup if it doesn't already exist - if (backup == null) { - backup = new ArrayList<>(streams); - } final int originalIndex = getIndex(); final PlayQueueItem currentItem = getItem(); From acaf92d671b4dce40632df97504958ccd68ee2c4 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Tue, 2 Nov 2021 17:53:27 +0100 Subject: [PATCH 06/41] Unrevert PR 6824 PR 7061 reverted by mistake PR 6824 (it was a rebase issue). This commit unreverts this change and uses custom TextViews correctly in the file changed by PR 6824. --- .../layout/activity_player_queue_control.xml | 120 +++++++++--------- 1 file changed, 57 insertions(+), 63 deletions(-) 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"> - From 702adb53a7a0e5142805af6a85f2cdb239777f9c Mon Sep 17 00:00:00 2001 From: bopol Date: Wed, 3 Nov 2021 14:49:17 +0100 Subject: [PATCH 07/41] Support PeerTube short links --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c95699fb9..84054265a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,7 +189,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.B0pol:NewPipeExtractor:396aecef19ea61de9915727abd71aa55ef75549f' /** 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 @@ + + + + From ecac897e7b9e80690ed661bdb3c30df4f45ab3cd Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Wed, 3 Nov 2021 17:30:30 +0100 Subject: [PATCH 08/41] Fixed typo --- .../java/org/schabi/newpipe/player/playqueue/PlayQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 76fed8e9e..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 @@ -442,7 +442,7 @@ public abstract class PlayQueue implements Serializable { if (backup == null) { backup = new ArrayList<>(streams); } - // Can't shuffle an list that's empty or only has one element + // Can't shuffle a list that's empty or only has one element if (size() <= 2) { return; } From ad8f791f71d593df9bde4176fb5a226e9056180e Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:18:12 +0100 Subject: [PATCH 09/41] Changed extractor dependency back to TeamNewPipe ...as the required PR was merged. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 84054265a..e6a78a093 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,7 +189,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.B0pol:NewPipeExtractor:396aecef19ea61de9915727abd71aa55ef75549f' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:7e7b78f1b3' /** Checkstyle **/ checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" From cddb9bccb93f69ed61bd738164b14b1cd14ce287 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 4 Nov 2021 19:46:22 +0100 Subject: [PATCH 10/41] Reworked ``dialog_playback_parameter`` * Removed dependency to @dimen/video_item_search_padding as it's unrelated * Made the margins/paddings a bit smaller * Put the checkboxes inside a layout * Removed some useless attributes (maxLine) --- .../res/layout/dialog_playback_parameter.xml | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) 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"> - + + + + From f933db811711a29b7e3e61e60a33b01d799faccb Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 4 Nov 2021 19:47:08 +0100 Subject: [PATCH 11/41] Added a custom title to also save some margin/padding/etc --- .../player/helper/PlaybackParameterDialog.java | 12 ++++++++---- .../layout/dialog_playback_parameter_title.xml | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/layout/dialog_playback_parameter_title.xml 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..1d1e0d23a 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,11 @@ public class PlaybackParameterDialog extends DialogFragment { setupControlViews(view); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) - .setTitle(R.string.playback_speed_control) + .setCustomTitle( + View.inflate( + getContext(), + R.layout.dialog_playback_parameter_title, + null)) .setView(view) .setCancelable(true) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> diff --git a/app/src/main/res/layout/dialog_playback_parameter_title.xml b/app/src/main/res/layout/dialog_playback_parameter_title.xml new file mode 100644 index 000000000..0bebf67d1 --- /dev/null +++ b/app/src/main/res/layout/dialog_playback_parameter_title.xml @@ -0,0 +1,18 @@ + + + + + + + From 62efb588efd056b0414cca7c191a7b48ee155281 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 7 Nov 2021 13:51:43 +0100 Subject: [PATCH 12/41] Removed obvious title from the "Playback Speed Controls" --- .../player/helper/PlaybackParameterDialog.java | 5 ----- .../layout/dialog_playback_parameter_title.xml | 18 ------------------ 2 files changed, 23 deletions(-) delete mode 100644 app/src/main/res/layout/dialog_playback_parameter_title.xml 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 1d1e0d23a..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 @@ -157,11 +157,6 @@ public class PlaybackParameterDialog extends DialogFragment { setupControlViews(view); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) - .setCustomTitle( - View.inflate( - getContext(), - R.layout.dialog_playback_parameter_title, - null)) .setView(view) .setCancelable(true) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> diff --git a/app/src/main/res/layout/dialog_playback_parameter_title.xml b/app/src/main/res/layout/dialog_playback_parameter_title.xml deleted file mode 100644 index 0bebf67d1..000000000 --- a/app/src/main/res/layout/dialog_playback_parameter_title.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - From 63291f81011757bc6a6709f9e4a4671f9e9471a2 Mon Sep 17 00:00:00 2001 From: Baji Shaik Date: Sun, 7 Nov 2021 23:11:10 -0500 Subject: [PATCH 13/41] added show watched items toggle preference default sharedpreference is used to persist and retrieve show watched menu option toggle state --- .../org/schabi/newpipe/local/feed/FeedFragment.kt | 4 +++- .../org/schabi/newpipe/local/feed/FeedViewModel.kt | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) 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..61805715c 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 @@ -128,6 +128,7 @@ class FeedFragment : BaseStateFragment() { val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems) viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java) + showPlayedItems = viewModel.getSavedPlayedItemsToggle() viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) }) groupAdapter = GroupieAdapter().apply { @@ -158,7 +159,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 { @@ -213,6 +214,7 @@ class FeedFragment : BaseStateFragment() { showPlayedItems = !item.isChecked updateTogglePlayedItemsButton(item) viewModel.togglePlayedItems(showPlayedItems) + viewModel.savePlayedItemsToggle(showPlayedItems) } return super.onOptionsItemSelected(item) 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..5f18b6342 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,10 +1,12 @@ 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 @@ -28,6 +30,10 @@ class FeedViewModel( initialShowPlayedItems: Boolean = true ) : ViewModel() { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) + private var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + companion object { + const val SHOW_PLAYED_ITEMS_PREFERENCE = "show_played_items_preference_tag" + } private val toggleShowPlayedItems = BehaviorProcessor.create() private val streamItems = toggleShowPlayedItems @@ -81,6 +87,13 @@ class FeedViewModel( toggleShowPlayedItems.onNext(showPlayedItems) } + fun savePlayedItemsToggle(showPlayedItems: Boolean) = sharedPreferences.edit { + this.putBoolean(SHOW_PLAYED_ITEMS_PREFERENCE, showPlayedItems) + this.apply() + } + + fun getSavedPlayedItemsToggle() = sharedPreferences.getBoolean(SHOW_PLAYED_ITEMS_PREFERENCE, true) + class Factory( private val context: Context, private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, From c35fe4f3f199b59e3763c8983ac8851e2488eefd Mon Sep 17 00:00:00 2001 From: Baji Shaik Date: Wed, 10 Nov 2021 16:16:17 -0500 Subject: [PATCH 14/41] moved preference key from viewmodel to settings_keys.xml --- .../org/schabi/newpipe/local/feed/FeedViewModel.kt | 10 ++++------ app/src/main/res/values/settings_keys.xml | 1 + 2 files changed, 5 insertions(+), 6 deletions(-) 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 5f18b6342..cea787c5a 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 @@ -12,6 +12,7 @@ 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 @@ -25,15 +26,12 @@ import java.time.OffsetDateTime import java.util.concurrent.TimeUnit class FeedViewModel( - applicationContext: Context, + val applicationContext: Context, groupId: Long = FeedGroupEntity.GROUP_ALL_ID, initialShowPlayedItems: Boolean = true ) : ViewModel() { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) private var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) - companion object { - const val SHOW_PLAYED_ITEMS_PREFERENCE = "show_played_items_preference_tag" - } private val toggleShowPlayedItems = BehaviorProcessor.create() private val streamItems = toggleShowPlayedItems @@ -88,11 +86,11 @@ class FeedViewModel( } fun savePlayedItemsToggle(showPlayedItems: Boolean) = sharedPreferences.edit { - this.putBoolean(SHOW_PLAYED_ITEMS_PREFERENCE, showPlayedItems) + this.putBoolean(applicationContext.getString(R.string.show_played_items_filter_key), showPlayedItems) this.apply() } - fun getSavedPlayedItemsToggle() = sharedPreferences.getBoolean(SHOW_PLAYED_ITEMS_PREFERENCE, true) + fun getSavedPlayedItemsToggle() = sharedPreferences.getBoolean(applicationContext.getString(R.string.show_played_items_filter_key), true) class Factory( private val context: Context, diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 1c57178b4..e60cf17c9 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -14,6 +14,7 @@ saved_tabs_key + show_played_items_preference_key download_path download_path_audio From adf9badbf6a8e49a7aba8953f977283a125ab985 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 11 Nov 2021 19:46:15 +0100 Subject: [PATCH 15/41] Fixed toggle not in sync with list after app restart + refactored the code a bit --- .../schabi/newpipe/local/feed/FeedFragment.kt | 6 ++-- .../newpipe/local/feed/FeedViewModel.kt | 30 ++++++++++++------- app/src/main/res/values/settings_keys.xml | 2 +- 3 files changed, 24 insertions(+), 14 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 61805715c..305802cf5 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 @@ -126,9 +126,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.getSavedPlayedItemsToggle() + showPlayedItems = viewModel.getShowPlayedItemsFromPreferences() viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) }) groupAdapter = GroupieAdapter().apply { @@ -214,7 +214,7 @@ class FeedFragment : BaseStateFragment() { showPlayedItems = !item.isChecked updateTogglePlayedItemsButton(item) viewModel.togglePlayedItems(showPlayedItems) - viewModel.savePlayedItemsToggle(showPlayedItems) + viewModel.saveShowPlayedItemsToPreferences(showPlayedItems) } return super.onOptionsItemSelected(item) 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 cea787c5a..ecdcb7349 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 @@ -26,12 +26,11 @@ import java.time.OffsetDateTime import java.util.concurrent.TimeUnit class FeedViewModel( - val applicationContext: Context, + private val applicationContext: Context, groupId: Long = FeedGroupEntity.GROUP_ALL_ID, initialShowPlayedItems: Boolean = true ) : ViewModel() { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) - private var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) private val toggleShowPlayedItems = BehaviorProcessor.create() private val streamItems = toggleShowPlayedItems @@ -85,21 +84,32 @@ class FeedViewModel( toggleShowPlayedItems.onNext(showPlayedItems) } - fun savePlayedItemsToggle(showPlayedItems: Boolean) = sharedPreferences.edit { - this.putBoolean(applicationContext.getString(R.string.show_played_items_filter_key), showPlayedItems) - this.apply() - } + fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) = + PreferenceManager.getDefaultSharedPreferences(applicationContext).edit { + this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems) + this.apply() + } - fun getSavedPlayedItemsToggle() = sharedPreferences.getBoolean(applicationContext.getString(R.string.show_played_items_filter_key), true) + 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/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index e60cf17c9..01de6e977 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -14,7 +14,7 @@ saved_tabs_key - show_played_items_preference_key + feed_show_played_items download_path download_path_audio From 90cc8e2144b93db9d3557e16c376c09b7922402f Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 11 Nov 2021 19:49:46 +0100 Subject: [PATCH 16/41] A feed settings-key better fits there --- app/src/main/res/values/settings_keys.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 01de6e977..9db147deb 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -14,7 +14,6 @@ saved_tabs_key - feed_show_played_items download_path download_path_audio @@ -264,6 +263,7 @@ feed_update_threshold_key 300 + feed_show_played_items show_thumbnail_key From 53303ac5d39559ebd02f5c48a7bb248d2da1e74a Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 11 Nov 2021 20:17:54 +0100 Subject: [PATCH 17/41] Replaced deprecated ``with`` with ``using`` --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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') // } //} From eae1f8b5976dd11cfc4435a3302b8913d1d2ac9a Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 28 Aug 2021 19:41:58 +0200 Subject: [PATCH 18/41] Update ExoPlayer to 2.14.2 --- app/build.gradle | 2 +- .../org/schabi/newpipe/player/Player.java | 57 ++++++++++--------- .../newpipe/player/helper/AudioReactor.java | 9 +-- .../newpipe/player/helper/LoadController.java | 9 +-- .../player/helper/PlayerDataSource.java | 10 +--- .../newpipe/player/helper/PlayerHelper.java | 4 +- .../player/playback/CustomTrackSelector.java | 8 +-- .../player/resolver/PlaybackResolver.java | 17 ++++-- 8 files changed, 58 insertions(+), 58 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e6a78a093..94bec47fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,7 +105,7 @@ ext { androidxRoomVersion = '2.3.0' icepickVersion = '3.2.0' - exoPlayerVersion = '2.12.3' + exoPlayerVersion = '2.14.2' googleAutoServiceVersion = '1.0' groupieVersion = '2.10.0' markwonVersion = '4.6.2' 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 5435b9f81..d2e89baa0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.player; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP; import static com.google.android.exoplayer2.Player.DiscontinuityReason; -import static com.google.android.exoplayer2.Player.EventListener; +import static com.google.android.exoplayer2.Player.Listener; 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; @@ -116,6 +117,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player.PositionInfo; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -123,13 +125,14 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.CaptionStyleCompat; import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.VideoListener; +import com.google.android.exoplayer2.video.VideoSize; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; @@ -197,9 +200,8 @@ import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.SerialDisposable; public final class Player implements - EventListener, PlaybackListener, - VideoListener, + Listener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, PopupMenu.OnMenuItemClickListener, @@ -501,10 +503,6 @@ public final class Player implements // Setup video view setupVideoSurface(); - simpleExoPlayer.addVideoListener(this); - - // Setup subtitle view - simpleExoPlayer.addTextOutput(binding.subtitleView); // enable media tunneling if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context) @@ -513,7 +511,7 @@ public final class Player implements + "media tunneling disabled in debug preferences"); } else if (DeviceUtils.shouldSupportMediaTunneling()) { trackSelector.setParameters(trackSelector.buildUponParameters() - .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); + .setTunnelingEnabled(true)); } else if (DEBUG) { Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling"); } @@ -809,7 +807,6 @@ public final class Player implements if (!exoPlayerIsNull()) { simpleExoPlayer.removeListener(this); - simpleExoPlayer.removeVideoListener(this); simpleExoPlayer.stop(); simpleExoPlayer.release(); } @@ -898,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 @@ -2437,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 + "]"); @@ -2449,7 +2448,7 @@ 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_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()) { @@ -2470,7 +2469,8 @@ public final class Player implements playQueue.setIndex(newWindowIndex); } break; - case DISCONTINUITY_REASON_AD_INSERTION: + case DISCONTINUITY_REASON_SKIP: + case DISCONTINUITY_REASON_AUTO_TRANSITION: break; // only makes Android Studio linter happy, as there are no ads } @@ -2482,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 @@ -2503,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) { @@ -3867,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..ca3b1a3c1 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 @@ -4,7 +4,7 @@ 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.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.upstream.Allocator; public class LoadController implements LoadControl { @@ -47,7 +47,7 @@ public class LoadController implements LoadControl { @Override public void onTracksSelected(final Renderer[] renderers, final TrackGroupArray trackGroups, - final TrackSelectionArray trackSelections) { + final ExoTrackSelection[] trackSelections) { internalLoadControl.onTracksSelected(renderers, trackGroups, trackSelections); } @@ -91,11 +91,12 @@ public class LoadController implements LoadControl { @Override public boolean shouldStartPlayback(final long bufferedDurationUs, final float playbackSpeed, - final boolean rebuffering) { + final boolean rebuffering, final long targetLiveOffsetUs) { final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >= this.initialPlaybackBufferUs * playbackSpeed; final boolean isInternalStartingPlayback = internalLoadControl - .shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering); + .shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering, + targetLiveOffsetUs); return isInitialPlaybackBufferFilled || isInternalStartingPlayback; } 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..42a7838c3 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 @@ -19,7 +19,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; @@ -50,8 +50,7 @@ public class PlayerDataSource { return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( cachelessDataSourceFactory), cachelessDataSourceFactory) .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) - .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true); + new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); } public SsMediaSource.Factory getSsMediaSourceFactory() { @@ -74,11 +73,6 @@ public class PlayerDataSource { new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); } - public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory( - @NonNull final String key) { - return getExtractorMediaSourceFactory().setCustomCacheKey(key); - } - public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { return new SingleSampleMediaSource.Factory(cacheDataSourceFactory); } 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..80dacc801 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 @@ -21,11 +21,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; 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/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java index 81e629c2f..d0c2009a1 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 @@ -35,7 +35,7 @@ public interface PlaybackResolver extends Resolver { return null; } - +0 @NonNull default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, @NonNull final String sourceUrl, @@ -48,7 +48,12 @@ public interface PlaybackResolver extends Resolver { .createMediaSource(MediaItem.fromUri(uri)); case C.TYPE_DASH: return dataSource.getLiveDashMediaSourceFactory().setTag(metadata) - .createMediaSource(MediaItem.fromUri(uri)); + .createMediaSource( + new MediaItem.Builder() + .setUri(uri) + .setLiveTargetOffsetMs( + PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS) + .build()); case C.TYPE_HLS: return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata) .createMediaSource(MediaItem.fromUri(uri)); @@ -78,8 +83,12 @@ public interface PlaybackResolver extends Resolver { return dataSource.getHlsMediaSourceFactory().setTag(metadata) .createMediaSource(MediaItem.fromUri(uri)); case C.TYPE_OTHER: - return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata) - .createMediaSource(MediaItem.fromUri(uri)); + return dataSource.getExtractorMediaSourceFactory().setTag(metadata) + .createMediaSource( + new MediaItem.Builder() + .setUri(uri) + .setCustomCacheKey(cacheKey) + .build()); default: throw new IllegalStateException("Unsupported type: " + type); } From 1b9c2b37c5e1b1c74a4ad27ae3592b8b4a4f2f8d Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 Oct 2021 12:07:25 +0200 Subject: [PATCH 19/41] Use Android11+ extractors --- .../org/schabi/newpipe/player/Player.java | 2 +- .../player/helper/PlayerDataSource.java | 66 +++++++++++++++---- .../newpipe/player/helper/PlayerHelper.java | 28 ++++---- .../player/resolver/PlaybackResolver.java | 2 +- 4 files changed, 68 insertions(+), 30 deletions(-) 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 d2e89baa0..b2708e075 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -177,12 +177,12 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder; -import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.SerializedCache; +import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.views.ExpandableSurfaceView; 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 42a7838c3..9f419520d 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; @@ -40,17 +44,33 @@ public class PlayerDataSource { } public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { - return new HlsMediaSource.Factory(cachelessDataSourceFactory) - .setAllowChunklessPreparation(true) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return new HlsMediaSource.Factory(cachelessDataSourceFactory) + .setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY) + .setAllowChunklessPreparation(true) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); + } else { + return new HlsMediaSource.Factory(cachelessDataSourceFactory) + .setAllowChunklessPreparation(true) + .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)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( + MediaParserChunkExtractor.FACTORY, + cachelessDataSourceFactory, 1), cachelessDataSourceFactory) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); + } else { + return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( + cachelessDataSourceFactory), cachelessDataSourceFactory) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); + } } public SsMediaSource.Factory getSsMediaSourceFactory() { @@ -59,18 +79,36 @@ public class PlayerDataSource { } public HlsMediaSource.Factory getHlsMediaSourceFactory() { - return new HlsMediaSource.Factory(cacheDataSourceFactory); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return new HlsMediaSource.Factory(cacheDataSourceFactory) + .setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY); + } else { + return new HlsMediaSource.Factory(cacheDataSourceFactory); + } } public DashMediaSource.Factory getDashMediaSourceFactory() { - return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( - cacheDataSourceFactory), cacheDataSourceFactory); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( + MediaParserChunkExtractor.FACTORY, + cacheDataSourceFactory, 1), cacheDataSourceFactory); + } else { + return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( + cacheDataSourceFactory), cacheDataSourceFactory); + } } public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() { - return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return new ProgressiveMediaSource.Factory(cacheDataSourceFactory, + MediaParserExtractorAdapter.FACTORY) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); + } else { + return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) + .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 80dacc801..6cb94336b 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; @@ -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 @@ -312,7 +312,7 @@ public final class PlayerHelper { 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/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java index d0c2009a1..48ee305ee 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 @@ -35,7 +35,7 @@ public interface PlaybackResolver extends Resolver { return null; } -0 + @NonNull default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, @NonNull final String sourceUrl, From dd2cde3c1adafd09734bd652f604ab4a8434c006 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 12 Nov 2021 19:40:00 +0100 Subject: [PATCH 20/41] De-duplicated PlayerDataSource-code --- .../player/helper/PlayerDataSource.java | 98 ++++++++++--------- 1 file changed, 51 insertions(+), 47 deletions(-) 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 9f419520d..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 @@ -36,79 +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() { + 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) { - return new HlsMediaSource.Factory(cachelessDataSourceFactory) - .setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY) - .setAllowChunklessPreparation(true) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); - } else { - return new HlsMediaSource.Factory(cachelessDataSourceFactory) - .setAllowChunklessPreparation(true) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); + factory.setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY); } + + return factory; } public DashMediaSource.Factory getLiveDashMediaSourceFactory() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( - MediaParserChunkExtractor.FACTORY, - cachelessDataSourceFactory, 1), cachelessDataSourceFactory) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); - } else { - return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( - cachelessDataSourceFactory), cachelessDataSourceFactory) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); - } + return new DashMediaSource.Factory( + getDefaultDashChunkSourceFactory(cachelessDataSourceFactory), + cachelessDataSourceFactory + ) + .setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); } - public SsMediaSource.Factory getSsMediaSourceFactory() { - return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( - cacheDataSourceFactory), cacheDataSourceFactory); + 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 + ); + } + + return new DefaultDashChunkSource.Factory(dataSourceFactory); } public HlsMediaSource.Factory getHlsMediaSourceFactory() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return new HlsMediaSource.Factory(cacheDataSourceFactory) - .setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY); - } else { - 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() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( - MediaParserChunkExtractor.FACTORY, - cacheDataSourceFactory, 1), cacheDataSourceFactory); - } else { - return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( - cacheDataSourceFactory), cacheDataSourceFactory); - } + return new DashMediaSource.Factory( + getDefaultDashChunkSourceFactory(cacheDataSourceFactory), + cacheDataSourceFactory + ); } public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() { + final ProgressiveMediaSource.Factory factory; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return new ProgressiveMediaSource.Factory(cacheDataSourceFactory, - MediaParserExtractorAdapter.FACTORY) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); + factory = new ProgressiveMediaSource.Factory( + cacheDataSourceFactory, + MediaParserExtractorAdapter.FACTORY + ); } else { - return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); + factory = new ProgressiveMediaSource.Factory(cacheDataSourceFactory); } + + return factory.setLoadErrorHandlingPolicy( + new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); } public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { From 48a1ab64b01d5106fa7f7fc4790312b967607966 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 12 Nov 2021 20:14:39 +0100 Subject: [PATCH 21/41] Refactored ``PlaybackResolver`` * fixes the deprecation of ``setTag`` * makes the code more consistent * de-duplicates some code --- .../player/resolver/PlaybackResolver.java | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) 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 48ee305ee..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,25 +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( - new MediaItem.Builder() - .setUri(uri) - .setLiveTargetOffsetMs( - PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS) - .build()); + 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 @@ -72,25 +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().setTag(metadata) - .createMediaSource( - new MediaItem.Builder() - .setUri(uri) - .setCustomCacheKey(cacheKey) - .build()); + factory = dataSource.getExtractorMediaSourceFactory(); + break; default: throw new IllegalStateException("Unsupported type: " + type); } + + return factory.createMediaSource( + new MediaItem.Builder() + .setTag(metadata) + .setUri(uri) + .setCustomCacheKey(cacheKey) + .build() + ); } } From 605b8fac5efda4e0f68c81a4e333dd7c2198f09c Mon Sep 17 00:00:00 2001 From: TobiGr Date: Fri, 15 Oct 2021 23:23:40 +0200 Subject: [PATCH 22/41] Remove JCenter All dependencies which were fetched from JCenter are now available via Maven Central. This source change is necessary becuase JCenter announced they werer going to be read-only starting at 31st March 2021 (https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter). --- build.gradle | 2 -- 1 file changed, 2 deletions(-) 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" } } From dee32c3dc5753f470159efd446dfc4aa052ba535 Mon Sep 17 00:00:00 2001 From: Nathan Schulzke Date: Sat, 13 Nov 2021 10:14:54 -0700 Subject: [PATCH 23/41] Factor out shouldAddMarkAsWatched as a shared function --- .../newpipe/fragments/list/BaseListFragment.java | 8 +------- .../fragments/list/playlist/PlaylistFragment.java | 9 +-------- .../org/schabi/newpipe/local/feed/FeedFragment.kt | 8 +------- .../local/history/StatisticsPlaylistFragment.java | 12 ++++-------- .../local/playlist/LocalPlaylistFragment.java | 12 ++++-------- .../org/schabi/newpipe/util/StreamDialogEntry.java | 12 ++++++++++++ 6 files changed, 23 insertions(+), 38 deletions(-) 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 6a255b914..4899af353 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 @@ -380,13 +380,7 @@ public abstract class BaseListFragment extends BaseStateFragment } // show "mark as watched" only when watch history is enabled - final boolean isWatchHistoryEnabled = PreferenceManager - .getDefaultSharedPreferences(context) - .getBoolean(getString(R.string.enable_watch_history_key), false); - if (item.getStreamType() != StreamType.AUDIO_LIVE_STREAM - && item.getStreamType() != StreamType.LIVE_STREAM - && isWatchHistoryEnabled - ) { + if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) { entries.add( StreamDialogEntry.mark_as_watched ); 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 b03dddc20..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 @@ -15,7 +15,6 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; -import androidx.preference.PreferenceManager; import androidx.viewbinding.ViewBinding; import org.reactivestreams.Subscriber; @@ -178,13 +177,7 @@ public class PlaylistFragment extends BaseListInfoFragment { } // show "mark as watched" only when watch history is enabled - final boolean isWatchHistoryEnabled = PreferenceManager - .getDefaultSharedPreferences(context) - .getBoolean(getString(R.string.enable_watch_history_key), false); - if (item.getStreamType() != StreamType.AUDIO_LIVE_STREAM - && item.getStreamType() != StreamType.LIVE_STREAM - && isWatchHistoryEnabled - ) { + if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) { entries.add( StreamDialogEntry.mark_as_watched ); 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 b3619276d..4959244d8 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 @@ -356,13 +356,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 ) 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 4bb907abc..4f03debca 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 @@ -14,7 +14,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; import androidx.viewbinding.ViewBinding; import com.google.android.material.snackbar.Snackbar; @@ -369,13 +368,10 @@ public class StatisticsPlaylistFragment } // show "mark as watched" only when watch history is enabled - final boolean isWatchHistoryEnabled = PreferenceManager - .getDefaultSharedPreferences(context) - .getBoolean(getString(R.string.enable_watch_history_key), false); - if (item.getStreamEntity().getStreamType() != StreamType.AUDIO_LIVE_STREAM - && item.getStreamEntity().getStreamType() != StreamType.LIVE_STREAM - && isWatchHistoryEnabled - ) { + if (StreamDialogEntry.shouldAddMarkAsWatched( + item.getStreamEntity().getStreamType(), + context + )) { entries.add( StreamDialogEntry.mark_as_watched ); 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 2e33f3db4..11174e735 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 @@ -19,7 +19,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import androidx.viewbinding.ViewBinding; @@ -785,13 +784,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment Date: Sun, 14 Nov 2021 20:12:12 +0100 Subject: [PATCH 24/41] Use DefaultLoadcontrol --- .../newpipe/player/helper/LoadController.java | 74 ++----------------- .../newpipe/player/helper/PlayerHelper.java | 7 -- 2 files changed, 5 insertions(+), 76 deletions(-) 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 ca3b1a3c1..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.ExoTrackSelection; -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 ExoTrackSelection[] 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,21 +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 long targetLiveOffsetUs) { - final boolean isInitialPlaybackBufferFilled - = bufferedDurationUs >= this.initialPlaybackBufferUs * playbackSpeed; - final boolean isInternalStartingPlayback = internalLoadControl - .shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering, - targetLiveOffsetUs); - return isInitialPlaybackBufferFilled || isInternalStartingPlayback; - } - public void disablePreloadingOfCurrentTrack() { preloadingEnabled = false; } 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 6cb94336b..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 @@ -305,13 +305,6 @@ 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 ExoTrackSelection.Factory getQualitySelector() { return new AdaptiveTrackSelection.Factory( 1000, From 3e099fb2a349622d04c62dba55bc576f8f42199d Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 14 Nov 2021 21:19:36 +0100 Subject: [PATCH 25/41] Fixed Period Transition --- app/src/main/java/org/schabi/newpipe/player/Player.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b2708e075..4cc0d4aa4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -2448,6 +2448,7 @@ 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_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 @@ -2470,7 +2471,6 @@ public final class Player implements } break; case DISCONTINUITY_REASON_SKIP: - case DISCONTINUITY_REASON_AUTO_TRANSITION: break; // only makes Android Studio linter happy, as there are no ads } From 010c607e40cc08314cd623b716c9ca10d263762d Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Mon, 8 Nov 2021 19:41:13 +0100 Subject: [PATCH 26/41] Prevent automatic replay after returning from background See also https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380 --- .../main/java/org/schabi/newpipe/player/Player.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 b2708e075..4310ccad7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -857,9 +857,15 @@ public final class Player implements final int queuePos = playQueue.getIndex(); final long windowPos = simpleExoPlayer.getCurrentPosition(); + final long duration = simpleExoPlayer.getDuration(); - if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) { - setRecovery(queuePos, windowPos); + if (windowPos > 0 + // Sometimes (e.g. when the playback ended) the windowPos is a few milliseconds + // higher than the duration. Due to this a little buffer (100ms) was introduced. + // See also https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380 + && windowPos <= duration + 100 + ) { + setRecovery(queuePos, Math.min(windowPos, duration)); } } From 316db0e4c626986f19565dded5f2d4f8d85bffbe Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Mon, 15 Nov 2021 19:56:14 +0100 Subject: [PATCH 27/41] setRecovery: Remove checks and use Math.min/max --- .../main/java/org/schabi/newpipe/player/Player.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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 4310ccad7..d448de5a4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -859,14 +859,8 @@ public final class Player implements final long windowPos = simpleExoPlayer.getCurrentPosition(); final long duration = simpleExoPlayer.getDuration(); - if (windowPos > 0 - // Sometimes (e.g. when the playback ended) the windowPos is a few milliseconds - // higher than the duration. Due to this a little buffer (100ms) was introduced. - // See also https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380 - && windowPos <= duration + 100 - ) { - setRecovery(queuePos, Math.min(windowPos, duration)); - } + // 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) { From 8b807b07069aa2ffd6650090b4be543f6366e1ed Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 3 Sep 2021 21:29:10 +0200 Subject: [PATCH 28/41] Enhanced ``View.slideUp`` --- .../main/java/org/schabi/newpipe/ktx/View.kt | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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..d7e5a1c42 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) +} + +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() } From 676bc02d5256294e981c375216a6c04125d7d871 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 3 Sep 2021 21:35:23 +0200 Subject: [PATCH 29/41] No more reaction to unnecessary feed db-changes This caused duplicate events (https://github.com/TeamNewPipe/NewPipe/pull/6686#issuecomment-909575283) and unnecessary processing of items --- .../newpipe/database/feed/dao/FeedDAO.kt | 9 ++--- .../newpipe/local/feed/FeedDatabaseManager.kt | 2 +- .../newpipe/local/feed/FeedViewModel.kt | 34 ++++++++++++++----- 3 files changed, 32 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 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/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index ff7c2848e..e28f2d31a 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/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index ecdcb7349..bdf5a60a8 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 @@ -33,12 +33,9 @@ class FeedViewModel( 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 @@ -46,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( @@ -78,7 +86,17 @@ 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) From 02789122a094628fdbd00f788e6870dcf6bf4470 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 3 Sep 2021 22:03:34 +0200 Subject: [PATCH 30/41] Implemented UI highlighting and "new feed items"-notification Fixed format --- .../main/java/org/schabi/newpipe/ktx/View.kt | 14 +- .../schabi/newpipe/local/feed/FeedFragment.kt | 128 +++++++++++++++++- .../newpipe/local/feed/FeedViewModel.kt | 24 ++-- .../newpipe/local/feed/item/StreamItem.kt | 9 ++ app/src/main/res/layout/fragment_feed.xml | 20 +++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 176 insertions(+), 20 deletions(-) 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 d7e5a1c42..496ae814f 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt @@ -300,18 +300,18 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long, } fun View.slideUp( - duration: Long, - delay: Long, - @FloatRange(from = 0.0, to = 1.0) translationPercent: Float + duration: Long, + delay: Long, + @FloatRange(from = 0.0, to = 1.0) translationPercent: Float ) { slideUp(duration, delay, translationPercent) } fun View.slideUp( - duration: Long, - delay: Long = 0L, - @FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F, - execOnEnd: Runnable? = null + 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() 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 118e65023..b408fa9b7 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 @@ -40,8 +40,10 @@ import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager -import com.xwray.groupie.GroupieAdapter +import androidx.recyclerview.widget.RecyclerView +import com.xwray.groupie.GroupAdapter import com.xwray.groupie.Item +import com.xwray.groupie.OnAsyncUpdateListener import com.xwray.groupie.OnItemClickListener import com.xwray.groupie.OnItemLongClickListener import icepick.State @@ -65,6 +67,7 @@ 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 @@ -76,6 +79,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 +101,8 @@ class FeedFragment : BaseStateFragment() { private var updateListViewModeOnResume = false private var isRefreshing = false + private var lastNewItemsCount = 0 + init { setHasOptionsMenu(true) } @@ -136,6 +142,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 (feedBinding.newItemsLoadedLayout.isVisible) { + hideNewItemsLoaded(true) + } + } + } + }) + feedBinding.itemsList.adapter = groupAdapter setupListViewMode() } @@ -171,6 +191,10 @@ class FeedFragment : BaseStateFragment() { super.initListeners() feedBinding.refreshRootView.setOnClickListener { reloadContent() } feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() } + feedBinding.newItemsLoadedButton.setOnClickListener { + hideNewItemsLoaded(true) + feedBinding.itemsList.scrollToPosition(0) + } } // ///////////////////////////////////////////////////////////////////////// @@ -400,7 +424,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) @@ -522,6 +556,94 @@ 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 resid = R.attr.selectableItemBackground + if (doCheck) { + if (item.streamWithState.stream.uploadDate?.isAfter(updateTime) != false) { + resid = R.attr.dashed_border + highlightCount++ + } else { + // Increases 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 = + androidx.core.content.ContextCompat.getDrawable( + context, + android.util.TypedValue().apply { + context.theme.resolveAttribute( + resid, + this, + true + ) + }.resourceId + ) + } + } + + // 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, + groupAdapter.itemCount.coerceAtMost(highlightCount.coerceAtLeast(lastNewItemsCount)) + ) + + if (highlightCount > 0) { + showNewItemsLoaded() + } + + lastNewItemsCount = highlightCount + } + + private fun showNewItemsLoaded() { + feedBinding.newItemsLoadedLayout.clearAnimation() + feedBinding.newItemsLoadedLayout + .slideUp( + 250L, + delay = 100, + execOnEnd = { + // Hide the new items-"popup" after 10s + hideNewItemsLoaded(true, 10000) + } + ) + } + + private fun hideNewItemsLoaded(animate: Boolean, delay: Long = 0) { + feedBinding.newItemsLoadedLayout.clearAnimation() + if (animate) { + feedBinding.newItemsLoadedLayout.animate( + false, + 200, + delay = delay, + execOnEnd = { + // Make the layout invisible so that the onScroll toTop method + // only does necessary work + feedBinding.newItemsLoadedLayout.isVisible = false + } + ) + } else { + feedBinding.newItemsLoadedLayout.isVisible = false + } + } + // ///////////////////////////////////////////////////////////////////////// // Load Service Handling // ///////////////////////////////////////////////////////////////////////// @@ -529,6 +651,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 bdf5a60a8..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 @@ -43,7 +43,7 @@ class FeedViewModel( private var combineDisposable = Flowable .combineLatest( FeedEventManager.events(), - toggleShowPlayedItemsFlowable, + toggleShowPlayedItemsFlowable, feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId), @@ -58,8 +58,8 @@ class FeedViewModel( .map { (event, showPlayedItems, notLoadedCount, oldestUpdate) -> var streamItems = if (event is SuccessResultEvent || event is IdleEvent) feedDatabaseManager - .getStreams(groupId, showPlayedItems) - .blockingGet(arrayListOf()) + .getStreams(groupId, showPlayedItems) + .blockingGet(arrayListOf()) else arrayListOf() @@ -87,16 +87,18 @@ class FeedViewModel( } private data class CombineResultEventHolder( - val t1: FeedEventManager.Event, - val t2: Boolean, - val t3: Long, - val t4: OffsetDateTime?) + 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?) + val t1: FeedEventManager.Event, + val t2: List, + val t3: Long, + val t4: OffsetDateTime? + ) fun togglePlayedItems(showPlayedItems: Boolean) { toggleShowPlayedItems.onNext(showPlayedItems) 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/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index d5ba0e8e3..8b2a44141 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -87,6 +87,26 @@ + + +