Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e758b5f
Adapt header handling changes from other recyclerview adapters to fix...
whistlingwoods Aug 9, 2025
738338d
Remove unneeded LayoutInflater from LocalItemListAdapter
whistlingwoods Aug 9, 2025
cea5dd4
Merge branch dev into reapply-local-list-header-fix-by-j-haldane
whistlingwoods Jan 12, 2026
25b1339
[YouTube] Adapt YoutubeHttpDataSource to TVHTML5 client removal in NPE
AudricV Jan 23, 2026
d0f32b3
Merge pull request #12996 from whistlingwoods/reapply-local-list-head…
Stypox Jan 27, 2026
d7dffb7
Add deprecation to LocalItemListAdapter.showFooter(true)
Stypox Jan 27, 2026
d1f6337
Fix removing unwatched streams from playlist when using "remove watched"
TobiGr Dec 21, 2025
0611d65
Use checkbox to remove partially watched videos
TobiGr Dec 21, 2025
8f32532
"Removed watched videos" changed to "Remove watched streams"
TobiGr Dec 21, 2025
8d9af62
Extract dialog creation into its own method
TobiGr Dec 21, 2025
817fccb
Swap && to reduce computation
Stypox Jan 27, 2026
4e0d542
Merge pull request #12929 from TeamNewPipe/fix/playlist-remove-watched
Stypox Jan 27, 2026
5155b24
Partial revert: fix VideoDetailFragment flickering
Stypox Jan 27, 2026
dc160da
Allow reporting ContentNotAvailableException
Stypox Jan 27, 2026
49e95d9
Revert "Remember and restore orientation on fullscreen exit"
Stypox Jan 28, 2026
bffee48
Update NewPipeExtractor
AudricV Jan 27, 2026
c0613b5
Merge pull request #13132 from Stypox/regression-detailfragment-flickers
Stypox Jan 28, 2026
7e36578
Merge pull request #13133 from Stypox/missing-report-button
Stypox Jan 28, 2026
11af6a2
Merge pull request #13129 from AudricV/npe_update
Stypox Jan 28, 2026
635b306
Merge pull request #13134 from TeamNewPipe/revert-12781-feat/similar-…
Stypox Jan 28, 2026
077f34c
Add a YouTube DASH manifest parser to make live DASH manifests usable
AudricV Jul 5, 2025
c670ad8
Use DASH first instead of HLS and YouTube's DASH parser for lives
AudricV Jul 5, 2025
0578e7f
Rename useVideoSource to useVideoAndSubtitles in Player
AudricV Jul 5, 2025
4648cac
Allow changing video and text tracks state without stream info
AudricV Sep 4, 2025
1d8ea01
Disable fetching video and text tracks in background player
AudricV Sep 4, 2025
216867c
Address review comments
Stypox Jan 28, 2026
eb7351c
Remove file committed by accident
Stypox Jan 28, 2026
c272309
Avoid rebuilding BackgroundPlayerUi if already in place
Stypox Jan 28, 2026
864725b
Merge pull request #12601 from AudricV/live-prefer-dash-and-fetch-bg-…
Stypox Jan 28, 2026
ee52b08
Translated using Weblate (Basque)
weblate Jan 28, 2026
d53f7ac
Translated using Weblate (Somali)
weblate Jan 28, 2026
a40d7ff
Hotfix release v0.28.2 (1007)
Stypox Jan 28, 2026
addf1e2
Add changelogs for v0.28.2 (1007)
Stypox Jan 28, 2026
35315d0
Merge branch 'dev' into Merge-dev-to-refactor
Isira-Seneviratne Jan 29, 2026
60615e6
Fix compile errors
Isira-Seneviratne Feb 2, 2026
abf9a80
Fix ErrorPanelTest after changes to exception classifications
Stypox Feb 2, 2026
f63ea4a
Fix ErrorInfoTest after changes to exception classifications
Stypox Feb 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ android {
minSdk = 23
targetSdk = 35

versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1006
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1007

versionName = "0.28.1"
versionName = "0.28.2"
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import androidx.test.filters.LargeTest
import java.io.IOException
import java.net.SocketTimeoutException
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
Expand All @@ -25,7 +24,7 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
@RunWith(AndroidJUnit4::class)
@LargeTest
class ErrorInfoTest {
private val context: Context by lazy { ApplicationProvider.getApplicationContext<Context>() }
private val context: Context by lazy { ApplicationProvider.getApplicationContext() }

/**
* @param errorInfo the error info to access
Expand Down Expand Up @@ -122,7 +121,7 @@ class ErrorInfoTest {
)
assertEquals(context.getString(R.string.recaptcha_request_toast), errorInfo.getMessage(context))
assertEquals(url, errorInfo.recaptchaUrl)
assertFalse(errorInfo.isReportable)
assertTrue(errorInfo.isReportable)
assertTrue(errorInfo.isRetryable)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import org.junit.runner.RunWith
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.exceptions.UnsupportedContentInCountryException
import org.schabi.newpipe.ui.theme.AppTheme

@RunWith(AndroidJUnit4::class)
Expand Down Expand Up @@ -71,10 +71,10 @@ class ErrorPanelTest {
}

/**
* Test Recaptcha Error shows solve, retry and open in browser buttons
* Test Recaptcha Error shows all buttons: solve, retry, open in browser, report
*/
@Test
fun recaptchaErrorShowsSolveAndRetryOpenInBrowserButtons() {
fun recaptchaErrorShowsAllButtons() {
var retryClicked = false
val recaptchaErrorInfo = ErrorInfo(
throwable = ReCaptchaException(
Expand All @@ -99,7 +99,7 @@ class ErrorPanelTest {
composeRule.onNodeWithText(text(R.string.open_in_browser), ignoreCase = true)
.assertIsDisplayed()
composeRule.onNodeWithText(text(R.string.error_snackbar_action), ignoreCase = true)
.assertDoesNotExist()
.assertIsDisplayed()
assert(retryClicked) { "onRetry callback should have been invoked" }
}

Expand All @@ -109,14 +109,14 @@ class ErrorPanelTest {
@Test
fun testNonRetryableErrorHidesRetryAndReportButtons() {
val contentNotAvailable = ErrorInfo(
throwable = ContentNotAvailableException("Video has been removed"),
throwable = UnsupportedContentInCountryException("Not available here"),
userAction = UserAction.REQUESTED_STREAM,
request = "https://example.com/watch?v=qux"
)

setErrorPanel(contentNotAvailable)

composeRule.onNodeWithText(text(R.string.content_not_available))
composeRule.onNodeWithText(text(R.string.unsupported_content_in_country))
.assertIsDisplayed()
composeRule.onNodeWithText(text(R.string.retry), ignoreCase = true)
.assertDoesNotExist()
Expand Down
37 changes: 28 additions & 9 deletions app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -297,13 +297,9 @@ class ErrorInfo private constructor(
// indicates that it's important and is thus reportable
null -> true

// a recaptcha was detected, and the user needs to solve it, there is no use in
// letting users report it
is ReCaptchaException -> false

// the service explicitly said that content is not available (e.g. age restrictions,
// video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> false
// if the service explicitly said that content is not available (e.g. age
// restrictions, video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)

// we know the content is not supported, no need to let the user report it
is ContentNotSupportedException -> false
Expand All @@ -320,8 +316,8 @@ class ErrorInfo private constructor(

fun isRetryable(throwable: Throwable?): Boolean {
return when (throwable) {
// we know the content is not available, retrying won't help
is ContentNotAvailableException -> false
// if we know the content is surely not available, retrying won't help
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)

// we know the content is not supported, retrying won't help
is ContentNotSupportedException -> false
Expand All @@ -331,5 +327,28 @@ class ErrorInfo private constructor(
else -> true
}
}

/**
* Unfortunately sometimes [ContentNotAvailableException] may not indicate that the content
* is blocked/deleted/paid, but may just indicate that we could not extract it. This is an
* inconsistency in the exceptions thrown by the extractor, but until it is fixed, this
* function will distinguish between the two types.
* @return `true` if the content is not available because of a limitation imposed by the
* service or the owner, `false` if the extractor could not extract info about it
*/
fun isContentSurelyNotAvailable(e: ContentNotAvailableException): Boolean {
return when (e) {
is AccountTerminatedException,
is AgeRestrictedContentException,
is GeographicRestrictionException,
is PaidContentException,
is PrivateContentException,
is SoundCloudGoPlusContentException,
is UnsupportedContentInCountryException,
is YoutubeMusicPremiumContentException -> true

else -> false
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ThemeHelper.getItemViewMode;

import java.util.function.Supplier;

/**
* This fragment is design to be used with persistent data such as
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
Expand Down Expand Up @@ -100,7 +102,7 @@ private void refreshItemViewMode() {
//////////////////////////////////////////////////////////////////////////*/

@Nullable
protected ViewBinding getListHeader() {
protected Supplier<View> getListHeaderSupplier() {
return null;
}

Expand Down Expand Up @@ -131,9 +133,9 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) {
itemsList = rootView.findViewById(R.id.items_list);
refreshItemViewMode();

headerRootBinding = getListHeader();
if (headerRootBinding != null) {
itemListAdapter.setHeader(headerRootBinding.getRoot());
final Supplier<View> listHeaderSupplier = getListHeaderSupplier();
if (listHeaderSupplier != null) {
itemListAdapter.setHeaderSupplier(listHeaderSupplier);
}
footerRootBinding = getListFooter();
itemListAdapter.setFooter(footerRootBinding.getRoot());
Expand Down Expand Up @@ -210,6 +212,8 @@ public void showEmptyState() {
showListFooter(false);
}

@Deprecated(since = "Calling this method with `true` may cause crashes, see "
+ "https://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115")
@Override
public void showListFooter(final boolean show) {
if (itemsList == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

/*
* Created by Christian Schabesberger on 01.08.16.
Expand Down Expand Up @@ -88,7 +89,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private final DateTimeFormatter dateTimeFormatter;

private boolean showFooter = false;
private View header = null;
private Supplier<View> headerSupplier = null;
private View footer = null;
private ItemViewMode itemViewMode = ItemViewMode.LIST;
private boolean useItemHandle = false;
Expand All @@ -97,6 +98,7 @@ public LocalItemListAdapter(final Context context) {
recordManager = new HistoryRecordManager(context);
localItemBuilder = new LocalItemBuilder(context);
localItems = new ArrayList<>();

dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Localization.getPreferredLocale(context));
}
Expand Down Expand Up @@ -124,7 +126,7 @@ public void addItems(@Nullable final List<? extends LocalItem> data) {
if (DEBUG) {
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + ", "
+ "localItems.size() = " + localItems.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "header = " + hasHeader() + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
Expand All @@ -144,7 +146,7 @@ public void removeItem(final LocalItem data) {
final int index = localItems.indexOf(data);
if (index != -1) {
localItems.remove(index);
notifyItemRemoved(index + (header != null ? 1 : 0));
notifyItemRemoved(index + (hasHeader() ? 1 : 0));
} else {
// this happens when
// 1) removeItem is called on infoItemDuplicate as in showStreamItemDialog of
Expand Down Expand Up @@ -189,9 +191,9 @@ public void setUseItemHandle(final boolean useItemHandle) {
this.useItemHandle = useItemHandle;
}

public void setHeader(final View header) {
final boolean changed = header != this.header;
this.header = header;
public void setHeaderSupplier(@Nullable final Supplier<View> headerSupplier) {
final boolean changed = headerSupplier != this.headerSupplier;
this.headerSupplier = headerSupplier;
if (changed) {
notifyDataSetChanged();
}
Expand All @@ -201,6 +203,12 @@ public void setFooter(final View view) {
this.footer = view;
}

protected boolean hasHeader() {
return this.headerSupplier != null;
}

@Deprecated(since = "Calling this method with `true` may cause crashes, see "
+ "https://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115")
public void showFooter(final boolean show) {
if (DEBUG) {
Log.d(TAG, "showFooter() called with: show = [" + show + "]");
Expand All @@ -211,18 +219,20 @@ public void showFooter(final boolean show) {

showFooter = show;
if (show) {
Log.w(TAG, "Calling LocalItemListAdapter.showFooter(true) may cause crashes, see https"
+ "://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115");
notifyItemInserted(sizeConsideringHeader());
} else {
notifyItemRemoved(sizeConsideringHeader());
}
}

private int adapterOffsetWithoutHeader(final int offset) {
return offset - (header != null ? 1 : 0);
return offset - (hasHeader() ? 1 : 0);
}

private int sizeConsideringHeader() {
return localItems.size() + (header != null ? 1 : 0);
return localItems.size() + (hasHeader() ? 1 : 0);
}

public ArrayList<LocalItem> getItemsList() {
Expand All @@ -232,7 +242,7 @@ public ArrayList<LocalItem> getItemsList() {
@Override
public int getItemCount() {
int count = localItems.size();
if (header != null) {
if (hasHeader()) {
count++;
}
if (footer != null && showFooter) {
Expand All @@ -242,7 +252,7 @@ public int getItemCount() {
if (DEBUG) {
Log.d(TAG, "getItemCount() called, count = " + count + ", "
+ "localItems.size() = " + localItems.size() + ", "
+ "header = " + header + ", footer = " + footer + ", "
+ "header = " + hasHeader() + ", footer = " + footer + ", "
+ "showFooter = " + showFooter);
}
return count;
Expand All @@ -255,9 +265,9 @@ public int getItemViewType(int position) {
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
}

if (header != null && position == 0) {
if (hasHeader() && position == 0) {
return HEADER_TYPE;
} else if (header != null) {
} else if (hasHeader()) {
position--;
}
if (footer != null && position == localItems.size() && showFooter) {
Expand Down Expand Up @@ -318,7 +328,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup paren
}
switch (type) {
case HEADER_TYPE:
return new HeaderFooterHolder(header);
return new HeaderFooterHolder(headerSupplier.get());
case FOOTER_TYPE:
return new HeaderFooterHolder(footer);
case LOCAL_PLAYLIST_HOLDER_TYPE:
Expand Down Expand Up @@ -366,14 +376,14 @@ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int

if (holder instanceof LocalItemHolder) {
// If header isn't null, offset the items by -1
if (header != null) {
if (hasHeader()) {
position--;
}

((LocalItemHolder) holder)
.updateFromItem(localItems.get(position), recordManager, dateTimeFormatter);
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
((HeaderFooterHolder) holder).view = header;
} else if (holder instanceof HeaderFooterHolder && position == 0 && hasHeader()) {
((HeaderFooterHolder) holder).view = headerSupplier.get();
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
&& footer != null && showFooter) {
((HeaderFooterHolder) holder).view = footer;
Expand All @@ -387,10 +397,10 @@ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, fina
for (final Object payload : payloads) {
if (payload instanceof StreamStateEntity) {
((LocalItemHolder) holder).updateState(localItems
.get(header == null ? position : position - 1), recordManager);
.get(hasHeader() ? position - 1 : position), recordManager);
} else if (payload instanceof Boolean) {
((LocalItemHolder) holder).updateState(localItems
.get(header == null ? position : position - 1), recordManager);
.get(hasHeader() ? position - 1 : position), recordManager);
}
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;

import com.evernote.android.state.State;
import com.google.android.material.snackbar.Snackbar;
Expand Down Expand Up @@ -45,6 +44,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
Expand Down Expand Up @@ -126,12 +126,12 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) {
}

@Override
protected ViewBinding getListHeader() {
protected Supplier<View> getListHeaderSupplier() {
headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(),
itemsList, false);
playlistControlBinding = headerBinding.playlistControl;

return headerBinding;
return headerBinding::getRoot;
}

@Override
Expand Down
Loading