mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-10-21 02:32:14 +02:00
Compare commits
80 Commits
master
...
buildImpro
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ee01ba3209 | ||
![]() |
1bef2fdc25 | ||
![]() |
061ce870ac | ||
![]() |
15089245bb | ||
![]() |
d99435c4ad | ||
![]() |
320c693636 | ||
![]() |
eee1172e8a | ||
![]() |
0ebd01e65e | ||
![]() |
965eea2124 | ||
![]() |
59dfdda95e | ||
![]() |
3a2d427a46 | ||
![]() |
c25f83da6c | ||
![]() |
e2026dc378 | ||
![]() |
00f6203904 | ||
![]() |
980e8f3951 | ||
![]() |
4e9a480fdd | ||
![]() |
aa2b4821e2 | ||
![]() |
92a07a3445 | ||
![]() |
eed09f8a1d | ||
![]() |
fd3f030d0b | ||
![]() |
45c22c0db8 | ||
![]() |
2b7c72eb69 | ||
![]() |
89c4eb5237 | ||
![]() |
803aba4935 | ||
![]() |
1723bf0e8a | ||
![]() |
21e24c9e34 | ||
![]() |
eb277fe14b | ||
![]() |
d77771a60c | ||
![]() |
01f9a3de33 | ||
![]() |
150649aea9 | ||
![]() |
3803d49489 | ||
![]() |
25a4a9a253 | ||
![]() |
d534946550 | ||
![]() |
8fb3e90fe1 | ||
![]() |
5750ef6aa8 | ||
![]() |
ab7d1377e5 | ||
![]() |
fd24c08529 | ||
![]() |
e14ec3a4f9 | ||
![]() |
b592403a66 | ||
![]() |
90e1ac56ef | ||
![]() |
32eb3afe16 | ||
![]() |
83a0abddcc | ||
![]() |
35c7f2f5d1 | ||
![]() |
8afb00d2f0 | ||
![]() |
f27ec53c08 | ||
![]() |
a3ddd616f9 | ||
![]() |
79980e2078 | ||
![]() |
b204fad9d5 | ||
![]() |
08f51abefb | ||
![]() |
204df4c45a | ||
![]() |
989c0cfd28 | ||
![]() |
a369deeef4 | ||
![]() |
1bde2dcd9f | ||
![]() |
29a3ca83b5 | ||
![]() |
38064be702 | ||
![]() |
d17eae9bad | ||
![]() |
74562db965 | ||
![]() |
386d5197d8 | ||
![]() |
ccd76dea1f | ||
![]() |
9282cce6a8 | ||
![]() |
7644066c5a | ||
![]() |
9bc8139b8c | ||
![]() |
ff3526b28d | ||
![]() |
d6c0dc32d1 | ||
![]() |
124ab56c5f | ||
![]() |
56f79fac13 | ||
![]() |
ef29c318b0 | ||
![]() |
9f11db8e06 | ||
![]() |
fece0741e5 | ||
![]() |
b9b47fc520 | ||
![]() |
59db955493 | ||
![]() |
22a709d53b | ||
![]() |
329d76c857 | ||
![]() |
9f526e8e8f | ||
![]() |
50caba6606 | ||
![]() |
26443f9f14 | ||
![]() |
366129eee2 | ||
![]() |
4c8d44b6ba | ||
![]() |
14cd562ebd | ||
![]() |
04ef608f7a |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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:
|
||||
|
360
app/build.gradle
360
app/build.gradle
File diff suppressed because it is too large
Load Diff
297
app/build.gradle.kts
Normal file
297
app/build.gradle.kts
Normal file
File diff suppressed because it is too large
Load Diff
59
app/check-dependencies.gradle.kts
Normal file
59
app/check-dependencies.gradle.kts
Normal 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") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
@@ -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 -->
|
||||
|
@@ -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.
|
||||
|
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
@@ -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
@@ -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
|
@@ -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;
|
||||
}
|
||||
|
@@ -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())];
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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),
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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
Reference in New Issue
Block a user