1
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-10-21 02:32:14 +02:00

Compare commits

...

80 Commits

Author SHA1 Message Date
Aayush Gupta
ee01ba3209 Specify JDK toolchain directly
Specifying JDK toolchain in the java block lets us avoid specifying
same version again and again for different options while ensuring everything
is on the same version

Ref: https://developer.android.com/build/jdks#toolchain

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2025-10-17 16:28:00 +08:00
Aayush Gupta
1bef2fdc25 Drop deprecated non-working archivesBaseName property
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2025-10-16 22:27:56 +08:00
Jie Li
061ce870ac Gradle script to enforce dependencies order
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2025-10-16 22:27:15 +08:00
Aayush Gupta
15089245bb Migrate to build version catalog
Ref: https://developer.android.com/build/migrate-to-catalogs

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2025-10-16 22:25:25 +08:00
Aayush Gupta
d99435c4ad Migrate build scripts to kotlin DSL
Ref: https://developer.android.com/build/migrate-to-kotlin-dsl

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2025-10-14 23:07:07 +08:00
Aayush Gupta
320c693636 Revert "Add snippet to ensure baseline.profm file is sorted"
This reverts commit 1db1a00581.

The issue has been long resolved making this fix no longer required.
2025-10-14 23:07:07 +08:00
Tobi
eee1172e8a Merge pull request #12692 from Zer0tier/issue#12433
[Bug] Long-pressing Play All-button does nothing
2025-10-10 08:08:55 -07:00
Howar
0ebd01e65e fix(playback): handle long-press on “Play All” button for issue #12433 2025-10-11 02:01:52 +11:00
Stypox
965eea2124 Merge pull request #12676 from thonsi/dev 2025-10-02 16:22:14 +02:00
Thonsi
59dfdda95e remove isUsingDSP 2025-10-01 15:56:08 +00:00
Stypox
3a2d427a46 Merge pull request #12642 from Stypox/fireos-SAF 2025-10-01 16:28:12 +02:00
Tobi
c25f83da6c Merge pull request #12671 from TransZAllen/SRT_numbering
Fix initial numbering of frames in TTML to SRT converter
2025-10-01 02:42:33 -07:00
Stypox
e2026dc378 Merge pull request #12606 from Stypox/do-not-startService-randomly 2025-09-30 17:47:08 +02:00
Stypox
00f6203904 Merge pull request #12605 from TeamNewPipe/open-in-browser 2025-09-30 17:45:29 +02:00
TransZAllen
980e8f3951 [YouTube] *.srt numbering start at 1 instead of 0. (issue: https://github.com/TeamNewPipe/NewPipe/issues/12670)
- The SubRip (.srt) specification requires subtitle numbering to begin from 1.
- Please refer to https://en.wikipedia.org/wiki/SubRip
- Previously numbering started from 0, which is accepted by most
  players (tested on mpv, VLC, MPlayer, Totem) but not strictly compliant.
2025-09-29 18:04:35 +08:00
Stypox
4e9a480fdd Enforce using SAF on FireOS TVs with Android 10+
Even if SAF is bugged there, there is no other way to open a file dialog, since NewPipe does not have permissions, see #10643
2025-09-17 12:24:18 +02:00
Stypox
aa2b4821e2 Post dummy notification then close player service on invalid intent
This should solve "Context.startForegroundService() did not then call Service.startForeground()" according to https://github.com/TeamNewPipe/NewPipe/issues/12489#issuecomment-3290318112
2025-09-17 11:50:46 +02:00
Stypox
92a07a3445 Use tryBindIfNeeded(), send player started only if player!=null
This commit fixes one way ghost notifications could be produced (although I don't know if there are other ways). This is the call chain that would lead to ghost notifications being created:
1. the system starts `PlayerService` to query information from it, without providing `SHOULD_START_FOREGROUND_EXTRA=true`, so NewPipe does not start the player nor show any notification, as expected
2. the `PlayerHolder::serviceConnection.onServiceConnected()` gets called by the system to inform `PlayerHolder` that the player started
3. `PlayerHolder`  notifies `MainActivity` that the player has started (although in fact only the service has started), by sending a `ACTION_PLAYER_STARTED` broadcast
4. `MainActivity` receives the `ACTION_PLAYER_STARTED` broadcast and brings up the mini-player, but then also tries to make `PlayerHolder` bind to `PlayerService` just in case it was not bound yet, but does so using `PlayerHolder::startService()` instead of the more passive `PlayerHolder::tryBindIfNeeded()`
5. `PlayerHolder::startService()` sends an intent to the `PlayerService` again, this time with `startForegroundService` and with `SHOULD_START_FOREGROUND_EXTRA=true`
6. the `PlayerService` receives the intent and due to `SHOULD_START_FOREGROUND_EXTRA=true` decides to start up the player and show a dummy notification

Steps 3 and 4 are wrong, and this commit fixes them:
3. `PlayerHolder` will now broadcast `ACTION_PLAYER_STARTED` when the service connects, only if the player is not-null
4. `PlayerHolder::tryBindIfNeeded()` is now used to passively try to bind, instead of `PlayerHolder::startService()`
2025-09-17 11:49:16 +02:00
Tobi
eed09f8a1d Merge pull request #12550 from whistlingwoods/fix-downloads-lost-progress
Try to recover pending download missions when possible
2025-09-16 23:26:56 -07:00
Stypox
fd3f030d0b Merge pull request #12616 from Isira-Seneviratne/Bump-AGP 2025-09-10 09:18:32 +02:00
Profpatsch
45c22c0db8 Merge pull request #12615 from Isira-Seneviratne/Player-intent-refactor
Refactor player intent logic
2025-09-09 10:24:42 +02:00
Isira Seneviratne
2b7c72eb69 Update AGP to 8.13.0 2025-09-08 08:08:07 +05:30
Isira Seneviratne
89c4eb5237 Refactor player intent logic 2025-09-08 07:56:13 +05:30
Stypox
803aba4935 Merge pull request #12254 from TeamNewPipe/timestamp-keep-current-player 2025-09-06 17:51:36 +02:00
Profpatsch
1723bf0e8a Player/handleIntent: keep current player when clicking timestamp
This was always a bit weird, that clicking a timestamp would
unconditionally switch to the popup player.

With the new enum, it’s trivial to change it to always stay at the
selected player now ;)
2025-09-06 17:40:18 +02:00
whistlingwoods
21e24c9e34 Apply review suggestions 2025-09-06 19:14:15 +05:30
Profpatsch
eb277fe14b Player/handleIntent: call handleIntentPost unconditionally
We always need to handleIntentPost otherwise the VideoDetailFragment
is not setup correctly.
2025-09-06 15:31:14 +02:00
Profpatsch
d77771a60c Player/handleIntent: fix enqueue if player not running
In 063dcd41e57c46721f1691cd57d21fbde75a35ea I falsely claimed that the
fallthrough case is always degenerate, but it kinda somehow still
worked because if you long-click on e.g. the popup button, it would
call enqueue, but if nothing was running yet it would fallthrough to
the very last case and start the player with the video.

So let’s return to that and add a TODO for further refactoring in the
future.
2025-09-06 15:09:11 +02:00
Profpatsch
01f9a3de33 Fix Checkstyle & remove unused fields 2025-09-06 15:09:11 +02:00
Profpatsch
150649aea9 Player/handleIntent: Don’t delete queue when clicking on timestamp
Fixes https://github.com/TeamNewPipe/NewPipe/issues/11013

We finally are at the point where we can have good logic around
clicking on timestamps.

This is pretty straightforward:

1) if we are already playing the stream (usual case), we skip to the
   correct second directly
2) If we don’t have a queue yet, create a trivial one with the stream
3) If we have a queue, we insert the video as next item and start
  playing it.

The skipping logic in 1) is similar to the one further down in the old
optimization block, but will always correctly fire for timestamps now.
I copied it because it’s not quite the same code, and moving into a
separate method at this stage would complicate the code too much.
2025-09-06 15:09:11 +02:00
Profpatsch
3803d49489 Player/handleIntent: separate out the timestamp request into enum
Instead of implicitely reconstructing whether the intent was
intended (lol) to be a timestamp change, we create a new kind of
intent that *only* sets the data we need to switch to a new timestamp.

This means that the logic of what to do (opening a popup player) gets
moved from `InternalUrlsHandler.playOnPopup` to the
`Player.handleIntent` method, we only pass that we want to jump to a
new timestamp. Thus, the stream is now loaded *after* sending the
intent instead of before sending.

This is somewhat messy right now and still does not fix the issue of
queue deletion, but from now on the queue logic should get more
straightforward to implement.

In the end, everything should be a giant switch. Thus we don’t
fall-through anymore, but run the post-setup code manually by calling
`handeIntentPost` and then returning.
2025-09-06 15:06:53 +02:00
Profpatsch
25a4a9a253 Player/handleIntent: move prefs parameters into initPlayback
They are just read from the player preferences and don’t influence the
branching, no need to read them in the intent parsing logic.
2025-09-06 15:04:06 +02:00
Profpatsch
d534946550 Player: inline repeat mode cycling 2025-09-06 15:04:06 +02:00
Profpatsch
8fb3e90fe1 Player: remove unused REPEAT_MODE intent key 2025-09-06 15:04:06 +02:00
Profpatsch
5750ef6aa8 Player/handleIntent: start converting intent data to enum
The goal here is to convert all player intents to use a single enum
with extra data for each case. The queue ones are pretty easy, they
don’t carry any extra data. We fall through for everything else for
now.
2025-09-06 15:04:06 +02:00
Profpatsch
ab7d1377e5 Player/handleIntent: always early return on ENQUEUE an ENQUEUE_NEXT
We can do this, because:

1. if `playQueue` is not null, we return early
2. if `playQueue` is null and we need to enqueue:
  - the only “proper” case that could be triggered is
    the `RESUME_PLAYBACK` case, which is never `true` for the queuing
    intents, see the comment in `NavigationHelper.enqueueOnPlayer`
  - the generic `else` case is degenerate, because it would crash on
  `playQueue` being `null`.

This makes some sense, because there is no way to trigger the
enqueueing logic via the UI currently if there is no video playing
yet, in which case `playQueue` is not `null`.

So we need to transform this whole if desaster into a big switch.
2025-09-06 15:04:06 +02:00
Profpatsch
fd24c08529 Player/handleIntent: de morgan samePlayQueue
Okay, so this is the … only? branch in this if-chain that will
conditionally fire if `playQueue` *is* `null`, sometimes.

This is why the unconditional `initPlayback` in `else` is not passed a
`null` in many cases … because `RESUME_PLAYBACK` is `true` and
`playQueue` is `null`.

It’s gonna be hard to figure out which parts of that are intentional,
I say.
2025-09-06 15:04:06 +02:00
Profpatsch
e14ec3a4f9 NavigationHelper: inline trivial getPlayerIntent use 2025-09-06 15:04:06 +02:00
Profpatsch
b592403a66 NavigationHelper: push out resumePlayback one layer 2025-09-06 15:04:06 +02:00
Profpatsch
90e1ac56ef NavigationHelper: inline getPlayerEnqueueIntent
Funnily enough, I’m pretty sure that whole comment will be not
necessary, because we never check `resumePlayback` on handling the
intent anyway.
2025-09-06 15:04:06 +02:00
Profpatsch
32eb3afe16 Player/handleIntent: a few comments 2025-09-06 15:04:06 +02:00
Fynn Godau
83a0abddcc Fix and simplify openUrlInBrowser
The code was not previously working in case no default browser is set[1]
AND NewPipe is set as default handler for the link in question.

We improve it by telling the system to choose the target app as if the
URI was `http://`, which works even if the user has not set a default
browser.

[1]: also the case if platform refuses to tell an app what the user's
default browser is, which I observed on CalyxOS.
2025-09-05 17:49:58 +02:00
Profpatsch
35c7f2f5d1 Player: Remove unused IS_MUTED intent key
The only use of the key was removed in commit
2a2c82e73b
but the handling logic stayed around. So let’s get rid of it.
2025-09-05 16:57:27 +02:00
Stypox
8afb00d2f0 Merge pull request #12603 from Stypox/better-error-panel 2025-09-05 13:31:39 +02:00
Stypox
f27ec53c08 Even more centralized error handling in ErrorInfo 2025-09-05 12:39:16 +02:00
Stypox
a3ddd616f9 Merge pull request #12578 from Stypox/better-error-messages 2025-09-04 13:18:40 +02:00
Stypox
79980e2078 Address PR reviews 2025-09-04 13:17:45 +02:00
Isira Seneviratne
b204fad9d5 Merge pull request #12471 from Isira-Seneviratne/Fix-notifications
Fix foreground service issues
2025-09-01 05:05:47 +05:30
Isira Seneviratne
08f51abefb Added comments 2025-08-31 22:25:12 +05:30
Stypox
204df4c45a Fix test 2025-08-30 14:58:08 +02:00
Stypox
989c0cfd28 Fix REPORT in snackbar not opening ErrorActivity if MainActivity not shown
Bug caused by https://github.com/TeamNewPipe/NewPipe/pull/11789
2025-08-30 14:39:23 +02:00
Stypox
a369deeef4 Allow ErrorInfo messages with formatArgs
- ErrorInfo.getMessage() now returns an ErrorMessage instance that can be formatted into a string using a context (this allows the construction of an ErrorInfo to remain independent of a Context)
- now the service ID is used in ErrorInfo.getMessage() to customize some messages based on the currently selected service
- player HTTP invalid statuses are now included in the message
- building a custom error message for AccountTerminatedException was moved from ErrorPanelHelper to ErrorInfo
2025-08-30 14:36:27 +02:00
Stypox
1bde2dcd9f Fix ordering of error messages conditions 2025-08-28 17:06:10 +02:00
Stypox
29a3ca83b5 Show better information about player errors 2025-08-28 17:06:09 +02:00
Stypox
38064be702 Add more specific error messages and deduplicate their handling 2025-08-28 17:05:52 +02:00
Tobi
d17eae9bad Merge pull request #12253 from Profpatsch/popup-overlay-alert-dialog
Overlay Permission: display explanatory dialog for Android > R
2025-08-27 02:50:45 -07:00
TobiGr
74562db965 Use androidx compat alert dialog 2025-08-27 11:45:31 +02:00
Profpatsch
386d5197d8 Permission: display explanatory dialog for Android > R
On Android > R, ACTION_MANAGE_OVERLAY_PERMISSION always brings the
user to the app selection screen.

https://developer.android.com/about/versions/11/privacy/permissions#manage_overlay

This is highly confusing behaviour from the system, so let’s add an
instruction before navigating to the settings menu.
2025-08-27 11:38:25 +02:00
Tobi
ccd76dea1f Merge pull request #12544 from Stypox/download-options
Add option to delete a download without also deleting file
2025-08-27 02:31:14 -07:00
whistlingwoods
9282cce6a8 fix: unfinished downloads disappear from the downloads list after app gets killed
Author: InfinityLoop1308
Adapted for NewPipe from a fork's this commit 1cf059ce5e
2025-08-22 01:14:24 +05:30
Stypox
7644066c5a Add option to delete a download without also deleting file 2025-08-16 16:50:01 +02:00
Stypox
9bc8139b8c Merge pull request #12483 from TeamNewPipe/ignore-picasso-update 2025-08-11 17:48:30 +02:00
Tobi
ff3526b28d Merge pull request #12460 from Isira-Seneviratne/Short-count-refactor
Fix short count formatting for Android versions below 7.0
2025-08-01 10:56:41 -07:00
TobiGr
d6c0dc32d1 Correctly ignore new version check for picasso 2025-08-01 10:50:54 +02:00
Stypox
124ab56c5f Merge branch 'master' into dev 2025-07-31 23:52:01 +02:00
Stypox
56f79fac13 Merge branch 'release-0.28.0' into dev 2025-07-30 11:42:06 +02:00
Isira Seneviratne
ef29c318b0 Remove NewApi suppression 2025-07-29 06:18:27 +05:30
Isira Seneviratne
9f11db8e06 Improve scale display 2025-07-28 09:02:52 +05:30
Isira Seneviratne
fece0741e5 Suppress NewApi 2025-07-27 15:47:06 +05:30
Isira Seneviratne
b9b47fc520 Update manifest, startForeground call 2025-07-27 11:58:01 +05:30
Isira Seneviratne
59db955493 Fix new streams notification issue 2025-07-27 11:31:23 +05:30
Isira Seneviratne
22a709d53b Merge pull request #12388 from mikooomich/sdk35
Target SDK 35
2025-07-24 08:18:32 +05:30
Michael Zh
329d76c857 Bump emulator target 33 -> 35 2025-07-23 22:30:34 -04:00
Isira Seneviratne
9f526e8e8f Fix short count formatting for Android versions below 7.0 2025-07-24 07:56:44 +05:30
Michael Zh
50caba6606 Fix compile
Co-Authored-By: Isira Seneviratne <31027858+Isira-Seneviratne@users.noreply.github.com>
2025-07-23 18:49:28 -04:00
Michael Zh
26443f9f14 WIP: Fix compile 2025-07-23 18:45:30 -04:00
Michael Zh
366129eee2 Fix error toast crash 2025-07-23 18:45:30 -04:00
Michael Zh
4c8d44b6ba Bump compileSdk to 36 and targetSdk to 35
* Sdk 36 requires edge to edge, so use 35 so we can opt out for now
2025-07-23 18:45:30 -04:00
Michael Zh
14cd562ebd Update manifest for sdk34 FGS changes 2025-07-23 18:45:30 -04:00
Michael Zh
04ef608f7a Specify RECEIVER_EXPORTED/RECEIVER_NOT_EXPORTED for sdk34 2025-07-23 18:45:30 -04:00
137 changed files with 1527 additions and 1342 deletions

View File

@@ -72,8 +72,8 @@ jobs:
- api-level: 21
target: default
arch: x86
- api-level: 33
target: google_apis # emulator API 33 only exists with Google APIs
- api-level: 35
target: default
arch: x86_64
permissions:

File diff suppressed because it is too large Load Diff

297
app/build.gradle.kts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2024 NewPipe contributors <https://newpipe.net>
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
tasks.register("checkDependenciesOrder") {
group = "verification"
description = "Checks that each section in libs.versions.toml is sorted alphabetically"
val tomlFile = file("../gradle/libs.versions.toml")
doLast {
if (!tomlFile.exists()) {
throw GradleException("TOML file not found")
}
val lines = tomlFile.readLines()
val nonSortedBlocks = mutableListOf<List<String>>()
var currentBlock = mutableListOf<String>()
var prevLine = ""
var prevIndex = 0
lines.forEachIndexed { lineIndex, line ->
if (line.trim().isNotEmpty() && !line.startsWith("#")) {
if (line.startsWith("[")) {
prevLine = ""
} else {
val currIndex = lineIndex + 1
if (prevLine > line) {
if (currentBlock.isNotEmpty() && currentBlock.last() == "$prevIndex: $prevLine") {
currentBlock.add("$currIndex: $line")
} else {
if (currentBlock.isNotEmpty()) {
nonSortedBlocks.add(currentBlock)
currentBlock = mutableListOf()
}
currentBlock.add("$prevIndex: $prevLine")
currentBlock.add("$currIndex: $line")
}
}
prevLine = line
prevIndex = lineIndex + 1
}
}
}
if (currentBlock.isNotEmpty()) {
nonSortedBlocks.add(currentBlock)
}
if (nonSortedBlocks.isNotEmpty()) {
throw GradleException(
"The following lines were not sorted:\n" +
nonSortedBlocks.joinToString("\n\n") { it.joinToString("\n") }
)
}
}
}

View File

@@ -12,6 +12,7 @@ import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.Arrays;
import java.util.Objects;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -23,8 +24,23 @@ import static org.junit.Assert.assertTrue;
@LargeTest
public class ErrorInfoTest {
/**
* @param errorInfo the error info to access
* @return the private field errorInfo.message.stringRes using reflection
*/
private int getMessageFromErrorInfo(final ErrorInfo errorInfo)
throws NoSuchFieldException, IllegalAccessException {
final var message = ErrorInfo.class.getDeclaredField("message");
message.setAccessible(true);
final var messageValue = (ErrorInfo.Companion.ErrorMessage) message.get(errorInfo);
final var stringRes = ErrorInfo.Companion.ErrorMessage.class.getDeclaredField("stringRes");
stringRes.setAccessible(true);
return (int) Objects.requireNonNull(stringRes.get(messageValue));
}
@Test
public void errorInfoTestParcelable() {
public void errorInfoTestParcelable() throws NoSuchFieldException, IllegalAccessException {
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
// Obtain a Parcel object and write the parcelable object to it:
@@ -39,7 +55,7 @@ public class ErrorInfoTest {
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
assertEquals(R.string.parsing_error, getMessageFromErrorInfo(infoFromParcel));
parcel.recycle();
}

View File

@@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- We need to be able to open links in the browser on API 30+ -->
@@ -94,9 +96,22 @@
android:exported="false"
android:label="@string/title_activity_about" />
<service android:name=".local.subscription.services.SubscriptionsImportService" />
<service android:name=".local.subscription.services.SubscriptionsExportService" />
<service android:name=".local.feed.service.FeedLoadService" />
<service
android:name=".local.subscription.services.SubscriptionsImportService"
android:foregroundServiceType="dataSync" />
<service
android:name=".local.subscription.services.SubscriptionsExportService"
android:foregroundServiceType="dataSync" />
<service
android:name=".local.feed.service.FeedLoadService"
android:foregroundServiceType="dataSync" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<activity
android:name=".PanicResponderActivity"
@@ -128,7 +143,8 @@
android:label="@string/app_name"
android:launchMode="singleTask" />
<service android:name="us.shandian.giga.service.DownloadManagerService" />
<service android:name="us.shandian.giga.service.DownloadManagerService"
android:foregroundServiceType="dataSync" />
<activity
android:name=".util.FilePickerActivityHelper"
@@ -421,6 +437,7 @@
</activity>
<service
android:name=".RouterActivity$FetcherService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<!-- opting out of sending metrics to Google in Android System WebView -->

View File

@@ -48,6 +48,7 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
@@ -896,7 +897,8 @@ public class MainActivity extends AppCompatActivity {
};
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
registerReceiver(broadcastReceiver, intentFilter);
ContextCompat.registerReceiver(this, broadcastReceiver, intentFilter,
ContextCompat.RECEIVER_EXPORTED);
// If the PlayerHolder is not bound yet, but the service is running, try to bind to it.
// Once the connection is established, the ACTION_PLAYER_STARTED will be sent.

View File

@@ -58,20 +58,10 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.StreamingService.LinkType;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper;
@@ -260,7 +250,8 @@ public class RouterActivity extends AppCompatActivity {
showUnsupportedUrlDialog(url);
}
}, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))));
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url,
null, url))));
}
/**
@@ -269,40 +260,19 @@ public class RouterActivity extends AppCompatActivity {
* @param errorInfo the error information
*/
private static void handleError(final Context context, final ErrorInfo errorInfo) {
if (errorInfo.getThrowable() != null) {
errorInfo.getThrowable().printStackTrace();
}
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
if (errorInfo.getRecaptchaUrl() != null) {
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(context, ReCaptchaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.getRecaptchaUrl());
context.startActivity(intent);
} else if (errorInfo.getThrowable() != null
&& ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
Toast.makeText(context, R.string.restricted_video_no_stream,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PaidContentException) {
Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PrivateContentException) {
Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
Toast.makeText(context, R.string.soundcloud_go_plus_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
Toast.makeText(context, R.string.youtube_music_premium_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
} else {
} else if (errorInfo.isReportable()) {
ErrorUtil.createNotification(context, errorInfo);
} else {
// this exception does not usually indicate a problem that should be reported,
// so just show a toast instead of the notification
Toast.makeText(context, errorInfo.getMessage(context), Toast.LENGTH_LONG).show();
}
if (context instanceof RouterActivity) {
@@ -665,7 +635,8 @@ public class RouterActivity extends AppCompatActivity {
startActivity(intent);
finish();
}, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl,
null, currentUrl)))
);
return;
}
@@ -852,10 +823,10 @@ public class RouterActivity extends AppCompatActivity {
})
)),
throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo(
throwable,
UserAction.REQUESTED_STREAM,
throwable, UserAction.REQUESTED_STREAM,
"Tried to add " + currentUrl + " to a playlist",
((RouterActivity) ctx).currentService.getServiceId())
((RouterActivity) ctx).currentService.getServiceId(),
currentUrl)
))
)
);
@@ -995,7 +966,7 @@ public class RouterActivity extends AppCompatActivity {
}
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
choice.url + " opened with " + choice.playerChoice,
choice.serviceId)));
choice.serviceId, choice.url)));
}
}

View File

@@ -389,8 +389,7 @@ public class DownloadDialog extends DialogFragment
}
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size",
currentInfo.getServiceId()))));
"Downloading video stream size", currentInfo))));
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
@@ -399,8 +398,7 @@ public class DownloadDialog extends DialogFragment
}
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading audio stream size",
currentInfo.getServiceId()))));
"Downloading audio stream size", currentInfo))));
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
@@ -409,8 +407,7 @@ public class DownloadDialog extends DialogFragment
}
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading subtitle stream size",
currentInfo.getServiceId()))));
"Downloading subtitle stream size", currentInfo))));
}
private void setupAudioTrackSpinner() {

View File

@@ -36,8 +36,8 @@ public class AcraReportSender implements ReportSender {
ErrorUtil.openActivity(context, new ErrorInfo(
new String[]{report.getString(ReportField.STACK_TRACE)},
UserAction.UI_ERROR,
ErrorInfo.SERVICE_NONE,
"ACRA report",
null,
R.string.app_ui_crash));
}
}

View File

@@ -115,7 +115,7 @@ public class ErrorActivity extends AppCompatActivity {
// normal bugreport
buildInfo(errorInfo);
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.error
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
@@ -14,21 +13,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
import org.schabi.newpipe.extractor.exceptions.PaidContentException
import org.schabi.newpipe.extractor.exceptions.PrivateContentException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.isInterruptedCaused
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.util.concurrent.TimeUnit
@@ -78,64 +63,32 @@ class ErrorPanelHelper(
}
fun showError(errorInfo: ErrorInfo) {
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]")
}
return
}
ensureDefaultVisibility()
errorTextView.text = errorInfo.getMessage(context)
if (errorInfo.throwable is ReCaptchaException) {
errorTextView.setText(R.string.recaptcha_request_toast)
showAndSetErrorButtonAction(
R.string.recaptcha_solve
) {
if (errorInfo.recaptchaUrl != null) {
showAndSetErrorButtonAction(R.string.recaptcha_solve) {
// Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java)
intent.putExtra(
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
(errorInfo.throwable as ReCaptchaException).url
)
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.recaptchaUrl)
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
errorActionButton.setOnClickListener(null)
}
errorRetryButton.isVisible = retryShouldBeShown
showAndSetOpenInBrowserButtonAction(errorInfo)
} else if (errorInfo.throwable is AccountTerminatedException) {
errorTextView.setText(R.string.account_terminated)
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.text = context.resources.getString(
R.string.service_provides_reason,
ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "<unknown>"
)
errorServiceInfoTextView.isVisible = true
errorServiceExplanationTextView.text =
(errorInfo.throwable as AccountTerminatedException).message
errorServiceExplanationTextView.isVisible = true
}
} else {
showAndSetErrorButtonAction(
R.string.error_snackbar_action
) {
} else if (errorInfo.isReportable) {
showAndSetErrorButtonAction(R.string.error_snackbar_action) {
ErrorUtil.openActivity(context, errorInfo)
}
}
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
if (errorInfo.isRetryable) {
errorRetryButton.isVisible = retryShouldBeShown
}
if (errorInfo.throwable !is ContentNotAvailableException &&
errorInfo.throwable !is ContentNotSupportedException
) {
// show retry button only for content which is not unavailable or unsupported
errorRetryButton.isVisible = retryShouldBeShown
if (errorInfo.openInBrowserUrl != null) {
errorOpenInBrowserButton.isVisible = true
errorOpenInBrowserButton.setOnClickListener {
ShareUtils.openUrlInBrowser(context, errorInfo.openInBrowserUrl)
}
showAndSetOpenInBrowserButtonAction(errorInfo)
}
setRootVisible()
@@ -153,15 +106,6 @@ class ErrorPanelHelper(
errorActionButton.setOnClickListener(listener)
}
fun showAndSetOpenInBrowserButtonAction(
errorInfo: ErrorInfo
) {
errorOpenInBrowserButton.isVisible = true
errorOpenInBrowserButton.setOnClickListener {
ShareUtils.openUrlInBrowser(context, errorInfo.request)
}
}
fun showTextError(errorString: String) {
ensureDefaultVisibility()
@@ -192,27 +136,5 @@ class ErrorPanelHelper(
companion object {
val TAG: String = ErrorPanelHelper::class.simpleName!!
val DEBUG: Boolean = MainActivity.DEBUG
@StringRes
fun getExceptionDescription(throwable: Throwable?): Int {
return when (throwable) {
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
is GeographicRestrictionException -> R.string.georestricted_content
is PaidContentException -> R.string.paid_content
is PrivateContentException -> R.string.private_content
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
is ContentNotAvailableException -> R.string.content_not_available
is ContentNotSupportedException -> R.string.content_not_supported
else -> {
// show retry button only for content which is not unavailable or unsupported
if (throwable != null && throwable.isNetworkRelated) {
R.string.network_error
} else {
R.string.error_snackbar_message
}
}
}
}
}
}

View File

@@ -10,6 +10,7 @@ import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
@@ -121,7 +122,7 @@ class ErrorUtil {
)
.setSmallIcon(R.drawable.ic_bug_report)
.setContentTitle(context.getString(R.string.error_report_notification_title))
.setContentText(context.getString(errorInfo.messageStringId))
.setContentText(errorInfo.getMessage(context))
.setAutoCancel(true)
.setContentIntent(
PendingIntentCompat.getActivity(
@@ -136,9 +137,11 @@ class ErrorUtil {
NotificationManagerCompat.from(context)
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
// since the notification is silent, also show a toast, otherwise the user is confused
Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT)
.show()
ContextCompat.getMainExecutor(context).execute {
// since the notification is silent, also show a toast, otherwise the user is confused
Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT)
.show()
}
}
private fun getErrorActivityIntent(context: Context, errorInfo: ErrorInfo): Intent {
@@ -153,10 +156,10 @@ class ErrorUtil {
// fallback to showing a notification if no root view is available
createNotification(context, errorInfo)
} else {
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
Snackbar.make(rootView, errorInfo.getMessage(context), Snackbar.LENGTH_LONG)
.setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).uppercase()) {
openActivity(context, errorInfo)
context.startActivity(getErrorActivityIntent(context, errorInfo))
}.show()
}
}

View File

@@ -33,7 +33,9 @@ public enum UserAction {
SHARE_TO_NEWPIPE("share to newpipe"),
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
OPEN_INFO_ITEM_DIALOG("open info item dialog"),
GETTING_MAIN_SCREEN_TAB("getting main screen tab");
GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
PLAY_ON_POPUP("play on popup"),
SUBSCRIPTIONS("loading subscriptions");
private final String message;

View File

@@ -93,6 +93,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.OnKeyDownListener;
@@ -876,7 +877,7 @@ public final class VideoDetailFragment
}
}
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
url == null ? "no url" : url, serviceId)));
url == null ? "no url" : url, serviceId, url)));
}
/*//////////////////////////////////////////////////////////////////////////
@@ -1166,8 +1167,12 @@ public final class VideoDetailFragment
final PlayQueue queue = setupPlayQueueForIntent(false);
tryAddVideoPlayerView();
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
PlayerService.class, queue, true, autoPlayEnabled);
final Context context = requireContext();
final Intent playerIntent =
NavigationHelper.getPlayerIntent(context, PlayerService.class, queue,
PlayerIntentType.AllOthers)
.putExtra(Player.PLAY_WHEN_READY, autoPlayEnabled)
.putExtra(Player.RESUME_PLAYBACK, true);
ContextCompat.startForegroundService(activity, playerIntent);
}
@@ -1411,10 +1416,8 @@ public final class VideoDetailFragment
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
// Rebound to the service if it was closed via notification or mini player
if (!playerHolder.isBound()) {
playerHolder.startService(
false, VideoDetailFragment.this);
}
playerHolder.setListener(VideoDetailFragment.this);
playerHolder.tryBindIfNeeded(context);
break;
}
}
@@ -1423,7 +1426,8 @@ public final class VideoDetailFragment
intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER);
intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER);
intentFilter.addAction(ACTION_PLAYER_STARTED);
activity.registerReceiver(broadcastReceiver, intentFilter);
ContextCompat.registerReceiver(activity, broadcastReceiver, intentFilter,
ContextCompat.RECEIVER_EXPORTED);
}
@@ -1592,8 +1596,8 @@ public final class VideoDetailFragment
}
if (!info.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(info.getErrors(),
UserAction.REQUESTED_STREAM, info.getUrl(), info));
showSnackBarError(new ErrorInfo(info.getErrors(), UserAction.REQUESTED_STREAM,
"Some info not extracted: " + info.getUrl(), info));
}
}

View File

@@ -153,7 +153,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
handleResult(result);
}, throwable ->
showError(new ErrorInfo(throwable, errorUserAction,
"Start loading: " + url, serviceId)));
"Start loading: " + url, serviceId, url)));
}
/**
@@ -184,7 +184,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
handleNextItems(infoItemsPage);
}, (@NonNull Throwable throwable) ->
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
errorUserAction, "Loading more items: " + url, serviceId)));
errorUserAction, "Loading more items: " + url, serviceId, url)));
}
private void forbidDownwardFocusScroll() {
@@ -210,7 +210,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
if (!result.getErrors().isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
"Get next items of: " + url, serviceId));
"Get next items of: " + url, serviceId, url));
}
}
@@ -250,7 +250,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
if (!errors.isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
errorUserAction, "Start loading: " + url, serviceId));
errorUserAction, "Start loading: " + url, serviceId, url));
}
}
}

View File

@@ -577,7 +577,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
isLoading.set(false);
handleResult(result);
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
url == null ? "No URL" : url, serviceId)));
url == null ? "No URL" : url, serviceId, url)));
}
@Override

View File

@@ -54,6 +54,7 @@ import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
@@ -934,7 +935,21 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
infoListAdapter.clearStreamItemList();
showEmptyState();
} else {
showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId));
showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId,
getOpenInBrowserUrlForErrors()));
}
}
@Nullable
private String getOpenInBrowserUrlForErrors() {
if (TextUtils.isEmpty(searchString)) {
return null;
}
try {
return service.getSearchQHFactory().getUrl(searchString,
Arrays.asList(contentFilter), sortFilter);
} catch (final NullPointerException | ParsingException ignored) {
return null;
}
}
@@ -1022,7 +1037,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
&& !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
searchString, serviceId));
searchString, serviceId, getOpenInBrowserUrlForErrors()));
}
searchSuggestion = result.getSearchSuggestion();
@@ -1095,13 +1110,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// whose results are handled here, but let's check it anyway
if (nextPage == null) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
"\"" + searchString + "\" → nextPage == null", serviceId));
"\"" + searchString + "\" → nextPage == null", serviceId,
getOpenInBrowserUrlForErrors()));
} else {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ "pageIds: " + nextPage.getIds() + ", "
+ "pageCookies: " + nextPage.getCookies(),
serviceId));
serviceId, getOpenInBrowserUrlForErrors()));
}
}

View File

@@ -1,6 +1,8 @@
package org.schabi.newpipe.local.feed.notifications
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
@@ -83,7 +85,9 @@ class NotificationWorker(
.setPriority(NotificationCompat.PRIORITY_LOW)
.setContentTitle(applicationContext.getString(R.string.feed_notification_loading))
.build()
setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification))
// ServiceInfo constants are not used below Android Q, so 0 is set here
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0
setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification, serviceType))
}
companion object {

View File

@@ -31,6 +31,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable
@@ -200,7 +201,7 @@ class FeedLoadService : Service() {
}
}
}
registerReceiver(broadcastReceiver, IntentFilter(ACTION_CANCEL))
ContextCompat.registerReceiver(this, broadcastReceiver, IntentFilter(ACTION_CANCEL), ContextCompat.RECEIVER_NOT_EXPORTED)
}
// /////////////////////////////////////////////////////////////////////////

View File

@@ -89,8 +89,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
ErrorUtil.showSnackbar(activity,
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
ServiceHelper.getNameOfServiceById(currentServiceId),
"Service does not support importing subscriptions",
currentServiceId,
R.string.general_error));
activity.finish();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
package org.schabi.newpipe.player
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
// We model this as an enum class plus one struct for each enum value
// so we can consume it from Java properly. After converting to Kotlin,
// we could switch to a sealed enum class & a proper Kotlin `when` match.
enum class PlayerIntentType {
Enqueue,
EnqueueNext,
TimestampChange,
AllOthers
}
/**
* A timestamp on the given was clicked and we should switch the playing stream to it.
*/
@Parcelize
data class TimestampChangeData(
val serviceId: Int,
val url: String,
val seconds: Int
) : Parcelable

View File

@@ -40,6 +40,7 @@ import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl;
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.player.notification.NotificationUtil;
import org.schabi.newpipe.util.ThemeHelper;
import java.lang.ref.WeakReference;
@@ -156,23 +157,24 @@ public final class PlayerService extends MediaBrowserServiceCompat {
}
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& (player == null || player.getPlayQueue() == null)) {
/*
No need to process media button's actions if the player is not working, otherwise
the player service would strangely start with nothing to play
Stop the service in this case, which will be removed from the foreground and its
notification cancelled in its destruction
*/
if (player == null) {
// No need to process media button's actions or other system intents if the player is
// not running. However, since the current intent might have been issued by the system
// with `startForegroundService()` (for unknown reasons), we need to ensure that we post
// a (dummy) foreground notification, otherwise we'd incur in
// "Context.startForegroundService() did not then call Service.startForeground()". Then
// we stop the service again.
Log.d(TAG, "onStartCommand() got a useless intent, closing the service");
NotificationUtil.startForegroundWithDummyNotification(this);
destroyPlayerAndStopService();
return START_NOT_STICKY;
}
if (player != null) {
player.handleIntent(intent);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
}
final PlayerType oldPlayerType = player.getPlayerType();
player.handleIntent(intent);
player.handleIntentPost(oldPlayerType);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
return START_NOT_STICKY;
}

View File

@@ -1,32 +1,7 @@
package org.schabi.newpipe.player;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import android.content.Intent;
public enum PlayerType {
MAIN,
AUDIO,
POPUP;
/**
* @return an integer representing this {@link PlayerType}, to be used to save it in intents
* @see #retrieveFromIntent(Intent) Use retrieveFromIntent() to retrieve and convert player type
* integers from an intent
*/
public int valueForIntent() {
return ordinal();
}
/**
* @param intent the intent to retrieve a player type from
* @return the player type integer retrieved from the intent, converted back into a {@link
* PlayerType}, or {@link PlayerType#MAIN} if there is no player type extra in the
* intent
* @throws ArrayIndexOutOfBoundsException if the intent contains an invalid player type integer
* @see #valueForIntent() Use valueForIntent() to obtain valid player type integers
*/
public static PlayerType retrieveFromIntent(final Intent intent) {
return values()[intent.getIntExtra(PLAYER_TYPE, MAIN.valueForIntent())];
}
}

View File

@@ -154,9 +154,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
notifyAudioSessionUpdate(true, audioSessionId);
}
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
if (!PlayerHelper.isUsingDSP()) {
return;
}
final Intent intent = new Intent(active
? AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION
: AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);

View File

@@ -1,8 +1,5 @@
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.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;
@@ -25,7 +22,6 @@ import androidx.core.content.ContextCompat;
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.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
@@ -300,10 +296,6 @@ public final class PlayerHelper {
AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
}
public static boolean isUsingDSP() {
return true;
}
@NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
@@ -410,23 +402,9 @@ public final class PlayerHelper {
return singlePlayQueue;
}
// endregion
// region Utils used by player
@RepeatMode
public static int nextRepeatMode(@RepeatMode final int repeatMode) {
switch (repeatMode) {
case REPEAT_MODE_OFF:
return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL;
case REPEAT_MODE_ALL:
default:
return REPEAT_MODE_OFF;
}
}
@ResizeMode
public static int retrieveResizeModeFromPrefs(final Player player) {
return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),

View File

@@ -192,9 +192,11 @@ public final class PlayerHolder {
startPlayerListener();
// ^ will call listener.onPlayerConnected() down the line if there is an active player
// notify the main activity that binding the service has completed, so that it can
// open the bottom mini-player
NavigationHelper.sendPlayerStartedEvent(localBinder.getService());
if (playerService != null && playerService.getPlayer() != null) {
// notify the main activity that binding the service has completed and that there is
// a player, so that it can open the bottom mini-player
NavigationHelper.sendPlayerStartedEvent(localBinder.getService());
}
}
}

View File

@@ -17,6 +17,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.extractor.InfoItem.InfoType
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
@@ -84,7 +85,7 @@ class MediaBrowserPlaybackPreparer(
},
{ throwable ->
Log.e(TAG, "Failed to start playback of media ID [$mediaId]", throwable)
onPrepareError()
onPrepareError(throwable)
}
)
}
@@ -115,9 +116,9 @@ class MediaBrowserPlaybackPreparer(
)
}
private fun onPrepareError() {
private fun onPrepareError(throwable: Throwable) {
setMediaSessionError.accept(
ContextCompat.getString(context, R.string.error_snackbar_message),
ErrorInfo.getMessage(throwable, null, null).getString(context),
PlaybackStateCompat.ERROR_CODE_APP_ERROR
)
}

View File

@@ -147,18 +147,15 @@ internal class PackageValidator(context: Context) {
private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? {
val packageInfo = getPackageInfo(callingPackage) ?: return null
val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString()
val uid = packageInfo.applicationInfo.uid
val appName = packageInfo.applicationInfo?.loadLabel(packageManager).toString()
val uid = packageInfo.applicationInfo?.uid ?: -1
val signature = getSignature(packageInfo)
val requestedPermissions = packageInfo.requestedPermissions
val permissionFlags = packageInfo.requestedPermissionsFlags
val activePermissions = mutableSetOf<String>()
requestedPermissions?.forEachIndexed { index, permission ->
if (permissionFlags[index] and REQUESTED_PERMISSION_GRANTED != 0) {
activePermissions += permission
}
}
val requestedPermissions = packageInfo.requestedPermissions?.asSequence().orEmpty()
val permissionFlags = packageInfo.requestedPermissionsFlags?.asSequence().orEmpty()
val activePermissions = (requestedPermissions zip permissionFlags)
.filter { (permission, flag) -> flag and REQUESTED_PERMISSION_GRANTED != 0 }
.mapTo(mutableSetOf()) { (permission, flag) -> permission }
return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet())
}
@@ -189,12 +186,12 @@ internal class PackageValidator(context: Context) {
*/
@Suppress("deprecation")
private fun getSignature(packageInfo: PackageInfo): String? =
if (packageInfo.signatures == null || packageInfo.signatures.size != 1) {
if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) {
// Security best practices dictate that an app should be signed with exactly one (1)
// signature. Because of this, if there are multiple signatures, reject it.
null
} else {
val certificate = packageInfo.signatures[0].toByteArray()
val certificate = packageInfo.signatures!![0].toByteArray()
getSignatureSha256(certificate)
}

View File

@@ -5,7 +5,9 @@ import static androidx.media.app.NotificationCompat.MediaStyle;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap;
@@ -23,6 +25,8 @@ import androidx.core.content.ContextCompat;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerIntentType;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.NavigationHelper;
@@ -89,12 +93,9 @@ public final class NotificationUtil {
Log.d(TAG, "createNotification()");
}
notificationManager = NotificationManagerCompat.from(player.getContext());
final NotificationCompat.Builder builder =
new NotificationCompat.Builder(player.getContext(),
player.getContext().getString(R.string.notification_channel_id));
final MediaStyle mediaStyle = new MediaStyle();
// setup media style (compact notification slots and media session)
final MediaStyle mediaStyle = new MediaStyle();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// notification actions are ignored on Android 13+, and are replaced by code in
// MediaSessionPlayerUi
@@ -107,18 +108,9 @@ public final class NotificationUtil {
.ifPresent(mediaStyle::setMediaSession);
// setup notification builder
builder.setStyle(mediaStyle)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setShowWhen(false)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setColor(ContextCompat.getColor(player.getContext(),
R.color.dark_background_color))
final var builder = setupNotificationBuilder(player.getContext(), mediaStyle)
.setColorized(player.getPrefs().getBoolean(
player.getContext().getString(R.string.notification_colorize_key), true))
.setDeleteIntent(PendingIntentCompat.getBroadcast(player.getContext(),
NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT, false));
player.getContext().getString(R.string.notification_colorize_key), true));
// set the initial value for the video thumbnail, updatable with updateNotificationThumbnail
setLargeIcon(builder);
@@ -167,19 +159,17 @@ public final class NotificationUtil {
&& notificationBuilder.mActions.get(2).actionIntent != null);
}
public static void startForegroundWithDummyNotification(final PlayerService service) {
final var builder = setupNotificationBuilder(service, new MediaStyle());
startForeground(service, builder.build());
}
public void createNotificationAndStartForeground() {
if (notificationBuilder == null) {
notificationBuilder = createNotification();
}
updateNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
startForeground(player.getService(), notificationBuilder.build());
}
public void cancelNotificationAndStopForeground() {
@@ -193,6 +183,34 @@ public final class NotificationUtil {
}
/////////////////////////////////////////////////////
// STATIC FUNCTIONS IN COMMON BETWEEN DUMMY AND REAL NOTIFICATION
/////////////////////////////////////////////////////
private static NotificationCompat.Builder setupNotificationBuilder(final Context context,
final MediaStyle style) {
return new NotificationCompat.Builder(context,
context.getString(R.string.notification_channel_id))
.setStyle(style)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setShowWhen(false)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setColor(ContextCompat.getColor(context, R.color.dark_background_color))
.setDeleteIntent(PendingIntentCompat.getBroadcast(context,
NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT, false));
}
private static void startForeground(final PlayerService service,
final Notification notification) {
// ServiceInfo constants are not used below Android Q, so 0 is set here
final int serviceType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
? ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK : 0;
ServiceCompat.startForeground(service, NOTIFICATION_ID, notification, serviceType);
}
/////////////////////////////////////////////////////
// ACTIONS
/////////////////////////////////////////////////////
@@ -256,7 +274,9 @@ public final class NotificationUtil {
} else {
// We are playing in fragment. Don't open another activity just show fragment. That's it
final Intent intent = NavigationHelper.getPlayerIntent(
player.getContext(), MainActivity.class, null, true);
player.getContext(), MainActivity.class, null,
PlayerIntentType.AllOthers);
intent.putExtra(Player.RESUME_PLAYBACK, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);

Some files were not shown because too many files have changed in this diff Show More