mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-10-03 09:49:21 +02:00
Merge branch 'dev' into Core-i18n
This commit is contained in:
commit
48ed95fc9b
26 changed files with 466 additions and 380 deletions
|
@ -58,10 +58,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.StreamingService.LinkType;
|
import org.schabi.newpipe.extractor.StreamingService.LinkType;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
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.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
@ -253,7 +250,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
showUnsupportedUrlDialog(url);
|
showUnsupportedUrlDialog(url);
|
||||||
}
|
}
|
||||||
}, throwable -> handleError(this, new ErrorInfo(throwable,
|
}, 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))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -262,23 +260,19 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
* @param errorInfo the error information
|
* @param errorInfo the error information
|
||||||
*/
|
*/
|
||||||
private static void handleError(final Context context, final ErrorInfo errorInfo) {
|
private static void handleError(final Context context, final ErrorInfo errorInfo) {
|
||||||
if (errorInfo.getThrowable() != null) {
|
if (errorInfo.getRecaptchaUrl() != null) {
|
||||||
errorInfo.getThrowable().printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
|
|
||||||
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||||
// Starting ReCaptcha Challenge Activity
|
// Starting ReCaptcha Challenge Activity
|
||||||
final Intent intent = new Intent(context, ReCaptchaActivity.class);
|
final Intent intent = new Intent(context, ReCaptchaActivity.class);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.getRecaptchaUrl());
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
} else if (errorInfo.getThrowable() instanceof ContentNotAvailableException
|
} else if (errorInfo.isReportable()) {
|
||||||
|| errorInfo.getThrowable() instanceof ContentNotSupportedException) {
|
ErrorUtil.createNotification(context, errorInfo);
|
||||||
|
} else {
|
||||||
// this exception does not usually indicate a problem that should be reported,
|
// this exception does not usually indicate a problem that should be reported,
|
||||||
// so just show a toast instead of the notification
|
// so just show a toast instead of the notification
|
||||||
Toast.makeText(context, errorInfo.getMessage(context), Toast.LENGTH_LONG).show();
|
Toast.makeText(context, errorInfo.getMessage(context), Toast.LENGTH_LONG).show();
|
||||||
} else {
|
|
||||||
ErrorUtil.createNotification(context, errorInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context instanceof RouterActivity) {
|
if (context instanceof RouterActivity) {
|
||||||
|
@ -641,7 +635,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
finish();
|
finish();
|
||||||
}, throwable -> handleError(this, new ErrorInfo(throwable,
|
}, 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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -828,10 +823,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
})
|
})
|
||||||
)),
|
)),
|
||||||
throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo(
|
throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo(
|
||||||
throwable,
|
throwable, UserAction.REQUESTED_STREAM,
|
||||||
UserAction.REQUESTED_STREAM,
|
|
||||||
"Tried to add " + currentUrl + " to a playlist",
|
"Tried to add " + currentUrl + " to a playlist",
|
||||||
((RouterActivity) ctx).currentService.getServiceId())
|
((RouterActivity) ctx).currentService.getServiceId(),
|
||||||
|
currentUrl)
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -971,7 +966,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
|
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
|
||||||
choice.url + " opened with " + choice.playerChoice,
|
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,
|
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||||
"Downloading video stream size",
|
"Downloading video stream size", currentInfo))));
|
||||||
currentInfo.getServiceId()))));
|
|
||||||
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
|
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||||
|
@ -399,8 +398,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
}, throwable -> ErrorUtil.showSnackbar(context,
|
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||||
"Downloading audio stream size",
|
"Downloading audio stream size", currentInfo))));
|
||||||
currentInfo.getServiceId()))));
|
|
||||||
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
|
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||||
|
@ -409,8 +407,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
}, throwable -> ErrorUtil.showSnackbar(context,
|
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||||
"Downloading subtitle stream size",
|
"Downloading subtitle stream size", currentInfo))));
|
||||||
currentInfo.getServiceId()))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupAudioTrackSpinner() {
|
private void setupAudioTrackSpinner() {
|
||||||
|
|
|
@ -36,8 +36,8 @@ public class AcraReportSender implements ReportSender {
|
||||||
ErrorUtil.openActivity(context, new ErrorInfo(
|
ErrorUtil.openActivity(context, new ErrorInfo(
|
||||||
new String[]{report.getString(ReportField.STACK_TRACE)},
|
new String[]{report.getString(ReportField.STACK_TRACE)},
|
||||||
UserAction.UI_ERROR,
|
UserAction.UI_ERROR,
|
||||||
null,
|
|
||||||
"ACRA report",
|
"ACRA report",
|
||||||
|
null,
|
||||||
R.string.app_ui_crash));
|
R.string.app_ui_crash));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import androidx.core.content.ContextCompat
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException
|
import com.google.android.exoplayer2.ExoPlaybackException
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource
|
import com.google.android.exoplayer2.upstream.HttpDataSource
|
||||||
import com.google.android.exoplayer2.upstream.Loader
|
import com.google.android.exoplayer2.upstream.Loader
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.Info
|
import org.schabi.newpipe.extractor.Info
|
||||||
|
@ -29,70 +28,108 @@ import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentExcepti
|
||||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||||
import org.schabi.newpipe.player.mediasource.FailedMediaSource
|
import org.schabi.newpipe.player.mediasource.FailedMediaSource
|
||||||
import org.schabi.newpipe.player.resolver.PlaybackResolver
|
import org.schabi.newpipe.player.resolver.PlaybackResolver
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error has occurred in the app. This class contains plain old parcelable data that can be used
|
||||||
|
* to report the error and to show it to the user along with correct action buttons.
|
||||||
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class ErrorInfo private constructor(
|
class ErrorInfo private constructor(
|
||||||
val stackTraces: Array<String>,
|
val stackTraces: Array<String>,
|
||||||
val userAction: UserAction,
|
val userAction: UserAction,
|
||||||
val serviceId: Int?,
|
|
||||||
val request: String,
|
val request: String,
|
||||||
|
val serviceId: Int?,
|
||||||
private val message: ErrorMessage,
|
private val message: ErrorMessage,
|
||||||
|
/**
|
||||||
|
* If `true`, a report button will be shown for this error. Otherwise the error is not something
|
||||||
|
* that can really be reported (e.g. a network issue, or content not being available at all).
|
||||||
|
*/
|
||||||
|
val isReportable: Boolean,
|
||||||
|
/**
|
||||||
|
* If `true`, the process causing this error can be retried, otherwise not.
|
||||||
|
*/
|
||||||
|
val isRetryable: Boolean,
|
||||||
|
/**
|
||||||
|
* If present, indicates that the exception was a ReCaptchaException, and this is the URL
|
||||||
|
* provided by the service that can be used to solve the ReCaptcha challenge.
|
||||||
|
*/
|
||||||
|
val recaptchaUrl: String?,
|
||||||
|
/**
|
||||||
|
* If present, this resource can alternatively be opened in browser (useful if NewPipe is
|
||||||
|
* badly broken).
|
||||||
|
*/
|
||||||
|
val openInBrowserUrl: String?,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
// no need to store throwable, all data for report is in other variables
|
@JvmOverloads
|
||||||
// also, the throwable might not be serializable, see TeamNewPipe/NewPipe#7302
|
constructor(
|
||||||
@IgnoredOnParcel
|
|
||||||
var throwable: Throwable? = null
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
throwable: Throwable,
|
throwable: Throwable,
|
||||||
userAction: UserAction,
|
userAction: UserAction,
|
||||||
serviceId: Int?,
|
request: String,
|
||||||
request: String
|
serviceId: Int? = null,
|
||||||
|
openInBrowserUrl: String? = null,
|
||||||
) : this(
|
) : this(
|
||||||
throwableToStringList(throwable),
|
throwableToStringList(throwable),
|
||||||
userAction,
|
userAction,
|
||||||
serviceId,
|
|
||||||
request,
|
request,
|
||||||
getMessage(throwable, userAction, serviceId)
|
serviceId,
|
||||||
) {
|
getMessage(throwable, userAction, serviceId),
|
||||||
this.throwable = throwable
|
isReportable(throwable),
|
||||||
}
|
isRetryable(throwable),
|
||||||
|
(throwable as? ReCaptchaException)?.url,
|
||||||
|
openInBrowserUrl,
|
||||||
|
)
|
||||||
|
|
||||||
private constructor(
|
@JvmOverloads
|
||||||
throwable: List<Throwable>,
|
constructor(
|
||||||
|
throwables: List<Throwable>,
|
||||||
userAction: UserAction,
|
userAction: UserAction,
|
||||||
serviceId: Int?,
|
request: String,
|
||||||
request: String
|
serviceId: Int? = null,
|
||||||
|
openInBrowserUrl: String? = null,
|
||||||
) : this(
|
) : this(
|
||||||
throwableListToStringList(throwable),
|
throwableListToStringList(throwables),
|
||||||
userAction,
|
userAction,
|
||||||
serviceId,
|
|
||||||
request,
|
request,
|
||||||
getMessage(throwable.firstOrNull(), userAction, serviceId)
|
serviceId,
|
||||||
) {
|
getMessage(throwables.firstOrNull(), userAction, serviceId),
|
||||||
this.throwable = throwable.firstOrNull()
|
throwables.any(::isReportable),
|
||||||
}
|
throwables.isEmpty() || throwables.any(::isRetryable),
|
||||||
|
throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url,
|
||||||
|
openInBrowserUrl,
|
||||||
|
)
|
||||||
|
|
||||||
// constructor to manually build ErrorInfo
|
// constructor to manually build ErrorInfo when no throwable is available
|
||||||
constructor(stackTraces: Array<String>, userAction: UserAction, serviceId: Int?, request: String, @StringRes message: Int) :
|
constructor(
|
||||||
this(stackTraces, userAction, serviceId, request, ErrorMessage(message))
|
stackTraces: Array<String>,
|
||||||
|
userAction: UserAction,
|
||||||
|
request: String,
|
||||||
|
serviceId: Int?,
|
||||||
|
@StringRes message: Int
|
||||||
|
) :
|
||||||
|
this(
|
||||||
|
stackTraces, userAction, request, serviceId, ErrorMessage(message),
|
||||||
|
true, false, null, null
|
||||||
|
)
|
||||||
|
|
||||||
// constructors with single throwable
|
// constructor with only one throwable to extract service id and openInBrowserUrl from an Info
|
||||||
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
constructor(
|
||||||
this(throwable, userAction, null, request)
|
throwable: Throwable,
|
||||||
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
|
userAction: UserAction,
|
||||||
this(throwable, userAction, serviceId, request)
|
request: String,
|
||||||
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
|
info: Info?,
|
||||||
this(throwable, userAction, info?.serviceId, request)
|
) :
|
||||||
|
this(throwable, userAction, request, info?.serviceId, info?.url)
|
||||||
|
|
||||||
// constructors with list of throwables
|
// constructor with multiple throwables to extract service id and openInBrowserUrl from an Info
|
||||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
|
constructor(
|
||||||
this(throwable, userAction, null, request)
|
throwables: List<Throwable>,
|
||||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
|
userAction: UserAction,
|
||||||
this(throwable, userAction, serviceId, request)
|
request: String,
|
||||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
|
info: Info?,
|
||||||
this(throwable, userAction, info?.serviceId, request)
|
) :
|
||||||
|
this(throwables, userAction, request, info?.serviceId, info?.url)
|
||||||
|
|
||||||
fun getServiceName(): String {
|
fun getServiceName(): String {
|
||||||
return getServiceName(serviceId)
|
return getServiceName(serviceId)
|
||||||
|
@ -205,8 +242,7 @@ class ErrorInfo private constructor(
|
||||||
// other extractor exceptions
|
// other extractor exceptions
|
||||||
throwable is ContentNotSupportedException ->
|
throwable is ContentNotSupportedException ->
|
||||||
ErrorMessage(R.string.content_not_supported)
|
ErrorMessage(R.string.content_not_supported)
|
||||||
// ReCaptchas should have already been handled elsewhere,
|
// ReCaptchas will be handled in a special way anyway
|
||||||
// but return an error message here just in case
|
|
||||||
throwable is ReCaptchaException ->
|
throwable is ReCaptchaException ->
|
||||||
ErrorMessage(R.string.recaptcha_request_toast)
|
ErrorMessage(R.string.recaptcha_request_toast)
|
||||||
// test this at the end as many exceptions could be a subclass of IOException
|
// test this at the end as many exceptions could be a subclass of IOException
|
||||||
|
@ -234,5 +270,36 @@ class ErrorInfo private constructor(
|
||||||
ErrorMessage(R.string.error_snackbar_message)
|
ErrorMessage(R.string.error_snackbar_message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isReportable(throwable: Throwable?): Boolean {
|
||||||
|
return when (throwable) {
|
||||||
|
// we don't have an exception, so this is a manually built error, which likely
|
||||||
|
// indicates that it's important and is thus reportable
|
||||||
|
null -> true
|
||||||
|
// the service explicitly said that content is not available (e.g. age restrictions,
|
||||||
|
// video deleted, etc.), there is no use in letting users report it
|
||||||
|
is ContentNotAvailableException -> false
|
||||||
|
// we know the content is not supported, no need to let the user report it
|
||||||
|
is ContentNotSupportedException -> false
|
||||||
|
// happens often when there is no internet connection; we don't use
|
||||||
|
// `throwable.isNetworkRelated` since any `IOException` would make that function
|
||||||
|
// return true, but not all `IOException`s are network related
|
||||||
|
is UnknownHostException -> false
|
||||||
|
// by default, this is an unexpected exception, which the user could report
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRetryable(throwable: Throwable?): Boolean {
|
||||||
|
return when (throwable) {
|
||||||
|
// we know the content is not available, retrying won't help
|
||||||
|
is ContentNotAvailableException -> false
|
||||||
|
// we know the content is not supported, retrying won't help
|
||||||
|
is ContentNotSupportedException -> false
|
||||||
|
// by default (including if throwable is null), enable retrying (though the retry
|
||||||
|
// button will be shown only if a way to perform the retry is implemented)
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.error
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -14,11 +13,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import org.schabi.newpipe.MainActivity
|
import org.schabi.newpipe.MainActivity
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.ktx.isInterruptedCaused
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -68,50 +63,32 @@ class ErrorPanelHelper(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showError(errorInfo: ErrorInfo) {
|
fun showError(errorInfo: ErrorInfo) {
|
||||||
|
|
||||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureDefaultVisibility()
|
ensureDefaultVisibility()
|
||||||
|
errorTextView.text = errorInfo.getMessage(context)
|
||||||
|
|
||||||
if (errorInfo.throwable is ReCaptchaException) {
|
if (errorInfo.recaptchaUrl != null) {
|
||||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
showAndSetErrorButtonAction(R.string.recaptcha_solve) {
|
||||||
|
|
||||||
showAndSetErrorButtonAction(
|
|
||||||
R.string.recaptcha_solve
|
|
||||||
) {
|
|
||||||
// Starting ReCaptcha Challenge Activity
|
// Starting ReCaptcha Challenge Activity
|
||||||
val intent = Intent(context, ReCaptchaActivity::class.java)
|
val intent = Intent(context, ReCaptchaActivity::class.java)
|
||||||
intent.putExtra(
|
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.recaptchaUrl)
|
||||||
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
|
|
||||||
(errorInfo.throwable as ReCaptchaException).url
|
|
||||||
)
|
|
||||||
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
||||||
errorActionButton.setOnClickListener(null)
|
errorActionButton.setOnClickListener(null)
|
||||||
}
|
}
|
||||||
|
} else if (errorInfo.isReportable) {
|
||||||
errorRetryButton.isVisible = retryShouldBeShown
|
showAndSetErrorButtonAction(R.string.error_snackbar_action) {
|
||||||
showAndSetOpenInBrowserButtonAction(errorInfo)
|
|
||||||
} else {
|
|
||||||
showAndSetErrorButtonAction(
|
|
||||||
R.string.error_snackbar_action
|
|
||||||
) {
|
|
||||||
ErrorUtil.openActivity(context, errorInfo)
|
ErrorUtil.openActivity(context, errorInfo)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
errorTextView.text = errorInfo.getMessage(context)
|
if (errorInfo.isRetryable) {
|
||||||
|
errorRetryButton.isVisible = retryShouldBeShown
|
||||||
|
}
|
||||||
|
|
||||||
if (errorInfo.throwable !is ContentNotAvailableException &&
|
if (errorInfo.openInBrowserUrl != null) {
|
||||||
errorInfo.throwable !is ContentNotSupportedException
|
errorOpenInBrowserButton.isVisible = true
|
||||||
) {
|
errorOpenInBrowserButton.setOnClickListener {
|
||||||
// show retry button only for content which is not unavailable or unsupported
|
ShareUtils.openUrlInBrowser(context, errorInfo.openInBrowserUrl)
|
||||||
errorRetryButton.isVisible = retryShouldBeShown
|
|
||||||
}
|
}
|
||||||
showAndSetOpenInBrowserButtonAction(errorInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setRootVisible()
|
setRootVisible()
|
||||||
|
@ -129,15 +106,6 @@ class ErrorPanelHelper(
|
||||||
errorActionButton.setOnClickListener(listener)
|
errorActionButton.setOnClickListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showAndSetOpenInBrowserButtonAction(
|
|
||||||
errorInfo: ErrorInfo
|
|
||||||
) {
|
|
||||||
errorOpenInBrowserButton.isVisible = true
|
|
||||||
errorOpenInBrowserButton.setOnClickListener {
|
|
||||||
ShareUtils.openUrlInBrowser(context, errorInfo.request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showTextError(errorString: String) {
|
fun showTextError(errorString: String) {
|
||||||
ensureDefaultVisibility()
|
ensureDefaultVisibility()
|
||||||
|
|
||||||
|
|
|
@ -93,6 +93,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerIntentType;
|
||||||
import org.schabi.newpipe.player.PlayerService;
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
|
@ -876,7 +877,7 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
}, 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);
|
final PlayQueue queue = setupPlayQueueForIntent(false);
|
||||||
tryAddVideoPlayerView();
|
tryAddVideoPlayerView();
|
||||||
|
|
||||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
final Context context = requireContext();
|
||||||
PlayerService.class, queue, true, autoPlayEnabled);
|
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);
|
ContextCompat.startForegroundService(activity, playerIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1593,8 +1598,8 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info.getErrors().isEmpty()) {
|
if (!info.getErrors().isEmpty()) {
|
||||||
showSnackBarError(new ErrorInfo(info.getErrors(),
|
showSnackBarError(new ErrorInfo(info.getErrors(), UserAction.REQUESTED_STREAM,
|
||||||
UserAction.REQUESTED_STREAM, info.getUrl(), info));
|
"Some info not extracted: " + info.getUrl(), info));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -153,7 +153,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||||
handleResult(result);
|
handleResult(result);
|
||||||
}, throwable ->
|
}, throwable ->
|
||||||
showError(new ErrorInfo(throwable, errorUserAction,
|
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);
|
handleNextItems(infoItemsPage);
|
||||||
}, (@NonNull Throwable throwable) ->
|
}, (@NonNull Throwable throwable) ->
|
||||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
|
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
|
||||||
errorUserAction, "Loading more items: " + url, serviceId)));
|
errorUserAction, "Loading more items: " + url, serviceId, url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forbidDownwardFocusScroll() {
|
private void forbidDownwardFocusScroll() {
|
||||||
|
@ -210,7 +210,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
|
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()) {
|
if (!errors.isEmpty()) {
|
||||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
|
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);
|
isLoading.set(false);
|
||||||
handleResult(result);
|
handleResult(result);
|
||||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
|
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
|
||||||
url == null ? "No URL" : url, serviceId)));
|
url == null ? "No URL" : url, serviceId, url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -54,6 +54,7 @@ import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
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.SearchExtractor;
|
||||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
|
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
|
||||||
|
@ -934,7 +935,21 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
showEmptyState();
|
showEmptyState();
|
||||||
} else {
|
} 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.size() == 1
|
||||||
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
|
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
|
||||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
|
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
|
||||||
searchString, serviceId));
|
searchString, serviceId, getOpenInBrowserUrlForErrors()));
|
||||||
}
|
}
|
||||||
|
|
||||||
searchSuggestion = result.getSearchSuggestion();
|
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
|
// whose results are handled here, but let's check it anyway
|
||||||
if (nextPage == null) {
|
if (nextPage == null) {
|
||||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
|
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
|
||||||
"\"" + searchString + "\" → nextPage == null", serviceId));
|
"\"" + searchString + "\" → nextPage == null", serviceId,
|
||||||
|
getOpenInBrowserUrlForErrors()));
|
||||||
} else {
|
} else {
|
||||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
|
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
|
||||||
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
|
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
|
||||||
+ "pageIds: " + nextPage.getIds() + ", "
|
+ "pageIds: " + nextPage.getIds() + ", "
|
||||||
+ "pageCookies: " + nextPage.getCookies(),
|
+ "pageCookies: " + nextPage.getCookies(),
|
||||||
serviceId));
|
serviceId, getOpenInBrowserUrlForErrors()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,8 +89,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||||
ErrorUtil.showSnackbar(activity,
|
ErrorUtil.showSnackbar(activity,
|
||||||
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
|
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
|
||||||
currentServiceId,
|
|
||||||
"Service does not support importing subscriptions",
|
"Service does not support importing subscriptions",
|
||||||
|
currentServiceId,
|
||||||
R.string.general_error));
|
R.string.general_error));
|
||||||
activity.finish();
|
activity.finish();
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,12 +24,12 @@ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJ
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP;
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP;
|
||||||
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
|
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||||
import static com.google.android.exoplayer2.Player.Listener;
|
import static com.google.android.exoplayer2.Player.Listener;
|
||||||
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||||
import static com.google.android.exoplayer2.Player.RepeatMode;
|
import static com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
|
||||||
|
@ -61,6 +61,7 @@ import android.view.LayoutInflater;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.content.IntentCompat;
|
||||||
import androidx.core.math.MathUtils;
|
import androidx.core.math.MathUtils;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
@ -109,6 +110,7 @@ import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
||||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
|
||||||
|
@ -118,6 +120,7 @@ import org.schabi.newpipe.player.ui.PlayerUiList;
|
||||||
import org.schabi.newpipe.player.ui.PopupPlayerUi;
|
import org.schabi.newpipe.player.ui.PopupPlayerUi;
|
||||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
|
@ -125,14 +128,17 @@ import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public final class Player implements PlaybackListener, Listener {
|
public final class Player implements PlaybackListener, Listener {
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
@ -154,15 +160,13 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
// Intent
|
// Intent
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static final String REPEAT_MODE = "repeat_mode";
|
|
||||||
public static final String PLAYBACK_QUALITY = "playback_quality";
|
public static final String PLAYBACK_QUALITY = "playback_quality";
|
||||||
public static final String PLAY_QUEUE_KEY = "play_queue_key";
|
public static final String PLAY_QUEUE_KEY = "play_queue_key";
|
||||||
public static final String ENQUEUE = "enqueue";
|
|
||||||
public static final String ENQUEUE_NEXT = "enqueue_next";
|
|
||||||
public static final String RESUME_PLAYBACK = "resume_playback";
|
public static final String RESUME_PLAYBACK = "resume_playback";
|
||||||
public static final String PLAY_WHEN_READY = "play_when_ready";
|
public static final String PLAY_WHEN_READY = "play_when_ready";
|
||||||
public static final String PLAYER_TYPE = "player_type";
|
public static final String PLAYER_TYPE = "player_type";
|
||||||
public static final String IS_MUTED = "is_muted";
|
public static final String PLAYER_INTENT_TYPE = "player_intent_type";
|
||||||
|
public static final String PLAYER_INTENT_DATA = "player_intent_data";
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Time constants
|
// Time constants
|
||||||
|
@ -247,6 +251,8 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
||||||
@NonNull
|
@NonNull
|
||||||
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
||||||
|
@NonNull
|
||||||
|
private final CompositeDisposable streamItemDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
// This is the only listener we need for thumbnail loading, since there is always at most only
|
// This is the only listener we need for thumbnail loading, since there is always at most only
|
||||||
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
|
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
|
||||||
|
@ -347,49 +353,121 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
|
|
||||||
@SuppressWarnings("MethodLength")
|
@SuppressWarnings("MethodLength")
|
||||||
public void handleIntent(@NonNull final Intent intent) {
|
public void handleIntent(@NonNull final Intent intent) {
|
||||||
// fail fast if no play queue was provided
|
final var playerIntentType = IntentCompat.getSerializableExtra(intent, PLAYER_INTENT_TYPE,
|
||||||
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
|
PlayerIntentType.class);
|
||||||
if (queueCache == null) {
|
if (playerIntentType == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class);
|
// TODO: this should be in the second switch below, but I’m not sure whether I
|
||||||
if (newQueue == null) {
|
// can move the initUIs stuff without breaking the setup for edge cases somehow.
|
||||||
return;
|
// when playing from a timestamp, keep the current player as-is.
|
||||||
|
if (playerIntentType != PlayerIntentType.TimestampChange) {
|
||||||
|
playerType = IntentCompat.getSerializableExtra(intent, PLAYER_TYPE, PlayerType.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
final PlayerType oldPlayerType = playerType;
|
|
||||||
playerType = PlayerType.retrieveFromIntent(intent);
|
|
||||||
initUIsForCurrentPlayerType();
|
initUIsForCurrentPlayerType();
|
||||||
// We need to setup audioOnly before super(), see "sourceOf"
|
|
||||||
isAudioOnly = audioPlayerSelected();
|
isAudioOnly = audioPlayerSelected();
|
||||||
|
|
||||||
if (intent.hasExtra(PLAYBACK_QUALITY)) {
|
if (intent.hasExtra(PLAYBACK_QUALITY)) {
|
||||||
videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
|
videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve enqueue intents
|
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
|
||||||
if (intent.getBooleanExtra(ENQUEUE, false) && playQueue != null) {
|
|
||||||
playQueue.append(newQueue.getStreams());
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Resolve enqueue next intents
|
switch (playerIntentType) {
|
||||||
} else if (intent.getBooleanExtra(ENQUEUE_NEXT, false) && playQueue != null) {
|
case Enqueue -> {
|
||||||
final int currentIndex = playQueue.getIndex();
|
if (playQueue != null) {
|
||||||
playQueue.append(newQueue.getStreams());
|
final PlayQueue newQueue = getPlayQueueFromCache(intent);
|
||||||
playQueue.move(playQueue.size() - 1, currentIndex + 1);
|
if (newQueue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playQueue.append(newQueue.getStreams());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This falls through to the old logic, there was no playQueue
|
||||||
|
// yet so we should start the player and add the new video
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EnqueueNext -> {
|
||||||
|
if (playQueue != null) {
|
||||||
|
final PlayQueue newQueue = getPlayQueueFromCache(intent);
|
||||||
|
if (newQueue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final PlayQueueItem newItem = newQueue.getStreams().get(0);
|
||||||
|
newQueue.enqueueNext(newItem, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This falls through to the old logic, there was no playQueue
|
||||||
|
// yet so we should start the player and add the new video
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TimestampChange -> {
|
||||||
|
final var data = Objects.requireNonNull(IntentCompat.getParcelableExtra(intent,
|
||||||
|
PLAYER_INTENT_DATA, TimestampChangeData.class));
|
||||||
|
final Single<StreamInfo> single =
|
||||||
|
ExtractorHelper.getStreamInfo(data.getServiceId(), data.getUrl(), false);
|
||||||
|
streamItemDisposable.add(single.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(info -> {
|
||||||
|
final @Nullable PlayQueue oldPlayQueue = playQueue;
|
||||||
|
info.setStartPosition(data.getSeconds());
|
||||||
|
final PlayQueueItem playQueueItem = new PlayQueueItem(info);
|
||||||
|
|
||||||
|
// If the stream is already playing,
|
||||||
|
// we can just seek to the appropriate timestamp
|
||||||
|
if (oldPlayQueue != null
|
||||||
|
&& playQueueItem.isSameItem(oldPlayQueue.getItem())) {
|
||||||
|
// Player can have state = IDLE when playback is stopped or failed
|
||||||
|
// and we should retry in this case
|
||||||
|
if (simpleExoPlayer.getPlaybackState()
|
||||||
|
== com.google.android.exoplayer2.Player.STATE_IDLE) {
|
||||||
|
simpleExoPlayer.prepare();
|
||||||
|
}
|
||||||
|
simpleExoPlayer.seekTo(oldPlayQueue.getIndex(),
|
||||||
|
data.getSeconds() * 1000L);
|
||||||
|
simpleExoPlayer.setPlayWhenReady(playWhenReady);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
final PlayQueue newPlayQueue;
|
||||||
|
|
||||||
|
// If there is no queue yet, just add our item
|
||||||
|
if (oldPlayQueue == null) {
|
||||||
|
newPlayQueue = new SinglePlayQueue(playQueueItem);
|
||||||
|
|
||||||
|
// else we add the timestamped stream behind the current video
|
||||||
|
// and start playing it.
|
||||||
|
} else {
|
||||||
|
oldPlayQueue.enqueueNext(playQueueItem, true);
|
||||||
|
oldPlayQueue.offsetIndex(1);
|
||||||
|
newPlayQueue = oldPlayQueue;
|
||||||
|
}
|
||||||
|
initPlayback(newPlayQueue, playWhenReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, throwable -> {
|
||||||
|
// This will only show a snackbar if the passed context has a root view:
|
||||||
|
// otherwise it will resort to showing a notification, so we are safe
|
||||||
|
// here.
|
||||||
|
final var info = new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP,
|
||||||
|
data.getUrl(), null, data.getUrl());
|
||||||
|
ErrorUtil.createNotification(context, info);
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case AllOthers -> {
|
||||||
|
// fallthrough; TODO: put other intent data in separate cases
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final PlayQueue newQueue = getPlayQueueFromCache(intent);
|
||||||
|
if (newQueue == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
|
// branching parameters for below
|
||||||
final float playbackSpeed = savedParameters.speed;
|
|
||||||
final float playbackPitch = savedParameters.pitch;
|
|
||||||
final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString(
|
|
||||||
R.string.playback_skip_silence_key), getPlaybackSkipSilence());
|
|
||||||
|
|
||||||
final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue);
|
final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue);
|
||||||
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
|
|
||||||
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
|
|
||||||
final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted());
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TODO As seen in #7427 this does not work:
|
* TODO As seen in #7427 this does not work:
|
||||||
|
@ -404,7 +482,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
if (!exoPlayerIsNull()
|
if (!exoPlayerIsNull()
|
||||||
&& newQueue.size() == 1 && newQueue.getItem() != null
|
&& newQueue.size() == 1 && newQueue.getItem() != null
|
||||||
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
|
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
|
||||||
&& newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl())
|
&& newQueue.getItem().isSameItem(playQueue.getItem())
|
||||||
&& newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
|
&& newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
|
||||||
// Player can have state = IDLE when playback is stopped or failed
|
// Player can have state = IDLE when playback is stopped or failed
|
||||||
// and we should retry in this case
|
// and we should retry in this case
|
||||||
|
@ -430,7 +508,8 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
|
|
||||||
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false)
|
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false)
|
||||||
&& DependentPreferenceHelper.getResumePlaybackEnabled(context)
|
&& DependentPreferenceHelper.getResumePlaybackEnabled(context)
|
||||||
&& !samePlayQueue
|
// !samePlayQueue
|
||||||
|
&& (playQueue == null || !playQueue.equalStreamsAndIndex(newQueue))
|
||||||
&& !newQueue.isEmpty()
|
&& !newQueue.isEmpty()
|
||||||
&& newQueue.getItem() != null
|
&& newQueue.getItem() != null
|
||||||
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
|
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
|
||||||
|
@ -446,30 +525,30 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
newQueue.setRecovery(newQueue.getIndex(),
|
newQueue.setRecovery(newQueue.getIndex(),
|
||||||
state.getProgressMillis());
|
state.getProgressMillis());
|
||||||
}
|
}
|
||||||
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
initPlayback(newQueue, playWhenReady);
|
||||||
playbackSkipSilence, playWhenReady, isMuted);
|
|
||||||
},
|
},
|
||||||
error -> {
|
error -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.w(TAG, "Failed to start playback", error);
|
Log.w(TAG, "Failed to start playback", error);
|
||||||
}
|
}
|
||||||
// In case any error we can start playback without history
|
// In case any error we can start playback without history
|
||||||
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
initPlayback(newQueue, playWhenReady);
|
||||||
playbackSkipSilence, playWhenReady, isMuted);
|
|
||||||
},
|
},
|
||||||
() -> {
|
() -> {
|
||||||
// Completed but not found in history
|
// Completed but not found in history
|
||||||
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
initPlayback(newQueue, playWhenReady);
|
||||||
playbackSkipSilence, playWhenReady, isMuted);
|
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
// Good to go...
|
// Good to go...
|
||||||
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
|
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
|
||||||
initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed,
|
initPlayback(samePlayQueue ? playQueue : newQueue, playWhenReady);
|
||||||
playbackPitch, playbackSkipSilence, playWhenReady, isMuted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void handleIntentPost(final PlayerType oldPlayerType) {
|
||||||
if (oldPlayerType != playerType && playQueue != null) {
|
if (oldPlayerType != playerType && playQueue != null) {
|
||||||
// If playerType changes from one to another we should reload the player
|
// If playerType changes from one to another we should reload the player
|
||||||
// (to disable/enable video stream or to set quality)
|
// (to disable/enable video stream or to set quality)
|
||||||
|
@ -480,6 +559,19 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
NavigationHelper.sendPlayerStartedEvent(context);
|
NavigationHelper.sendPlayerStartedEvent(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static PlayQueue getPlayQueueFromCache(@NonNull final Intent intent) {
|
||||||
|
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
|
||||||
|
if (queueCache == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class);
|
||||||
|
if (newQueue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return newQueue;
|
||||||
|
}
|
||||||
|
|
||||||
private void initUIsForCurrentPlayerType() {
|
private void initUIsForCurrentPlayerType() {
|
||||||
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|
||||||
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
|
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
|
||||||
|
@ -513,16 +605,13 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPlayback(@NonNull final PlayQueue queue,
|
private void initPlayback(@NonNull final PlayQueue queue,
|
||||||
@RepeatMode final int repeatMode,
|
final boolean playOnReady) {
|
||||||
final float playbackSpeed,
|
|
||||||
final float playbackPitch,
|
|
||||||
final boolean playbackSkipSilence,
|
|
||||||
final boolean playOnReady,
|
|
||||||
final boolean isMuted) {
|
|
||||||
destroyPlayer();
|
destroyPlayer();
|
||||||
initPlayer(playOnReady);
|
initPlayer(playOnReady);
|
||||||
setRepeatMode(repeatMode);
|
final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString(
|
||||||
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
|
R.string.playback_skip_silence_key), getPlaybackSkipSilence());
|
||||||
|
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
|
||||||
|
setPlaybackParameters(savedParameters.speed, savedParameters.pitch, playbackSkipSilence);
|
||||||
|
|
||||||
playQueue = queue;
|
playQueue = queue;
|
||||||
playQueue.init();
|
playQueue.init();
|
||||||
|
@ -530,7 +619,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
|
|
||||||
UIs.call(PlayerUi::initPlayback);
|
UIs.call(PlayerUi::initPlayback);
|
||||||
|
|
||||||
simpleExoPlayer.setVolume(isMuted ? 0 : 1);
|
simpleExoPlayer.setVolume(isMuted() ? 0 : 1);
|
||||||
notifyQueueUpdateToListeners();
|
notifyQueueUpdateToListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -612,6 +701,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
|
|
||||||
databaseUpdateDisposable.clear();
|
databaseUpdateDisposable.clear();
|
||||||
progressUpdateDisposable.set(null);
|
progressUpdateDisposable.set(null);
|
||||||
|
streamItemDisposable.clear();
|
||||||
cancelLoadingCurrentThumbnail();
|
cancelLoadingCurrentThumbnail();
|
||||||
|
|
||||||
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
|
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
|
||||||
|
@ -1178,16 +1268,25 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
|
return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRepeatMode(@RepeatMode final int repeatMode) {
|
public void cycleNextRepeatMode() {
|
||||||
if (!exoPlayerIsNull()) {
|
if (!exoPlayerIsNull()) {
|
||||||
|
@RepeatMode final int repeatMode;
|
||||||
|
switch (simpleExoPlayer.getRepeatMode()) {
|
||||||
|
case REPEAT_MODE_OFF:
|
||||||
|
repeatMode = REPEAT_MODE_ONE;
|
||||||
|
break;
|
||||||
|
case REPEAT_MODE_ONE:
|
||||||
|
repeatMode = REPEAT_MODE_ALL;
|
||||||
|
break;
|
||||||
|
case REPEAT_MODE_ALL:
|
||||||
|
default:
|
||||||
|
repeatMode = REPEAT_MODE_OFF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
simpleExoPlayer.setRepeatMode(repeatMode);
|
simpleExoPlayer.setRepeatMode(repeatMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cycleNextRepeatMode() {
|
|
||||||
setRepeatMode(nextRepeatMode(getRepeatMode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -1290,7 +1389,8 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
UserAction.PLAY_STREAM,
|
UserAction.PLAY_STREAM,
|
||||||
"Loading failed for [" + currentMetadata.getTitle()
|
"Loading failed for [" + currentMetadata.getTitle()
|
||||||
+ "]: " + currentMetadata.getStreamUrl(),
|
+ "]: " + currentMetadata.getStreamUrl(),
|
||||||
currentMetadata.getServiceId());
|
currentMetadata.getServiceId(),
|
||||||
|
currentMetadata.getStreamUrl());
|
||||||
ErrorUtil.createNotification(context, errorInfo);
|
ErrorUtil.createNotification(context, errorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1506,7 +1606,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||||
"Player error[type=" + error.getErrorCodeName()
|
"Player error[type=" + error.getErrorCodeName()
|
||||||
+ "] occurred while playing " + currentMetadata.getStreamUrl(),
|
+ "] occurred while playing " + currentMetadata.getStreamUrl(),
|
||||||
currentMetadata.getServiceId());
|
currentMetadata.getServiceId(), currentMetadata.getStreamUrl());
|
||||||
}
|
}
|
||||||
ErrorUtil.createNotification(context, errorInfo);
|
ErrorUtil.createNotification(context, errorInfo);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -169,7 +169,9 @@ public final class PlayerService extends MediaBrowserServiceCompat {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
|
final PlayerType oldPlayerType = player.getPlayerType();
|
||||||
player.handleIntent(intent);
|
player.handleIntent(intent);
|
||||||
|
player.handleIntentPost(oldPlayerType);
|
||||||
player.UIs().get(MediaSessionPlayerUi.class)
|
player.UIs().get(MediaSessionPlayerUi.class)
|
||||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,7 @@
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
public enum PlayerType {
|
public enum PlayerType {
|
||||||
MAIN,
|
MAIN,
|
||||||
AUDIO,
|
AUDIO,
|
||||||
POPUP;
|
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())];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
package org.schabi.newpipe.player.helper;
|
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_ALWAYS;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||||
|
@ -25,7 +22,6 @@ import androidx.core.content.ContextCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
|
@ -410,23 +406,9 @@ public final class PlayerHelper {
|
||||||
return singlePlayQueue;
|
return singlePlayQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
// region Utils used by player
|
// 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
|
@ResizeMode
|
||||||
public static int retrieveResizeModeFromPrefs(final Player player) {
|
public static int retrieveResizeModeFromPrefs(final Player player) {
|
||||||
return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),
|
return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),
|
||||||
|
|
|
@ -23,6 +23,7 @@ import androidx.core.content.ContextCompat;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerIntentType;
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
@ -254,7 +255,9 @@ public final class NotificationUtil {
|
||||||
} else {
|
} else {
|
||||||
// We are playing in fragment. Don't open another activity just show fragment. That's it
|
// We are playing in fragment. Don't open another activity just show fragment. That's it
|
||||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
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.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.setAction(Intent.ACTION_MAIN);
|
intent.setAction(Intent.ACTION_MAIN);
|
||||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
|
|
|
@ -291,6 +291,22 @@ public abstract class PlayQueue implements Serializable {
|
||||||
broadcast(new AppendEvent(itemList.size()));
|
broadcast(new AppendEvent(itemList.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given item after the current stream.
|
||||||
|
*
|
||||||
|
* @param item item to add.
|
||||||
|
* @param skipIfSame if set, skip adding if the next stream is the same stream.
|
||||||
|
*/
|
||||||
|
public void enqueueNext(@NonNull final PlayQueueItem item, final boolean skipIfSame) {
|
||||||
|
final int currentIndex = getIndex();
|
||||||
|
// if the next item is the same item as the one we want to enqueue, skip if flag is true
|
||||||
|
if (skipIfSame && item.isSameItem(getItem(currentIndex + 1))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
append(List.of(item));
|
||||||
|
move(size() - 1, currentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the item at the given index from the play queue.
|
* Removes the item at the given index from the play queue.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -529,8 +545,7 @@ public abstract class PlayQueue implements Serializable {
|
||||||
final PlayQueueItem stream = streams.get(i);
|
final PlayQueueItem stream = streams.get(i);
|
||||||
final PlayQueueItem otherStream = other.streams.get(i);
|
final PlayQueueItem otherStream = other.streams.get(i);
|
||||||
// Check is based on serviceId and URL
|
// Check is based on serviceId and URL
|
||||||
if (stream.getServiceId() != otherStream.getServiceId()
|
if (!stream.isSameItem(otherStream)) {
|
||||||
|| !stream.getUrl().equals(otherStream.getUrl())) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ public class PlayQueueItem implements Serializable {
|
||||||
private long recoveryPosition;
|
private long recoveryPosition;
|
||||||
private Throwable error;
|
private Throwable error;
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
public PlayQueueItem(@NonNull final StreamInfo info) {
|
||||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||||
info.getThumbnails(), info.getUploaderName(),
|
info.getThumbnails(), info.getUploaderName(),
|
||||||
info.getUploaderUrl(), info.getStreamType());
|
info.getUploaderUrl(), info.getStreamType());
|
||||||
|
@ -71,6 +71,22 @@ public class PlayQueueItem implements Serializable {
|
||||||
this.recoveryPosition = RECOVERY_UNSET;
|
this.recoveryPosition = RECOVERY_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether these two items should be treated as the same stream
|
||||||
|
* for the sake of keeping the same player running when e.g. jumping between timestamps.
|
||||||
|
*
|
||||||
|
* @param other the {@link PlayQueueItem} to compare against.
|
||||||
|
* @return whether the two items are the same so the stream can be re-used.
|
||||||
|
*/
|
||||||
|
public boolean isSameItem(@Nullable final PlayQueueItem other) {
|
||||||
|
if (other == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// We assume that the same service & URL uniquely determines
|
||||||
|
// that we can keep the same stream running.
|
||||||
|
return serviceId == other.serviceId
|
||||||
|
&& url.equals(other.url);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
return title;
|
return title;
|
||||||
|
|
|
@ -16,7 +16,9 @@ public final class SinglePlayQueue extends PlayQueue {
|
||||||
public SinglePlayQueue(final StreamInfo info) {
|
public SinglePlayQueue(final StreamInfo info) {
|
||||||
super(0, List.of(new PlayQueueItem(info)));
|
super(0, List.of(new PlayQueueItem(info)));
|
||||||
}
|
}
|
||||||
|
public SinglePlayQueue(final PlayQueueItem item) {
|
||||||
|
super(0, List.of(item));
|
||||||
|
}
|
||||||
public SinglePlayQueue(final StreamInfo info, final long startPosition) {
|
public SinglePlayQueue(final StreamInfo info, final long startPosition) {
|
||||||
super(0, List.of(new PlayQueueItem(info)));
|
super(0, List.of(new PlayQueueItem(info)));
|
||||||
getItem().setRecoveryPosition(startPosition);
|
getItem().setRecoveryPosition(startPosition);
|
||||||
|
|
|
@ -57,8 +57,10 @@ import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
|
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
|
||||||
import org.schabi.newpipe.player.PlayQueueActivity;
|
import org.schabi.newpipe.player.PlayQueueActivity;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerIntentType;
|
||||||
import org.schabi.newpipe.player.PlayerService;
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
|
import org.schabi.newpipe.player.TimestampChangeData;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
@ -67,6 +69,7 @@ import org.schabi.newpipe.settings.SettingsActivity;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public final class NavigationHelper {
|
public final class NavigationHelper {
|
||||||
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
||||||
|
@ -85,54 +88,32 @@ public final class NavigationHelper {
|
||||||
public static <T> Intent getPlayerIntent(@NonNull final Context context,
|
public static <T> Intent getPlayerIntent(@NonNull final Context context,
|
||||||
@NonNull final Class<T> targetClazz,
|
@NonNull final Class<T> targetClazz,
|
||||||
@Nullable final PlayQueue playQueue,
|
@Nullable final PlayQueue playQueue,
|
||||||
final boolean resumePlayback) {
|
@NonNull final PlayerIntentType playerIntentType) {
|
||||||
final Intent intent = new Intent(context, targetClazz);
|
final String cacheKey = Optional.ofNullable(playQueue)
|
||||||
|
.map(queue -> SerializedCache.getInstance().put(queue, PlayQueue.class))
|
||||||
if (playQueue != null) {
|
.orElse(null);
|
||||||
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
|
return new Intent(context, targetClazz)
|
||||||
if (cacheKey != null) {
|
.putExtra(Player.PLAY_QUEUE_KEY, cacheKey)
|
||||||
intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey);
|
.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN)
|
||||||
}
|
.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true)
|
||||||
}
|
.putExtra(Player.PLAYER_INTENT_TYPE, playerIntentType);
|
||||||
intent.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN.valueForIntent());
|
|
||||||
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
|
|
||||||
intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true);
|
|
||||||
|
|
||||||
return intent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static <T> Intent getPlayerIntent(@NonNull final Context context,
|
public static Intent getPlayerTimestampIntent(@NonNull final Context context,
|
||||||
@NonNull final Class<T> targetClazz,
|
@NonNull final TimestampChangeData data) {
|
||||||
@Nullable final PlayQueue playQueue,
|
return new Intent(context, PlayerService.class)
|
||||||
final boolean resumePlayback,
|
.putExtra(Player.PLAYER_INTENT_TYPE, PlayerIntentType.TimestampChange)
|
||||||
final boolean playWhenReady) {
|
.putExtra(Player.PLAYER_INTENT_DATA, data);
|
||||||
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
|
|
||||||
.putExtra(Player.PLAY_WHEN_READY, playWhenReady);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static <T> Intent getPlayerEnqueueIntent(@NonNull final Context context,
|
|
||||||
@NonNull final Class<T> targetClazz,
|
|
||||||
@Nullable final PlayQueue playQueue) {
|
|
||||||
// when enqueueing `resumePlayback` is always `false` since:
|
|
||||||
// - if there is a video already playing, the value of `resumePlayback` just doesn't make
|
|
||||||
// any difference.
|
|
||||||
// - if there is nothing already playing, it is useful for the enqueue action to have a
|
|
||||||
// slightly different behaviour than the normal play action: the latter resumes playback,
|
|
||||||
// the former doesn't. (note that enqueue can be triggered when nothing is playing only
|
|
||||||
// by long pressing the video detail fragment, playlist or channel controls
|
|
||||||
return getPlayerIntent(context, targetClazz, playQueue, false)
|
|
||||||
.putExtra(Player.ENQUEUE, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static <T> Intent getPlayerEnqueueNextIntent(@NonNull final Context context,
|
public static <T> Intent getPlayerEnqueueNextIntent(@NonNull final Context context,
|
||||||
@NonNull final Class<T> targetClazz,
|
@NonNull final Class<T> targetClazz,
|
||||||
@Nullable final PlayQueue playQueue) {
|
@Nullable final PlayQueue playQueue) {
|
||||||
// see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false
|
return getPlayerIntent(context, targetClazz, playQueue, PlayerIntentType.EnqueueNext)
|
||||||
return getPlayerIntent(context, targetClazz, playQueue, false)
|
// see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false
|
||||||
.putExtra(Player.ENQUEUE_NEXT, true);
|
.putExtra(Player.RESUME_PLAYBACK, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PLAY */
|
/* PLAY */
|
||||||
|
@ -166,8 +147,10 @@ public final class NavigationHelper {
|
||||||
|
|
||||||
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
|
final var intent = getPlayerIntent(context, PlayerService.class, queue,
|
||||||
intent.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP.valueForIntent());
|
PlayerIntentType.AllOthers)
|
||||||
|
.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP)
|
||||||
|
.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
|
||||||
ContextCompat.startForegroundService(context, intent);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,8 +160,10 @@ public final class NavigationHelper {
|
||||||
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
|
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
|
||||||
.show();
|
.show();
|
||||||
|
|
||||||
final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
|
final Intent intent = getPlayerIntent(context, PlayerService.class, queue,
|
||||||
intent.putExtra(Player.PLAYER_TYPE, PlayerType.AUDIO.valueForIntent());
|
PlayerIntentType.AllOthers)
|
||||||
|
.putExtra(Player.PLAYER_TYPE, PlayerType.AUDIO)
|
||||||
|
.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
|
||||||
ContextCompat.startForegroundService(context, intent);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,9 +176,18 @@ public final class NavigationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
|
||||||
final Intent intent = getPlayerEnqueueIntent(context, PlayerService.class, queue);
|
|
||||||
|
|
||||||
intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent());
|
// when enqueueing `resumePlayback` is always `false` since:
|
||||||
|
// - if there is a video already playing, the value of `resumePlayback` just doesn't make
|
||||||
|
// any difference.
|
||||||
|
// - if there is nothing already playing, it is useful for the enqueue action to have a
|
||||||
|
// slightly different behaviour than the normal play action: the latter resumes playback,
|
||||||
|
// the former doesn't. (note that enqueue can be triggered when nothing is playing only
|
||||||
|
// by long pressing the video detail fragment, playlist or channel controls
|
||||||
|
final Intent intent = getPlayerIntent(context, PlayerService.class, queue,
|
||||||
|
PlayerIntentType.Enqueue)
|
||||||
|
.putExtra(Player.RESUME_PLAYBACK, false)
|
||||||
|
.putExtra(Player.PLAYER_TYPE, playerType);
|
||||||
ContextCompat.startForegroundService(context, intent);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,9 +209,8 @@ public final class NavigationHelper {
|
||||||
playerType = PlayerType.AUDIO;
|
playerType = PlayerType.AUDIO;
|
||||||
}
|
}
|
||||||
Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show();
|
||||||
final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue);
|
final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue)
|
||||||
|
.putExtra(Player.PLAYER_TYPE, playerType);
|
||||||
intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent());
|
|
||||||
ContextCompat.startForegroundService(context, intent);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ public final class SparseItemUtil {
|
||||||
callback.accept(result);
|
callback.accept(result);
|
||||||
}, throwable -> ErrorUtil.createNotification(context,
|
}, throwable -> ErrorUtil.createNotification(context,
|
||||||
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||||
"Loading stream info: " + url, serviceId)
|
"Loading stream info: " + url, serviceId, url)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +1,27 @@
|
||||||
package org.schabi.newpipe.util.text;
|
package org.schabi.newpipe.util.text;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.player.TimestampChangeData;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public final class InternalUrlsHandler {
|
public final class InternalUrlsHandler {
|
||||||
private static final String TAG = InternalUrlsHandler.class.getSimpleName();
|
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
|
||||||
|
|
||||||
private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)");
|
private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)");
|
||||||
private static final Pattern HASHTAG_TIMESTAMP_PATTERN =
|
|
||||||
Pattern.compile("(.*)#timestamp=(\\d+)");
|
|
||||||
|
|
||||||
private InternalUrlsHandler() {
|
private InternalUrlsHandler() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a YouTube timestamp comment URL in NewPipe.
|
|
||||||
* <p>
|
|
||||||
* This method will check if the provided url is a YouTube comment description URL ({@code
|
|
||||||
* https://www.youtube.com/watch?v=}video_id{@code #timestamp=}time_in_seconds). If yes, the
|
|
||||||
* popup player will be opened when the user will click on the timestamp in the comment,
|
|
||||||
* at the time and for the video indicated in the timestamp.
|
|
||||||
*
|
|
||||||
* @param disposables a field of the Activity/Fragment class that calls this method
|
|
||||||
* @param context the context to use
|
|
||||||
* @param url the URL to check if it can be handled
|
|
||||||
* @return true if the URL can be handled by NewPipe, false if it cannot
|
|
||||||
*/
|
|
||||||
public static boolean handleUrlCommentsTimestamp(@NonNull final CompositeDisposable
|
|
||||||
disposables,
|
|
||||||
final Context context,
|
|
||||||
@NonNull final String url) {
|
|
||||||
return handleUrl(context, url, HASHTAG_TIMESTAMP_PATTERN, disposables);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a YouTube timestamp description URL in NewPipe.
|
* Handle a YouTube timestamp description URL in NewPipe.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -66,36 +30,13 @@ public final class InternalUrlsHandler {
|
||||||
* player will be opened when the user will click on the timestamp in the video description,
|
* player will be opened when the user will click on the timestamp in the video description,
|
||||||
* at the time and for the video indicated in the timestamp.
|
* at the time and for the video indicated in the timestamp.
|
||||||
*
|
*
|
||||||
* @param disposables a field of the Activity/Fragment class that calls this method
|
|
||||||
* @param context the context to use
|
* @param context the context to use
|
||||||
* @param url the URL to check if it can be handled
|
* @param url the URL to check if it can be handled
|
||||||
* @return true if the URL can be handled by NewPipe, false if it cannot
|
* @return true if the URL can be handled by NewPipe, false if it cannot
|
||||||
*/
|
*/
|
||||||
public static boolean handleUrlDescriptionTimestamp(@NonNull final CompositeDisposable
|
public static boolean handleUrlDescriptionTimestamp(final Context context,
|
||||||
disposables,
|
|
||||||
final Context context,
|
|
||||||
@NonNull final String url) {
|
@NonNull final String url) {
|
||||||
return handleUrl(context, url, AMPERSAND_TIMESTAMP_PATTERN, disposables);
|
final Matcher matcher = AMPERSAND_TIMESTAMP_PATTERN.matcher(url);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an URL in NewPipe.
|
|
||||||
* <p>
|
|
||||||
* This method will check if the provided url can be handled in NewPipe or not. If this is a
|
|
||||||
* service URL with a timestamp, the popup player will be opened and true will be returned;
|
|
||||||
* else, false will be returned.
|
|
||||||
*
|
|
||||||
* @param context the context to use
|
|
||||||
* @param url the URL to check if it can be handled
|
|
||||||
* @param pattern the pattern to use
|
|
||||||
* @param disposables a field of the Activity/Fragment class that calls this method
|
|
||||||
* @return true if the URL can be handled by NewPipe, false if it cannot
|
|
||||||
*/
|
|
||||||
private static boolean handleUrl(final Context context,
|
|
||||||
@NonNull final String url,
|
|
||||||
@NonNull final Pattern pattern,
|
|
||||||
@NonNull final CompositeDisposable disposables) {
|
|
||||||
final Matcher matcher = pattern.matcher(url);
|
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +61,7 @@ public final class InternalUrlsHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linkType == StreamingService.LinkType.STREAM && seconds != -1) {
|
if (linkType == StreamingService.LinkType.STREAM && seconds != -1) {
|
||||||
return playOnPopup(context, matchedUrl, service, seconds, disposables);
|
return playOnPopup(context, matchedUrl, service, seconds);
|
||||||
} else {
|
} else {
|
||||||
NavigationHelper.openRouterActivity(context, matchedUrl);
|
NavigationHelper.openRouterActivity(context, matchedUrl);
|
||||||
return true;
|
return true;
|
||||||
|
@ -134,15 +75,12 @@ public final class InternalUrlsHandler {
|
||||||
* @param url the URL of the content
|
* @param url the URL of the content
|
||||||
* @param service the service of the content
|
* @param service the service of the content
|
||||||
* @param seconds the position in seconds at which the floating player will start
|
* @param seconds the position in seconds at which the floating player will start
|
||||||
* @param disposables disposables created by the method are added here and their lifecycle
|
|
||||||
* should be handled by the calling class
|
|
||||||
* @return true if the playback of the content has successfully started or false if not
|
* @return true if the playback of the content has successfully started or false if not
|
||||||
*/
|
*/
|
||||||
public static boolean playOnPopup(final Context context,
|
public static boolean playOnPopup(final Context context,
|
||||||
final String url,
|
final String url,
|
||||||
@NonNull final StreamingService service,
|
@NonNull final StreamingService service,
|
||||||
final int seconds,
|
final int seconds) {
|
||||||
@NonNull final CompositeDisposable disposables) {
|
|
||||||
final LinkHandlerFactory factory = service.getStreamLHFactory();
|
final LinkHandlerFactory factory = service.getStreamLHFactory();
|
||||||
final String cleanUrl;
|
final String cleanUrl;
|
||||||
|
|
||||||
|
@ -152,19 +90,14 @@ public final class InternalUrlsHandler {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Single<StreamInfo> single =
|
final Intent intent = NavigationHelper.getPlayerTimestampIntent(context,
|
||||||
ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
|
new TimestampChangeData(
|
||||||
disposables.add(single.subscribeOn(Schedulers.io())
|
service.getServiceId(),
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
cleanUrl,
|
||||||
.subscribe(info -> {
|
seconds
|
||||||
final PlayQueue playQueue = new SinglePlayQueue(info, seconds * 1000L);
|
));
|
||||||
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
|
ContextCompat.startForegroundService(context, intent);
|
||||||
}, throwable -> {
|
|
||||||
final var errorInfo = new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP, url);
|
|
||||||
// This will only show a snackbar if the passed context has a root view:
|
|
||||||
// otherwise it will resort to showing a notification, so we are safe here.
|
|
||||||
ErrorUtil.showSnackbar(context, errorInfo);
|
|
||||||
}));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,7 +192,7 @@ public final class TextLinkifier {
|
||||||
* <p>
|
* <p>
|
||||||
* Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of
|
* Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of
|
||||||
* a content, this method will parse the {@link CharSequence} and replace all current web links
|
* a content, this method will parse the {@link CharSequence} and replace all current web links
|
||||||
* with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}.
|
* with {@link ShareUtils#openUrlInBrowser(Context, String)}.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -240,7 +240,7 @@ public final class TextLinkifier {
|
||||||
for (final URLSpan span : urls) {
|
for (final URLSpan span : urls) {
|
||||||
final String url = span.getURL();
|
final String url = span.getURL();
|
||||||
final LongPressClickableSpan longPressClickableSpan =
|
final LongPressClickableSpan longPressClickableSpan =
|
||||||
new UrlLongPressClickableSpan(context, disposables, url);
|
new UrlLongPressClickableSpan(context, url);
|
||||||
|
|
||||||
textBlockLinked.setSpan(longPressClickableSpan,
|
textBlockLinked.setSpan(longPressClickableSpan,
|
||||||
textBlockLinked.getSpanStart(span),
|
textBlockLinked.getSpanStart(span),
|
||||||
|
|
|
@ -46,7 +46,7 @@ final class TimestampLongPressClickableSpan extends LongPressClickableSpan {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(@NonNull final View view) {
|
public void onClick(@NonNull final View view) {
|
||||||
playOnPopup(context, relatedStreamUrl, relatedInfoService,
|
playOnPopup(context, relatedStreamUrl, relatedInfoService,
|
||||||
timestampMatchDTO.seconds(), disposables);
|
timestampMatchDTO.seconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -7,29 +7,22 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
final class UrlLongPressClickableSpan extends LongPressClickableSpan {
|
final class UrlLongPressClickableSpan extends LongPressClickableSpan {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final CompositeDisposable disposables;
|
|
||||||
@NonNull
|
|
||||||
private final String url;
|
private final String url;
|
||||||
|
|
||||||
UrlLongPressClickableSpan(@NonNull final Context context,
|
UrlLongPressClickableSpan(@NonNull final Context context,
|
||||||
@NonNull final CompositeDisposable disposables,
|
|
||||||
@NonNull final String url) {
|
@NonNull final String url) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.disposables = disposables;
|
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(@NonNull final View view) {
|
public void onClick(@NonNull final View view) {
|
||||||
if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(
|
if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(context, url)) {
|
||||||
disposables, context, url)) {
|
|
||||||
ShareUtils.openUrlInApp(context, url);
|
ShareUtils.openUrlInApp(context, url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -572,7 +572,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
|
|
||||||
ErrorUtil.createNotification(mContext,
|
ErrorUtil.createNotification(mContext,
|
||||||
new ErrorInfo(ErrorInfo.Companion.throwableToStringList(mission.errObject), action,
|
new ErrorInfo(ErrorInfo.Companion.throwableToStringList(mission.errObject), action,
|
||||||
service, request.toString(), reason));
|
request.toString(), service, reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearFinishedDownloads(boolean delete) {
|
public void clearFinishedDownloads(boolean delete) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue