mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-02-12 05:00:15 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d45b6b8c9 | ||
|
|
c3dbed54e5 | ||
|
|
8968aab578 | ||
|
|
d7a4435e94 | ||
|
|
4a7eaed3a7 | ||
|
|
869a3cea9b | ||
|
|
e6e0be772a | ||
|
|
224a5d0cb9 | ||
|
|
c6fc94e7bd | ||
|
|
1eedfd7eee | ||
|
|
2c7654a579 | ||
|
|
09a746dd6a | ||
|
|
d665a4f016 | ||
|
|
6cf932b2a7 | ||
|
|
48467669b6 | ||
|
|
780e6a4848 | ||
|
|
13186c0b15 | ||
|
|
edfdbe805f | ||
|
|
451409fc3b | ||
|
|
289d22eed7 | ||
|
|
21f446a78e | ||
|
|
6214ae33f3 | ||
|
|
37cef825a2 | ||
|
|
dab8e056e9 | ||
|
|
020dbdc82a | ||
|
|
5d7934249f | ||
|
|
d6be966db3 | ||
|
|
56a043669a | ||
|
|
85abc58158 | ||
|
|
955844b3e1 | ||
|
|
e74907561e | ||
|
|
1554f77762 | ||
|
|
118def08b4 | ||
|
|
725cb70cbd | ||
|
|
5525d206dc | ||
|
|
83f9646eec | ||
|
|
85d43fe45e | ||
|
|
8d6e68d6f4 | ||
|
|
07fe1e758a | ||
|
|
15b5cef6c2 | ||
|
|
ae60f7d7eb | ||
|
|
739b6ae57b | ||
|
|
cc33b685a5 | ||
|
|
d051e8ecc8 | ||
|
|
51e62f09ba | ||
|
|
8a2c47bc12 | ||
|
|
a7aad63bbb | ||
|
|
fd192b4f3f | ||
|
|
19e94bd30c | ||
|
|
7758a27694 | ||
|
|
a3301dcfb1 | ||
|
|
d045b27cea | ||
|
|
4f70235ee8 | ||
|
|
54f9bcb03e |
2
.github/workflows/backport-pr.yml
vendored
2
.github/workflows/backport-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
github.event.comment.author_association == 'MEMBER'
|
github.event.comment.author_association == 'MEMBER'
|
||||||
)
|
)
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Get backport metadata
|
- name: Get backport metadata
|
||||||
# the target branch is the first argument after `/backport`
|
# the target branch is the first argument after `/backport`
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: gradle/actions/wrapper-validation@v4
|
- uses: gradle/actions/wrapper-validation@v5
|
||||||
|
|
||||||
- name: create and checkout branch
|
- name: create and checkout branch
|
||||||
# push events already checked out the branch
|
# push events already checked out the branch
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.jetbrains.kotlin.android)
|
alias(libs.plugins.jetbrains.kotlin.android)
|
||||||
@@ -32,7 +34,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
configure<ApplicationExtension> {
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
namespace = "org.schabi.newpipe"
|
namespace = "org.schabi.newpipe"
|
||||||
|
|
||||||
@@ -78,7 +80,10 @@ android {
|
|||||||
}
|
}
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
|
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +105,7 @@ android {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("androidTest") {
|
getByName("androidTest") {
|
||||||
assets.srcDir("$projectDir/schemas")
|
assets.directories += "$projectDir/schemas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +116,7 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
resValues = true
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
|
|||||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@@ -39,3 +39,8 @@
|
|||||||
|
|
||||||
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
|
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
|
||||||
-keep class org.schabi.newpipe.settings.notifications.** { *; }
|
-keep class org.schabi.newpipe.settings.notifications.** { *; }
|
||||||
|
|
||||||
|
# Prevent R8 from stripping or renaming Protobuf internal fields
|
||||||
|
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
|
||||||
* ExitActivity.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class ExitActivity extends Activity {
|
|
||||||
|
|
||||||
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
|
||||||
final Intent intent = new Intent(activity, ExitActivity.class);
|
|
||||||
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
|
||||||
|
|
||||||
activity.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
finishAndRemoveTask();
|
|
||||||
|
|
||||||
NavigationHelper.restartApp(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
app/src/main/java/org/schabi/newpipe/ExitActivity.kt
Normal file
36
app/src/main/java/org/schabi/newpipe/ExitActivity.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
|
|
||||||
|
class ExitActivity : Activity() {
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
finishAndRemoveTask()
|
||||||
|
NavigationHelper.restartApp(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun exitAndRemoveFromRecentApps(activity: Activity) {
|
||||||
|
val intent = Intent(activity, ExitActivity::class.java)
|
||||||
|
intent.addFlags(
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
|
or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
or Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||||
|
)
|
||||||
|
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,7 +92,7 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
return when (position) {
|
return when (position) {
|
||||||
posAbout -> AboutFragment()
|
posAbout -> AboutFragment()
|
||||||
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
else -> error("Unknown position for ViewPager2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
return when (position) {
|
return when (position) {
|
||||||
posAbout -> R.string.tab_about
|
posAbout -> R.string.tab_about
|
||||||
posLicense -> R.string.tab_licenses
|
posLicense -> R.string.tab_licenses
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
else -> error("Unknown position for ViewPager2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,11 +62,7 @@ data class PlaylistRemoteEntity(
|
|||||||
orderingName = playlistInfo.name,
|
orderingName = playlistInfo.name,
|
||||||
url = playlistInfo.url,
|
url = playlistInfo.url,
|
||||||
thumbnailUrl = ImageStrategy.imageListToDbUrl(
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(
|
||||||
if (playlistInfo.thumbnails.isEmpty()) {
|
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars }
|
||||||
playlistInfo.uploaderAvatars
|
|
||||||
} else {
|
|
||||||
playlistInfo.thumbnails
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
uploader = playlistInfo.uploaderName,
|
uploader = playlistInfo.uploaderName,
|
||||||
streamCount = playlistInfo.streamCount
|
streamCount = playlistInfo.streamCount
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
|||||||
|
|
||||||
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
||||||
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
||||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
?: error("Stream cannot be null just after insertion.")
|
||||||
newerStream.uid = existentMinimalStream.uid
|
newerStream.uid = existentMinimalStream.uid
|
||||||
|
|
||||||
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
|||||||
entity.uid = uidFromInsert
|
entity.uid = uidFromInsert
|
||||||
} else {
|
} else {
|
||||||
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
|
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
|
||||||
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
|
?: error("Subscription cannot be null just after insertion.")
|
||||||
entity.uid = subscriptionIdFromDb
|
entity.uid = subscriptionIdFromDb
|
||||||
|
|
||||||
update(entity)
|
update(entity)
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
package org.schabi.newpipe.error;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.content.IntentCompat;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 24.10.15.
|
|
||||||
*
|
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
|
||||||
* ErrorActivity.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
* <
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
* <
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This activity is used to show error details and allow reporting them in various ways. Use {@link
|
|
||||||
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
|
|
||||||
*/
|
|
||||||
public class ErrorActivity extends AppCompatActivity {
|
|
||||||
// LOG TAGS
|
|
||||||
public static final String TAG = ErrorActivity.class.toString();
|
|
||||||
// BUNDLE TAGS
|
|
||||||
public static final String ERROR_INFO = "error_info";
|
|
||||||
|
|
||||||
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
|
|
||||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
|
|
||||||
|
|
||||||
public static final String ERROR_GITHUB_ISSUE_URL =
|
|
||||||
"https://github.com/TeamNewPipe/NewPipe/issues";
|
|
||||||
|
|
||||||
private ErrorInfo errorInfo;
|
|
||||||
private String currentTimeStamp;
|
|
||||||
|
|
||||||
private ActivityErrorBinding activityErrorBinding;
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
|
||||||
// Activity lifecycle
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
ThemeHelper.setDayNightMode(this);
|
|
||||||
ThemeHelper.setTheme(this);
|
|
||||||
|
|
||||||
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
|
|
||||||
setContentView(activityErrorBinding.getRoot());
|
|
||||||
|
|
||||||
final Intent intent = getIntent();
|
|
||||||
|
|
||||||
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
|
|
||||||
|
|
||||||
final ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
actionBar.setTitle(R.string.error_report_title);
|
|
||||||
actionBar.setDisplayShowTitleEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
|
|
||||||
|
|
||||||
// important add guru meditation
|
|
||||||
addGuruMeditation();
|
|
||||||
// print current time, as zoned ISO8601 timestamp
|
|
||||||
final ZonedDateTime now = ZonedDateTime.now();
|
|
||||||
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
||||||
|
|
||||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
|
||||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
|
||||||
|
|
||||||
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
|
|
||||||
ShareUtils.copyToClipboard(this, buildMarkdown()));
|
|
||||||
|
|
||||||
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
|
|
||||||
openPrivacyPolicyDialog(this, "GITHUB"));
|
|
||||||
|
|
||||||
// normal bugreport
|
|
||||||
buildInfo(errorInfo);
|
|
||||||
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
|
|
||||||
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
|
|
||||||
|
|
||||||
// print stack trace once again for debugging:
|
|
||||||
for (final String e : errorInfo.getStackTraces()) {
|
|
||||||
Log.e(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
|
||||||
final MenuInflater inflater = getMenuInflater();
|
|
||||||
inflater.inflate(R.menu.error_menu, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.home:
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
case R.id.menu_item_share_error:
|
|
||||||
ShareUtils.shareText(getApplicationContext(),
|
|
||||||
getString(R.string.error_report_title), buildJson());
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openPrivacyPolicyDialog(final Context context, final String action) {
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
|
||||||
.setTitle(R.string.privacy_policy_title)
|
|
||||||
.setMessage(R.string.start_accept_privacy_policy)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
|
|
||||||
ShareUtils.openUrlInApp(context,
|
|
||||||
context.getString(R.string.privacy_policy_url)))
|
|
||||||
.setPositiveButton(R.string.accept, (dialog, which) -> {
|
|
||||||
if (action.equals("EMAIL")) { // send on email
|
|
||||||
final Intent i = new Intent(Intent.ACTION_SENDTO)
|
|
||||||
.setData(Uri.parse("mailto:")) // only email apps should handle this
|
|
||||||
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
|
|
||||||
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
|
|
||||||
+ getString(R.string.app_name) + " "
|
|
||||||
+ BuildConfig.VERSION_NAME)
|
|
||||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
|
||||||
ShareUtils.openIntentInApp(context, i);
|
|
||||||
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
|
||||||
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.decline, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formErrorText(final String[] el) {
|
|
||||||
final String separator = "-------------------------------------";
|
|
||||||
return Arrays.stream(el)
|
|
||||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildInfo(final ErrorInfo info) {
|
|
||||||
String text = "";
|
|
||||||
|
|
||||||
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
|
|
||||||
.replace("\\n", "\n"));
|
|
||||||
|
|
||||||
text += getUserActionString(info.getUserAction()) + "\n"
|
|
||||||
+ info.getRequest() + "\n"
|
|
||||||
+ getContentLanguageString() + "\n"
|
|
||||||
+ getContentCountryString() + "\n"
|
|
||||||
+ getAppLanguage() + "\n"
|
|
||||||
+ info.getServiceName() + "\n"
|
|
||||||
+ currentTimeStamp + "\n"
|
|
||||||
+ getPackageName() + "\n"
|
|
||||||
+ BuildConfig.VERSION_NAME + "\n"
|
|
||||||
+ getOsString();
|
|
||||||
|
|
||||||
activityErrorBinding.errorInfosView.setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildJson() {
|
|
||||||
try {
|
|
||||||
return JsonWriter.string()
|
|
||||||
.object()
|
|
||||||
.value("user_action", getUserActionString(errorInfo.getUserAction()))
|
|
||||||
.value("request", errorInfo.getRequest())
|
|
||||||
.value("content_language", getContentLanguageString())
|
|
||||||
.value("content_country", getContentCountryString())
|
|
||||||
.value("app_language", getAppLanguage())
|
|
||||||
.value("service", errorInfo.getServiceName())
|
|
||||||
.value("package", getPackageName())
|
|
||||||
.value("version", BuildConfig.VERSION_NAME)
|
|
||||||
.value("os", getOsString())
|
|
||||||
.value("time", currentTimeStamp)
|
|
||||||
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
|
|
||||||
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
|
|
||||||
.toString())
|
|
||||||
.end()
|
|
||||||
.done();
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
Log.e(TAG, "Error while erroring: Could not build json");
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildMarkdown() {
|
|
||||||
try {
|
|
||||||
final StringBuilder htmlErrorReport = new StringBuilder();
|
|
||||||
|
|
||||||
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
|
|
||||||
if (!userComment.isEmpty()) {
|
|
||||||
htmlErrorReport.append(userComment).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// basic error info
|
|
||||||
htmlErrorReport
|
|
||||||
.append("## Exception")
|
|
||||||
.append("\n* __User Action:__ ")
|
|
||||||
.append(getUserActionString(errorInfo.getUserAction()))
|
|
||||||
.append("\n* __Request:__ ").append(errorInfo.getRequest())
|
|
||||||
.append("\n* __Content Country:__ ").append(getContentCountryString())
|
|
||||||
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
|
||||||
.append("\n* __App Language:__ ").append(getAppLanguage())
|
|
||||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
|
||||||
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
|
|
||||||
.append("\n* __Package:__ ").append(getPackageName())
|
|
||||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
|
||||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
|
||||||
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
|
||||||
|
|
||||||
|
|
||||||
// Collapse all logs to a single paragraph when there are more than one
|
|
||||||
// to keep the GitHub issue clean.
|
|
||||||
if (errorInfo.getStackTraces().length > 1) {
|
|
||||||
htmlErrorReport
|
|
||||||
.append("<details><summary><b>Exceptions (")
|
|
||||||
.append(errorInfo.getStackTraces().length)
|
|
||||||
.append(")</b></summary><p>\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the logs
|
|
||||||
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
|
|
||||||
htmlErrorReport.append("<details><summary><b>Crash log ");
|
|
||||||
if (errorInfo.getStackTraces().length > 1) {
|
|
||||||
htmlErrorReport.append(i + 1);
|
|
||||||
}
|
|
||||||
htmlErrorReport.append("</b>")
|
|
||||||
.append("</summary><p>\n")
|
|
||||||
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
|
|
||||||
.append("</details>\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure to close everything
|
|
||||||
if (errorInfo.getStackTraces().length > 1) {
|
|
||||||
htmlErrorReport.append("</p></details>\n");
|
|
||||||
}
|
|
||||||
htmlErrorReport.append("<hr>\n");
|
|
||||||
return htmlErrorReport.toString();
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
Log.e(TAG, "Error while erroring: Could not build markdown");
|
|
||||||
e.printStackTrace();
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getUserActionString(final UserAction userAction) {
|
|
||||||
if (userAction == null) {
|
|
||||||
return "Your description is in another castle.";
|
|
||||||
} else {
|
|
||||||
return userAction.getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getContentCountryString() {
|
|
||||||
return Localization.getPreferredContentCountry(this).getCountryCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getContentLanguageString() {
|
|
||||||
return Localization.getPreferredLocalization(this).getLocalizationCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getAppLanguage() {
|
|
||||||
return Localization.getAppLocale().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getOsString() {
|
|
||||||
final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
||||||
? Build.VERSION.BASE_OS : "Android";
|
|
||||||
return System.getProperty("os.name")
|
|
||||||
+ " " + (osBase.isEmpty() ? "Android" : osBase)
|
|
||||||
+ " " + Build.VERSION.RELEASE
|
|
||||||
+ " - " + Build.VERSION.SDK_INT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addGuruMeditation() {
|
|
||||||
//just an easter egg
|
|
||||||
String text = activityErrorBinding.errorSorryView.getText().toString();
|
|
||||||
text += "\n" + getString(R.string.guru_meditation);
|
|
||||||
activityErrorBinding.errorSorryView.setText(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
280
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.kt
Normal file
280
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.kt
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2015-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.error
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.IntentCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.grack.nanojson.JsonWriter
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.ActivityErrorBinding
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This activity is used to show error details and allow reporting them in various ways.
|
||||||
|
* Use [ErrorUtil.openActivity] to correctly open this activity.
|
||||||
|
*/
|
||||||
|
class ErrorActivity : AppCompatActivity() {
|
||||||
|
private lateinit var errorInfo: ErrorInfo
|
||||||
|
private lateinit var currentTimeStamp: String
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityErrorBinding
|
||||||
|
|
||||||
|
private val contentCountryString: String
|
||||||
|
get() = Localization.getPreferredContentCountry(this).countryCode
|
||||||
|
|
||||||
|
private val contentLanguageString: String
|
||||||
|
get() = Localization.getPreferredLocalization(this).localizationCode
|
||||||
|
|
||||||
|
private val appLanguage: String
|
||||||
|
get() = Localization.getAppLocale().toString()
|
||||||
|
|
||||||
|
private val osString: String
|
||||||
|
get() {
|
||||||
|
val name = System.getProperty("os.name")!!
|
||||||
|
val osBase = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Build.VERSION.BASE_OS.ifEmpty { "Android" }
|
||||||
|
} else {
|
||||||
|
"Android"
|
||||||
|
}
|
||||||
|
return "$name $osBase ${Build.VERSION.RELEASE} - ${Build.VERSION.SDK_INT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val errorEmailSubject: String
|
||||||
|
get() = "$ERROR_EMAIL_SUBJECT ${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}"
|
||||||
|
|
||||||
|
// /////////////////////////////////////////////////////////////////////
|
||||||
|
// Activity lifecycle
|
||||||
|
// /////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
ThemeHelper.setDayNightMode(this)
|
||||||
|
ThemeHelper.setTheme(this)
|
||||||
|
|
||||||
|
binding = ActivityErrorBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.getRoot())
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbarLayout.toolbar)
|
||||||
|
supportActionBar?.apply {
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setTitle(R.string.error_report_title)
|
||||||
|
setDisplayShowTitleEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
|
||||||
|
|
||||||
|
// important add guru meditation
|
||||||
|
addGuruMeditation()
|
||||||
|
// print current time, as zoned ISO8601 timestamp
|
||||||
|
currentTimeStamp = ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
|
||||||
|
binding.errorReportEmailButton.setOnClickListener { _ ->
|
||||||
|
openPrivacyPolicyDialog(this, "EMAIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.errorReportCopyButton.setOnClickListener { _ ->
|
||||||
|
ShareUtils.copyToClipboard(this, buildMarkdown())
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.errorReportGitHubButton.setOnClickListener { _ ->
|
||||||
|
openPrivacyPolicyDialog(this, "GITHUB")
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal bugreport
|
||||||
|
buildInfo(errorInfo)
|
||||||
|
binding.errorMessageView.text = errorInfo.getMessage(this)
|
||||||
|
binding.errorView.text = formErrorText(errorInfo.stackTraces)
|
||||||
|
|
||||||
|
// print stack trace once again for debugging:
|
||||||
|
errorInfo.stackTraces.forEach { Log.e(TAG, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.error_menu, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.menu_item_share_error -> {
|
||||||
|
ShareUtils.shareText(
|
||||||
|
applicationContext,
|
||||||
|
getString(R.string.error_report_title),
|
||||||
|
buildJson()
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPrivacyPolicyDialog(context: Context, action: String) {
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||||
|
.setTitle(R.string.privacy_policy_title)
|
||||||
|
.setMessage(R.string.start_accept_privacy_policy)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setNeutralButton(R.string.read_privacy_policy) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(context, context.getString(R.string.privacy_policy_url))
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.accept) { _, _ ->
|
||||||
|
if (action == "EMAIL") { // send on email
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO)
|
||||||
|
.setData("mailto:".toUri()) // only email apps should handle this
|
||||||
|
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
|
||||||
|
.putExtra(Intent.EXTRA_SUBJECT, errorEmailSubject)
|
||||||
|
.putExtra(Intent.EXTRA_TEXT, buildJson())
|
||||||
|
ShareUtils.openIntentInApp(context, intent)
|
||||||
|
} else if (action == "GITHUB") { // open the NewPipe issue page on GitHub
|
||||||
|
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.decline, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formErrorText(stacktrace: Array<String>): String {
|
||||||
|
val separator = "-------------------------------------"
|
||||||
|
return stacktrace.joinToString(separator + "\n", separator + "\n", separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInfo(info: ErrorInfo) {
|
||||||
|
binding.errorInfoLabelsView.text = getString(R.string.info_labels)
|
||||||
|
.replace("\\n", "\n")
|
||||||
|
|
||||||
|
val text = info.userAction.message + "\n" +
|
||||||
|
info.request + "\n" +
|
||||||
|
contentLanguageString + "\n" +
|
||||||
|
contentCountryString + "\n" +
|
||||||
|
appLanguage + "\n" +
|
||||||
|
info.getServiceName() + "\n" +
|
||||||
|
currentTimeStamp + "\n" +
|
||||||
|
packageName + "\n" +
|
||||||
|
BuildConfig.VERSION_NAME + "\n" +
|
||||||
|
osString
|
||||||
|
|
||||||
|
binding.errorInfosView.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildJson(): String {
|
||||||
|
try {
|
||||||
|
return JsonWriter.string()
|
||||||
|
.`object`()
|
||||||
|
.value("user_action", errorInfo.userAction.message)
|
||||||
|
.value("request", errorInfo.request)
|
||||||
|
.value("content_language", contentLanguageString)
|
||||||
|
.value("content_country", contentCountryString)
|
||||||
|
.value("app_language", appLanguage)
|
||||||
|
.value("service", errorInfo.getServiceName())
|
||||||
|
.value("package", packageName)
|
||||||
|
.value("version", BuildConfig.VERSION_NAME)
|
||||||
|
.value("os", osString)
|
||||||
|
.value("time", currentTimeStamp)
|
||||||
|
.array("exceptions", errorInfo.stackTraces.toList())
|
||||||
|
.value("user_comment", binding.errorCommentBox.getText().toString())
|
||||||
|
.end()
|
||||||
|
.done()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Error while erroring: Could not build json", exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMarkdown(): String {
|
||||||
|
try {
|
||||||
|
return buildString(1024) {
|
||||||
|
val userComment = binding.errorCommentBox.text.toString()
|
||||||
|
if (userComment.isNotEmpty()) {
|
||||||
|
appendLine(userComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic error info
|
||||||
|
appendLine("## Exception")
|
||||||
|
appendLine("* __User Action:__ ${errorInfo.userAction.message}")
|
||||||
|
appendLine("* __Request:__ ${errorInfo.request}")
|
||||||
|
appendLine("* __Content Country:__ $contentCountryString")
|
||||||
|
appendLine("* __Content Language:__ $contentLanguageString")
|
||||||
|
appendLine("* __App Language:__ $appLanguage")
|
||||||
|
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
|
||||||
|
appendLine("* __Timestamp:__ $currentTimeStamp")
|
||||||
|
appendLine("* __Package:__ $packageName")
|
||||||
|
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
|
||||||
|
appendLine("* __Version:__ ${BuildConfig.VERSION_NAME}")
|
||||||
|
appendLine("* __OS:__ $osString")
|
||||||
|
|
||||||
|
// Collapse all logs to a single paragraph when there are more than one
|
||||||
|
// to keep the GitHub issue clean.
|
||||||
|
if (errorInfo.stackTraces.isNotEmpty()) {
|
||||||
|
append("<details><summary><b>Exceptions (")
|
||||||
|
append(errorInfo.stackTraces.size)
|
||||||
|
append(")</b></summary><p>\n")
|
||||||
|
|
||||||
|
// add the logs
|
||||||
|
errorInfo.stackTraces.forEachIndexed { index, stacktrace ->
|
||||||
|
append("<details><summary><b>Crash log ")
|
||||||
|
if (errorInfo.stackTraces.isNotEmpty()) {
|
||||||
|
append(index + 1)
|
||||||
|
}
|
||||||
|
append("</b>")
|
||||||
|
append("</summary><p>\n")
|
||||||
|
append("\n```\n${stacktrace}\n```\n")
|
||||||
|
append("</details>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure to close everything
|
||||||
|
append("</p></details>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
append("<hr>\n")
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Error while erroring: Could not build markdown", exception)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addGuruMeditation() {
|
||||||
|
// just an easter egg
|
||||||
|
var text = binding.errorSorryView.text.toString()
|
||||||
|
text += "\n" + getString(R.string.guru_meditation)
|
||||||
|
binding.errorSorryView.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// LOG TAGS
|
||||||
|
private val TAG = ErrorActivity::class.java.toString()
|
||||||
|
|
||||||
|
// BUNDLE TAGS
|
||||||
|
const val ERROR_INFO = "error_info"
|
||||||
|
|
||||||
|
private const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
|
||||||
|
private const val ERROR_EMAIL_SUBJECT = "Exception in "
|
||||||
|
|
||||||
|
private const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package org.schabi.newpipe.info_list;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 26.09.16.
|
|
||||||
* <p>
|
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
|
||||||
* InfoItemBuilder.java is part of NewPipe.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class InfoItemBuilder {
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
|
||||||
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
|
||||||
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
|
||||||
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
|
|
||||||
|
|
||||||
public InfoItemBuilder(final Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
return buildView(parent, infoItem, historyRecordManager, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager,
|
|
||||||
final boolean useMiniVariant) {
|
|
||||||
final InfoItemHolder holder =
|
|
||||||
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
|
||||||
holder.updateFromItem(infoItem, historyRecordManager);
|
|
||||||
return holder.itemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
|
||||||
@NonNull final InfoItem.InfoType infoType,
|
|
||||||
final boolean useMiniVariant) {
|
|
||||||
switch (infoType) {
|
|
||||||
case STREAM:
|
|
||||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
|
||||||
: new StreamInfoItemHolder(this, parent);
|
|
||||||
case CHANNEL:
|
|
||||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
|
||||||
: new ChannelInfoItemHolder(this, parent);
|
|
||||||
case PLAYLIST:
|
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
|
||||||
: new PlaylistInfoItemHolder(this, parent);
|
|
||||||
case COMMENT:
|
|
||||||
return new CommentInfoItemHolder(this, parent);
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Context getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
|
|
||||||
return onStreamSelectedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
|
|
||||||
this.onStreamSelectedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<ChannelInfoItem> getOnChannelSelectedListener() {
|
|
||||||
return onChannelSelectedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
|
|
||||||
this.onChannelSelectedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() {
|
|
||||||
return onPlaylistSelectedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
|
|
||||||
this.onPlaylistSelectedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
|
|
||||||
return onCommentsSelectedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnCommentsSelectedListener(
|
|
||||||
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
|
|
||||||
this.onCommentsSelectedListener = onCommentsSelectedListener;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.info_list
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
|
|
||||||
|
class InfoItemBuilder(val context: Context) {
|
||||||
|
var onStreamSelectedListener: OnClickGesture<StreamInfoItem>? = null
|
||||||
|
var onChannelSelectedListener: OnClickGesture<ChannelInfoItem>? = null
|
||||||
|
var onPlaylistSelectedListener: OnClickGesture<PlaylistInfoItem>? = null
|
||||||
|
var onCommentsSelectedListener: OnClickGesture<CommentsInfoItem>? = null
|
||||||
|
}
|
||||||
@@ -129,8 +129,7 @@ class FeedViewModel(
|
|||||||
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
|
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
|
||||||
this.showPlayedItems.onNext(showPlayedItems)
|
this.showPlayedItems.onNext(showPlayedItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
|
putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
|
||||||
this.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,8 +138,7 @@ class FeedViewModel(
|
|||||||
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
|
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
|
||||||
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
|
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
|
putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
|
||||||
this.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +147,7 @@ class FeedViewModel(
|
|||||||
fun setSaveShowFutureItems(showFutureItems: Boolean) {
|
fun setSaveShowFutureItems(showFutureItems: Boolean) {
|
||||||
this.showFutureItems.onNext(showFutureItems)
|
this.showFutureItems.onNext(showFutureItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
|
putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
|
||||||
this.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ class FeedLoadManager(private val context: Context) {
|
|||||||
broadcastProgress()
|
broadcastProgress()
|
||||||
}
|
}
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.flatMap { Flowable.fromIterable(it) }
|
// Randomize user subscription ordering to attempt to resist fingerprinting
|
||||||
|
.flatMap { Flowable.fromIterable(it.shuffled()) }
|
||||||
.takeWhile { !cancelSignal.get() }
|
.takeWhile { !cancelSignal.get() }
|
||||||
.doOnNext { subscriptionEntity ->
|
.doOnNext { subscriptionEntity ->
|
||||||
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited
|
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
groupIcon = feedGroupEntity?.icon
|
groupIcon = feedGroupEntity?.icon
|
||||||
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
|
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
|
||||||
|
|
||||||
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
|
val feedGroupIcon = selectedIcon ?: icon
|
||||||
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
|
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
|
||||||
|
|
||||||
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
|
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ internal fun infoItemTypeToString(type: InfoType): String {
|
|||||||
InfoType.STREAM -> ID_STREAM
|
InfoType.STREAM -> ID_STREAM
|
||||||
InfoType.PLAYLIST -> ID_PLAYLIST
|
InfoType.PLAYLIST -> ID_PLAYLIST
|
||||||
InfoType.CHANNEL -> ID_CHANNEL
|
InfoType.CHANNEL -> ID_CHANNEL
|
||||||
else -> throw IllegalStateException("Unexpected value: $type")
|
else -> error("Unexpected value: $type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ internal fun infoItemTypeFromString(type: String): InfoType {
|
|||||||
ID_STREAM -> InfoType.STREAM
|
ID_STREAM -> InfoType.STREAM
|
||||||
ID_PLAYLIST -> InfoType.PLAYLIST
|
ID_PLAYLIST -> InfoType.PLAYLIST
|
||||||
ID_CHANNEL -> InfoType.CHANNEL
|
ID_CHANNEL -> InfoType.CHANNEL
|
||||||
else -> throw IllegalStateException("Unexpected value: $type")
|
else -> error("Unexpected value: $type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,11 +82,11 @@ internal class PackageValidator(context: Context) {
|
|||||||
|
|
||||||
// Build the caller info for the rest of the checks here.
|
// Build the caller info for the rest of the checks here.
|
||||||
val callerPackageInfo = buildCallerInfo(callingPackage)
|
val callerPackageInfo = buildCallerInfo(callingPackage)
|
||||||
?: throw IllegalStateException("Caller wasn't found in the system?")
|
?: error("Caller wasn't found in the system?")
|
||||||
|
|
||||||
// Verify that things aren't ... broken. (This test should always pass.)
|
// Verify that things aren't ... broken. (This test should always pass.)
|
||||||
if (callerPackageInfo.uid != callingUid) {
|
check(callerPackageInfo.uid == callingUid) {
|
||||||
throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?")
|
"Caller's package UID doesn't match caller's actual UID?"
|
||||||
}
|
}
|
||||||
|
|
||||||
val callerSignature = callerPackageInfo.signature
|
val callerSignature = callerPackageInfo.signature
|
||||||
@@ -202,7 +202,7 @@ internal class PackageValidator(context: Context) {
|
|||||||
*/
|
*/
|
||||||
private fun getSystemSignature(): String = getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
|
private fun getSystemSignature(): String = getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
|
||||||
getSignature(platformInfo)
|
getSignature(platformInfo)
|
||||||
} ?: throw IllegalStateException("Platform signature not found")
|
} ?: error("Platform signature not found")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a SHA-256 signature given a certificate byte array.
|
* Creates a SHA-256 signature given a certificate byte array.
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package org.schabi.newpipe.settings.export;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.io.ObjectStreamClass;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to
|
|
||||||
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
|
|
||||||
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
|
|
||||||
* <a href="https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution">
|
|
||||||
* cmu.edu
|
|
||||||
* </a>,
|
|
||||||
* <a href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream">
|
|
||||||
* OWASP cheatsheet
|
|
||||||
* </a>,
|
|
||||||
* <a href="https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118">
|
|
||||||
* Apache's {@code ValidatingObjectInputStream}
|
|
||||||
* </a>
|
|
||||||
*/
|
|
||||||
public class PreferencesObjectInputStream extends ObjectInputStream {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Primitive types, strings and other built-in types do not pass through resolveClass() but
|
|
||||||
* instead have a custom encoding; see
|
|
||||||
* <a href="https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152">
|
|
||||||
* official docs</a>.
|
|
||||||
*/
|
|
||||||
private static final Set<String> CLASS_WHITELIST = Set.of(
|
|
||||||
"java.lang.Boolean",
|
|
||||||
"java.lang.Byte",
|
|
||||||
"java.lang.Character",
|
|
||||||
"java.lang.Short",
|
|
||||||
"java.lang.Integer",
|
|
||||||
"java.lang.Long",
|
|
||||||
"java.lang.Float",
|
|
||||||
"java.lang.Double",
|
|
||||||
"java.lang.Void",
|
|
||||||
"java.util.HashMap",
|
|
||||||
"java.util.HashSet"
|
|
||||||
);
|
|
||||||
|
|
||||||
public PreferencesObjectInputStream(final InputStream in) throws IOException {
|
|
||||||
super(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Class<?> resolveClass(final ObjectStreamClass desc)
|
|
||||||
throws ClassNotFoundException, IOException {
|
|
||||||
if (CLASS_WHITELIST.contains(desc.getName())) {
|
|
||||||
return super.resolveClass(desc);
|
|
||||||
} else {
|
|
||||||
throw new ClassNotFoundException("Class not allowed: " + desc.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.settings.export
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.ObjectInputStream
|
||||||
|
import java.io.ObjectStreamClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [ObjectInputStream] that only allows preferences-related types to be deserialized, to
|
||||||
|
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
|
||||||
|
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
|
||||||
|
* [cmu.edu](https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution) * ,
|
||||||
|
* [OWASP cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream) * ,
|
||||||
|
* [Apache's `ValidatingObjectInputStream`](https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118) *
|
||||||
|
*/
|
||||||
|
class PreferencesObjectInputStream(stream: InputStream) : ObjectInputStream(stream) {
|
||||||
|
@Throws(ClassNotFoundException::class, IOException::class)
|
||||||
|
override fun resolveClass(desc: ObjectStreamClass): Class<*> {
|
||||||
|
if (desc.name in CLASS_WHITELIST) {
|
||||||
|
return super.resolveClass(desc)
|
||||||
|
} else {
|
||||||
|
throw ClassNotFoundException("Class not allowed: $desc.name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Primitive types, strings and other built-in types do not pass through resolveClass() but
|
||||||
|
* instead have a custom encoding; see
|
||||||
|
* [
|
||||||
|
* official docs](https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152).
|
||||||
|
*/
|
||||||
|
private val CLASS_WHITELIST = setOf<String>(
|
||||||
|
"java.lang.Boolean",
|
||||||
|
"java.lang.Byte",
|
||||||
|
"java.lang.Character",
|
||||||
|
"java.lang.Short",
|
||||||
|
"java.lang.Integer",
|
||||||
|
"java.lang.Long",
|
||||||
|
"java.lang.Float",
|
||||||
|
"java.lang.Double",
|
||||||
|
"java.lang.Void",
|
||||||
|
"java.util.HashMap",
|
||||||
|
"java.util.HashSet"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,14 +26,13 @@ data class PreferenceSearchItem(
|
|||||||
val breadcrumbs: String,
|
val breadcrumbs: String,
|
||||||
@XmlRes val searchIndexItemResId: Int
|
@XmlRes val searchIndexItemResId: Int
|
||||||
) {
|
) {
|
||||||
|
val allRelevantSearchFields: List<String>
|
||||||
|
get() = listOf(title, summary, entries, breadcrumbs)
|
||||||
|
|
||||||
fun hasData(): Boolean {
|
fun hasData(): Boolean {
|
||||||
return !key.isEmpty() && !title.isEmpty()
|
return !key.isEmpty() && !title.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllRelevantSearchFields(): MutableList<String?> {
|
|
||||||
return mutableListOf(title, summary, entries, breadcrumbs)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "PreferenceItem: $title $summary $key"
|
return "PreferenceItem: $title $summary $key"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import com.grack.nanojson.JsonParserException;
|
|||||||
import com.grack.nanojson.JsonStringWriter;
|
import com.grack.nanojson.JsonStringWriter;
|
||||||
import com.grack.nanojson.JsonWriter;
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to get a JSON representation of a list of tabs, and the other way around.
|
* Class to get a JSON representation of a list of tabs, and the other way around.
|
||||||
@@ -44,39 +45,25 @@ public final class TabsJsonHelper {
|
|||||||
return getDefaultTabs();
|
return getDefaultTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Tab> returnTabs = new ArrayList<>();
|
|
||||||
|
|
||||||
final JsonObject outerJsonObject;
|
|
||||||
try {
|
try {
|
||||||
outerJsonObject = JsonParser.object().from(tabsJson);
|
final JsonObject outerJsonObject = JsonParser.object().from(tabsJson);
|
||||||
|
|
||||||
if (!outerJsonObject.has(JSON_TABS_ARRAY_KEY)) {
|
if (!outerJsonObject.has(JSON_TABS_ARRAY_KEY)) {
|
||||||
throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY
|
throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY
|
||||||
+ "\" array");
|
+ "\" array");
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY);
|
final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY, null);
|
||||||
|
|
||||||
for (final Object o : tabsArray) {
|
final var returnTabs = tabsArray.streamAsJsonObjects()
|
||||||
if (!(o instanceof JsonObject)) {
|
.map(Tab::from)
|
||||||
continue;
|
.filter(Objects::nonNull)
|
||||||
}
|
.collect(Collectors.toUnmodifiableList());
|
||||||
|
|
||||||
final Tab tab = Tab.from((JsonObject) o);
|
return returnTabs.isEmpty() ? getDefaultTabs() : returnTabs;
|
||||||
|
|
||||||
if (tab != null) {
|
|
||||||
returnTabs.add(tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (final JsonParserException e) {
|
} catch (final JsonParserException e) {
|
||||||
throw new InvalidJsonException(e);
|
throw new InvalidJsonException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnTabs.isEmpty()) {
|
|
||||||
return getDefaultTabs();
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnTabs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For preferences with dependencies and multiple use case,
|
|
||||||
* this class can be used to reduce the lines of code.
|
|
||||||
*/
|
|
||||||
public final class DependentPreferenceHelper {
|
|
||||||
|
|
||||||
private DependentPreferenceHelper() {
|
|
||||||
// no instance
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
|
|
||||||
* `Resume playback` and its dependencies are all enabled.
|
|
||||||
*
|
|
||||||
* @param context the Android context
|
|
||||||
* @return returns true if `Resume playback` and `Watch history` are both enabled
|
|
||||||
*/
|
|
||||||
public static boolean getResumePlaybackEnabled(final Context context) {
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
|
|
||||||
return prefs.getBoolean(context.getString(
|
|
||||||
R.string.enable_watch_history_key), true)
|
|
||||||
&& prefs.getBoolean(context.getString(
|
|
||||||
R.string.enable_playback_resume_key), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
|
|
||||||
* `Position in lists` and its dependencies are all enabled.
|
|
||||||
*
|
|
||||||
* @param context the Android context
|
|
||||||
* @return returns true if `Positions in lists` and `Watch history` are both enabled
|
|
||||||
*/
|
|
||||||
public static boolean getPositionsInListsEnabled(final Context context) {
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
|
|
||||||
return prefs.getBoolean(context.getString(
|
|
||||||
R.string.enable_watch_history_key), true)
|
|
||||||
&& prefs.getBoolean(context.getString(
|
|
||||||
R.string.enable_playback_state_lists_key), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For preferences with dependencies and multiple use case,
|
||||||
|
* this class can be used to reduce the lines of code.
|
||||||
|
*/
|
||||||
|
object DependentPreferenceHelper {
|
||||||
|
/**
|
||||||
|
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
|
||||||
|
* `Resume playback` and its dependencies are all enabled.
|
||||||
|
*
|
||||||
|
* @param context the Android context
|
||||||
|
* @return returns true if `Resume playback` and `Watch history` are both enabled
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getResumePlaybackEnabled(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
|
||||||
|
prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
|
||||||
|
* `Position in lists` and its dependencies are all enabled.
|
||||||
|
*
|
||||||
|
* @param context the Android context
|
||||||
|
* @return returns true if `Positions in lists` and `Watch history` are both enabled
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getPositionsInListsEnabled(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
|
||||||
|
prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.text.Selection;
|
|
||||||
import android.text.Spannable;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.views.NewPipeEditText;
|
|
||||||
import org.schabi.newpipe.views.NewPipeTextView;
|
|
||||||
|
|
||||||
public final class NewPipeTextViewHelper {
|
|
||||||
private NewPipeTextViewHelper() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Share the selected text of {@link NewPipeTextView NewPipeTextViews} and
|
|
||||||
* {@link NewPipeEditText NewPipeEditTexts} with
|
|
||||||
* {@link ShareUtils#shareText(Context, String, String)}.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
|
|
||||||
* using the {@code Share} command of the popup menu which appears when selecting text.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param textView the {@link TextView} on which sharing the selected text. It should be a
|
|
||||||
* {@link NewPipeTextView} or a {@link NewPipeEditText} (even if
|
|
||||||
* {@link TextView standard TextViews} are supported).
|
|
||||||
*/
|
|
||||||
public static void shareSelectedTextWithShareUtils(@NonNull final TextView textView) {
|
|
||||||
final CharSequence textViewText = textView.getText();
|
|
||||||
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText));
|
|
||||||
if (textViewText instanceof Spannable) {
|
|
||||||
Selection.setSelection((Spannable) textViewText, textView.getSelectionEnd());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static CharSequence getSelectedText(@NonNull final TextView textView,
|
|
||||||
@Nullable final CharSequence text) {
|
|
||||||
if (!textView.hasSelection() || text == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int start = textView.getSelectionStart();
|
|
||||||
final int end = textView.getSelectionEnd();
|
|
||||||
return String.valueOf(start > end ? text.subSequence(end, start)
|
|
||||||
: text.subSequence(start, end));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void shareSelectedTextIfNotNullAndNotEmpty(
|
|
||||||
@NonNull final TextView textView,
|
|
||||||
@Nullable final CharSequence selectedText) {
|
|
||||||
if (selectedText != null && selectedText.length() != 0) {
|
|
||||||
ShareUtils.shareText(textView.getContext(), "", selectedText.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.text.Selection
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.widget.TextView
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
object NewPipeTextViewHelper {
|
||||||
|
/**
|
||||||
|
* Share the selected text of [NewPipeTextViews][org.schabi.newpipe.views.NewPipeTextView] and
|
||||||
|
* [NewPipeEditTexts][org.schabi.newpipe.views.NewPipeEditText] with
|
||||||
|
* [ShareUtils.shareText].
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
|
||||||
|
* using the `Share` command of the popup menu which appears when selecting text.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param textView the [TextView] on which sharing the selected text. It should be a
|
||||||
|
* [org.schabi.newpipe.views.NewPipeTextView] or a [org.schabi.newpipe.views.NewPipeEditText]
|
||||||
|
* (even if [standard TextViews][TextView] are supported).
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun shareSelectedTextWithShareUtils(textView: TextView) {
|
||||||
|
val textViewText = textView.getText()
|
||||||
|
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText))
|
||||||
|
if (textViewText is Spannable) {
|
||||||
|
Selection.setSelection(textViewText, textView.selectionEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSelectedText(textView: TextView, text: CharSequence?): CharSequence? {
|
||||||
|
if (!textView.hasSelection() || text == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val start = textView.selectionStart
|
||||||
|
val end = textView.selectionEnd
|
||||||
|
return if (start > end) {
|
||||||
|
text.subSequence(end, start)
|
||||||
|
} else {
|
||||||
|
text.subSequence(start, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareSelectedTextIfNotNullAndNotEmpty(
|
||||||
|
textView: TextView,
|
||||||
|
selectedText: CharSequence?
|
||||||
|
) {
|
||||||
|
if (!selectedText.isNullOrEmpty()) {
|
||||||
|
ShareUtils.shareText(textView.context, "", selectedText.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonArray;
|
|
||||||
import com.grack.nanojson.JsonObject;
|
|
||||||
import com.grack.nanojson.JsonParser;
|
|
||||||
import com.grack.nanojson.JsonParserException;
|
|
||||||
import com.grack.nanojson.JsonStringWriter;
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class PeertubeHelper {
|
|
||||||
private PeertubeHelper() { }
|
|
||||||
|
|
||||||
public static List<PeertubeInstance> getInstanceList(final Context context) {
|
|
||||||
final SharedPreferences sharedPreferences = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(context);
|
|
||||||
final String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
|
|
||||||
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
|
|
||||||
if (null == savedJson) {
|
|
||||||
return List.of(getCurrentInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
|
|
||||||
final List<PeertubeInstance> result = new ArrayList<>();
|
|
||||||
for (final Object o : array) {
|
|
||||||
if (o instanceof JsonObject) {
|
|
||||||
final JsonObject instance = (JsonObject) o;
|
|
||||||
final String name = instance.getString("name");
|
|
||||||
final String url = instance.getString("url");
|
|
||||||
result.add(new PeertubeInstance(url, name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (final JsonParserException e) {
|
|
||||||
return List.of(getCurrentInstance());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PeertubeInstance selectInstance(final PeertubeInstance instance,
|
|
||||||
final Context context) {
|
|
||||||
final SharedPreferences sharedPreferences = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(context);
|
|
||||||
final String selectedInstanceKey =
|
|
||||||
context.getString(R.string.peertube_selected_instance_key);
|
|
||||||
final JsonStringWriter jsonWriter = JsonWriter.string().object();
|
|
||||||
jsonWriter.value("name", instance.getName());
|
|
||||||
jsonWriter.value("url", instance.getUrl());
|
|
||||||
final String jsonToSave = jsonWriter.end().done();
|
|
||||||
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
|
|
||||||
ServiceList.PeerTube.setInstance(instance);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PeertubeInstance getCurrentInstance() {
|
|
||||||
return ServiceList.PeerTube.getInstance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
52
app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.kt
Normal file
52
app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.kt
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2019-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.grack.nanojson.JsonObject
|
||||||
|
import com.grack.nanojson.JsonParser
|
||||||
|
import com.grack.nanojson.JsonWriter
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
|
||||||
|
|
||||||
|
object PeertubeHelper {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val currentInstance: PeertubeInstance
|
||||||
|
get() = ServiceList.PeerTube.instance
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getInstanceList(context: Context): List<PeertubeInstance> {
|
||||||
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val savedInstanceListKey = context.getString(R.string.peertube_instance_list_key)
|
||||||
|
val savedJson = sharedPreferences.getString(savedInstanceListKey, null)
|
||||||
|
?: return listOf(currentInstance)
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
JsonParser.`object`().from(savedJson).getArray("instances")
|
||||||
|
.filterIsInstance<JsonObject>()
|
||||||
|
.map { PeertubeInstance(it.getString("url"), it.getString("name")) }
|
||||||
|
}.getOrDefault(listOf(currentInstance))
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun selectInstance(instance: PeertubeInstance, context: Context): PeertubeInstance {
|
||||||
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key)
|
||||||
|
|
||||||
|
val jsonWriter = JsonWriter.string().`object`()
|
||||||
|
jsonWriter.value("name", instance.name)
|
||||||
|
jsonWriter.value("url", instance.url)
|
||||||
|
val jsonToSave = jsonWriter.end().done()
|
||||||
|
|
||||||
|
sharedPreferences.edit { putString(selectedInstanceKey, jsonToSave) }
|
||||||
|
ServiceList.PeerTube.instance = instance
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for play buttons and their respective click listeners.
|
|
||||||
*/
|
|
||||||
public final class PlayButtonHelper {
|
|
||||||
|
|
||||||
private PlayButtonHelper() {
|
|
||||||
// utility class
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize {@link android.view.View.OnClickListener OnClickListener}
|
|
||||||
* and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control
|
|
||||||
* buttons defined in {@link R.layout#playlist_control}.
|
|
||||||
*
|
|
||||||
* @param activity The activity to use for the {@link android.widget.Toast Toast}.
|
|
||||||
* @param playlistControlBinding The binding of the
|
|
||||||
* {@link R.layout#playlist_control playlist control layout}.
|
|
||||||
* @param fragment The fragment to get the play queue from.
|
|
||||||
*/
|
|
||||||
public static void initPlaylistControlClickListener(
|
|
||||||
@NonNull final AppCompatActivity activity,
|
|
||||||
@NonNull final PlaylistControlBinding playlistControlBinding,
|
|
||||||
@NonNull final PlaylistControlViewHolder fragment) {
|
|
||||||
// click listener
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
|
|
||||||
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue());
|
|
||||||
showHoldToAppendToastIfNeeded(activity);
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false);
|
|
||||||
showHoldToAppendToastIfNeeded(activity);
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false);
|
|
||||||
showHoldToAppendToastIfNeeded(activity);
|
|
||||||
});
|
|
||||||
|
|
||||||
// long click listener
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the "hold to append" toast if the corresponding preference is enabled.
|
|
||||||
*
|
|
||||||
* @param context The context to show the toast.
|
|
||||||
*/
|
|
||||||
private static void showHoldToAppendToastIfNeeded(@NonNull final Context context) {
|
|
||||||
if (shouldShowHoldToAppendTip(context)) {
|
|
||||||
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the "hold to append" toast should be shown.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The tip is shown if the corresponding preference is enabled.
|
|
||||||
* This is the default behaviour.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param context The context to get the preference.
|
|
||||||
* @return {@code true} if the tip should be shown, {@code false} otherwise.
|
|
||||||
*/
|
|
||||||
public static boolean shouldShowHoldToAppendTip(@NonNull final Context context) {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
.getBoolean(context.getString(R.string.show_hold_to_append_key), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.OnLongClickListener
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.PlaylistControlBinding
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder
|
||||||
|
import org.schabi.newpipe.player.PlayerType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for play buttons and their respective click listeners.
|
||||||
|
*/
|
||||||
|
object PlayButtonHelper {
|
||||||
|
/**
|
||||||
|
* Initialize [OnClickListener][View.OnClickListener]
|
||||||
|
* and [OnLongClickListener][OnLongClickListener] for playlist control
|
||||||
|
* buttons defined in [R.layout.playlist_control].
|
||||||
|
*
|
||||||
|
* @param activity The activity to use for the [Toast][Toast].
|
||||||
|
* @param playlistControlBinding The binding of the
|
||||||
|
* [playlist control layout][R.layout.playlist_control].
|
||||||
|
* @param fragment The fragment to get the play queue from.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun initPlaylistControlClickListener(
|
||||||
|
activity: AppCompatActivity,
|
||||||
|
playlistControlBinding: PlaylistControlBinding,
|
||||||
|
fragment: PlaylistControlViewHolder
|
||||||
|
) {
|
||||||
|
// click listener
|
||||||
|
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener {
|
||||||
|
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue())
|
||||||
|
showHoldToAppendToastIfNeeded(activity)
|
||||||
|
}
|
||||||
|
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener {
|
||||||
|
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false)
|
||||||
|
showHoldToAppendToastIfNeeded(activity)
|
||||||
|
}
|
||||||
|
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener {
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false)
|
||||||
|
showHoldToAppendToastIfNeeded(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// long click listener
|
||||||
|
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener {
|
||||||
|
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener {
|
||||||
|
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener {
|
||||||
|
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the "hold to append" toast if the corresponding preference is enabled.
|
||||||
|
*
|
||||||
|
* @param context The context to show the toast.
|
||||||
|
*/
|
||||||
|
private fun showHoldToAppendToastIfNeeded(context: Context) {
|
||||||
|
if (shouldShowHoldToAppendTip(context)) {
|
||||||
|
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the "hold to append" toast should be shown.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The tip is shown if the corresponding preference is enabled.
|
||||||
|
* This is the default behaviour.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param context The context to get the preference.
|
||||||
|
* @return `true` if the tip should be shown, `false` otherwise.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun shouldShowHoldToAppendTip(context: Context): Boolean {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(context.getString(R.string.show_hold_to_append_key), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonObject;
|
|
||||||
import com.grack.nanojson.JsonParser;
|
|
||||||
import com.grack.nanojson.JsonParserException;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public final class ServiceHelper {
|
|
||||||
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
|
|
||||||
|
|
||||||
private ServiceHelper() { }
|
|
||||||
|
|
||||||
@DrawableRes
|
|
||||||
public static int getIcon(final int serviceId) {
|
|
||||||
switch (serviceId) {
|
|
||||||
case 0:
|
|
||||||
return R.drawable.ic_smart_display;
|
|
||||||
case 1:
|
|
||||||
return R.drawable.ic_cloud;
|
|
||||||
case 2:
|
|
||||||
return R.drawable.ic_placeholder_media_ccc;
|
|
||||||
case 3:
|
|
||||||
return R.drawable.ic_placeholder_peertube;
|
|
||||||
case 4:
|
|
||||||
return R.drawable.ic_placeholder_bandcamp;
|
|
||||||
default:
|
|
||||||
return R.drawable.ic_circle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getTranslatedFilterString(final String filter, final Context c) {
|
|
||||||
switch (filter) {
|
|
||||||
case "all":
|
|
||||||
return c.getString(R.string.all);
|
|
||||||
case "videos":
|
|
||||||
case "sepia_videos":
|
|
||||||
case "music_videos":
|
|
||||||
return c.getString(R.string.videos_string);
|
|
||||||
case "channels":
|
|
||||||
return c.getString(R.string.channels);
|
|
||||||
case "playlists":
|
|
||||||
case "music_playlists":
|
|
||||||
return c.getString(R.string.playlists);
|
|
||||||
case "tracks":
|
|
||||||
return c.getString(R.string.tracks);
|
|
||||||
case "users":
|
|
||||||
return c.getString(R.string.users);
|
|
||||||
case "conferences":
|
|
||||||
return c.getString(R.string.conferences);
|
|
||||||
case "events":
|
|
||||||
return c.getString(R.string.events);
|
|
||||||
case "music_songs":
|
|
||||||
return c.getString(R.string.songs);
|
|
||||||
case "music_albums":
|
|
||||||
return c.getString(R.string.albums);
|
|
||||||
case "music_artists":
|
|
||||||
return c.getString(R.string.artists);
|
|
||||||
default:
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a resource string with instructions for importing subscriptions for each service.
|
|
||||||
*
|
|
||||||
* @param serviceId service to get the instructions for
|
|
||||||
* @return the string resource containing the instructions or -1 if the service don't support it
|
|
||||||
*/
|
|
||||||
@StringRes
|
|
||||||
public static int getImportInstructions(final int serviceId) {
|
|
||||||
switch (serviceId) {
|
|
||||||
case 0:
|
|
||||||
return R.string.import_youtube_instructions;
|
|
||||||
case 1:
|
|
||||||
return R.string.import_soundcloud_instructions;
|
|
||||||
default:
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For services that support importing from a channel url, return a hint that will
|
|
||||||
* be used in the EditText that the user will type in his channel url.
|
|
||||||
*
|
|
||||||
* @param serviceId service to get the hint for
|
|
||||||
* @return the hint's string resource or -1 if the service don't support it
|
|
||||||
*/
|
|
||||||
@StringRes
|
|
||||||
public static int getImportInstructionsHint(final int serviceId) {
|
|
||||||
switch (serviceId) {
|
|
||||||
case 1:
|
|
||||||
return R.string.import_soundcloud_instructions_hint;
|
|
||||||
default:
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getSelectedServiceId(final Context context) {
|
|
||||||
return Optional.ofNullable(getSelectedService(context))
|
|
||||||
.orElse(DEFAULT_FALLBACK_SERVICE)
|
|
||||||
.getServiceId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static StreamingService getSelectedService(final Context context) {
|
|
||||||
final String serviceName = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
.getString(context.getString(R.string.current_service_key),
|
|
||||||
context.getString(R.string.default_service_value));
|
|
||||||
|
|
||||||
try {
|
|
||||||
return NewPipe.getService(serviceName);
|
|
||||||
} catch (final ExtractionException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static String getNameOfServiceById(final int serviceId) {
|
|
||||||
return ServiceList.all().stream()
|
|
||||||
.filter(s -> s.getServiceId() == serviceId)
|
|
||||||
.findFirst()
|
|
||||||
.map(StreamingService::getServiceInfo)
|
|
||||||
.map(StreamingService.ServiceInfo::getName)
|
|
||||||
.orElse("<unknown>");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param serviceId the id of the service
|
|
||||||
* @return the service corresponding to the provided id
|
|
||||||
* @throws java.util.NoSuchElementException if there is no service with the provided id
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public static StreamingService getServiceById(final int serviceId) {
|
|
||||||
return ServiceList.all().stream()
|
|
||||||
.filter(s -> s.getServiceId() == serviceId)
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setSelectedServiceId(final Context context, final int serviceId) {
|
|
||||||
String serviceName;
|
|
||||||
try {
|
|
||||||
serviceName = NewPipe.getService(serviceId).getServiceInfo().getName();
|
|
||||||
} catch (final ExtractionException e) {
|
|
||||||
serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedServicePreferences(context, serviceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setSelectedServicePreferences(final Context context,
|
|
||||||
final String serviceName) {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().
|
|
||||||
putString(context.getString(R.string.current_service_key), serviceName).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long getCacheExpirationMillis(final int serviceId) {
|
|
||||||
if (serviceId == SoundCloud.getServiceId()) {
|
|
||||||
return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
|
|
||||||
} else {
|
|
||||||
return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initService(final Context context, final int serviceId) {
|
|
||||||
if (serviceId == ServiceList.PeerTube.getServiceId()) {
|
|
||||||
final SharedPreferences sharedPreferences = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(context);
|
|
||||||
final String json = sharedPreferences.getString(context.getString(
|
|
||||||
R.string.peertube_selected_instance_key), null);
|
|
||||||
if (null == json) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final JsonObject jsonObject;
|
|
||||||
try {
|
|
||||||
jsonObject = JsonParser.object().from(json);
|
|
||||||
} catch (final JsonParserException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String name = jsonObject.getString("name");
|
|
||||||
final String url = jsonObject.getString("url");
|
|
||||||
final PeertubeInstance instance = new PeertubeInstance(url, name);
|
|
||||||
ServiceList.PeerTube.setInstance(instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initServices(final Context context) {
|
|
||||||
for (final StreamingService s : ServiceList.all()) {
|
|
||||||
initService(context, s.getServiceId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
168
app/src/main/java/org/schabi/newpipe/util/ServiceHelper.kt
Normal file
168
app/src/main/java/org/schabi/newpipe/util/ServiceHelper.kt
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.grack.nanojson.JsonParser
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
|
||||||
|
import org.schabi.newpipe.ktx.getStringSafe
|
||||||
|
|
||||||
|
object ServiceHelper {
|
||||||
|
private val DEFAULT_FALLBACK_SERVICE: StreamingService = ServiceList.YouTube
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@DrawableRes
|
||||||
|
fun getIcon(serviceId: Int): Int {
|
||||||
|
return when (serviceId) {
|
||||||
|
0 -> R.drawable.ic_smart_display
|
||||||
|
1 -> R.drawable.ic_cloud
|
||||||
|
2 -> R.drawable.ic_placeholder_media_ccc
|
||||||
|
3 -> R.drawable.ic_placeholder_peertube
|
||||||
|
4 -> R.drawable.ic_placeholder_bandcamp
|
||||||
|
else -> R.drawable.ic_circle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getTranslatedFilterString(filter: String, context: Context): String {
|
||||||
|
return when (filter) {
|
||||||
|
"all" -> context.getString(R.string.all)
|
||||||
|
"videos", "sepia_videos", "music_videos" -> context.getString(R.string.videos_string)
|
||||||
|
"channels" -> context.getString(R.string.channels)
|
||||||
|
"playlists", "music_playlists" -> context.getString(R.string.playlists)
|
||||||
|
"tracks" -> context.getString(R.string.tracks)
|
||||||
|
"users" -> context.getString(R.string.users)
|
||||||
|
"conferences" -> context.getString(R.string.conferences)
|
||||||
|
"events" -> context.getString(R.string.events)
|
||||||
|
"music_songs" -> context.getString(R.string.songs)
|
||||||
|
"music_albums" -> context.getString(R.string.albums)
|
||||||
|
"music_artists" -> context.getString(R.string.artists)
|
||||||
|
else -> filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a resource string with instructions for importing subscriptions for each service.
|
||||||
|
*
|
||||||
|
* @param serviceId service to get the instructions for
|
||||||
|
* @return the string resource containing the instructions or -1 if the service don't support it
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@StringRes
|
||||||
|
fun getImportInstructions(serviceId: Int): Int {
|
||||||
|
return when (serviceId) {
|
||||||
|
0 -> R.string.import_youtube_instructions
|
||||||
|
1 -> R.string.import_soundcloud_instructions
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For services that support importing from a channel url, return a hint that will
|
||||||
|
* be used in the EditText that the user will type in his channel url.
|
||||||
|
*
|
||||||
|
* @param serviceId service to get the hint for
|
||||||
|
* @return the hint's string resource or -1 if the service don't support it
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@StringRes
|
||||||
|
fun getImportInstructionsHint(serviceId: Int): Int {
|
||||||
|
return when (serviceId) {
|
||||||
|
1 -> R.string.import_soundcloud_instructions_hint
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getSelectedServiceId(context: Context): Int {
|
||||||
|
return (getSelectedService(context) ?: DEFAULT_FALLBACK_SERVICE).serviceId
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getSelectedService(context: Context): StreamingService? {
|
||||||
|
val serviceName: String = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getStringSafe(
|
||||||
|
context.getString(R.string.current_service_key),
|
||||||
|
context.getString(R.string.default_service_value)
|
||||||
|
)
|
||||||
|
|
||||||
|
return runCatching { NewPipe.getService(serviceName) }.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getNameOfServiceById(serviceId: Int): String {
|
||||||
|
return ServiceList.all().stream()
|
||||||
|
.filter { it.serviceId == serviceId }
|
||||||
|
.findFirst()
|
||||||
|
.map(StreamingService::getServiceInfo)
|
||||||
|
.map(StreamingService.ServiceInfo::getName)
|
||||||
|
.orElse("<unknown>")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param serviceId the id of the service
|
||||||
|
* @return the service corresponding to the provided id
|
||||||
|
* @throws java.util.NoSuchElementException if there is no service with the provided id
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getServiceById(serviceId: Int): StreamingService {
|
||||||
|
return ServiceList.all().firstNotNullOf { it.takeIf { it.serviceId == serviceId } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setSelectedServiceId(context: Context, serviceId: Int) {
|
||||||
|
val serviceName = runCatching { NewPipe.getService(serviceId).serviceInfo.name }
|
||||||
|
.getOrDefault(DEFAULT_FALLBACK_SERVICE.serviceInfo.name)
|
||||||
|
|
||||||
|
setSelectedServicePreferences(context, serviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSelectedServicePreferences(context: Context, serviceName: String?) {
|
||||||
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
sharedPreferences.edit { putString(context.getString(R.string.current_service_key), serviceName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getCacheExpirationMillis(serviceId: Int): Long {
|
||||||
|
return if (serviceId == ServiceList.SoundCloud.serviceId) {
|
||||||
|
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)
|
||||||
|
} else {
|
||||||
|
TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initService(context: Context, serviceId: Int) {
|
||||||
|
if (serviceId == ServiceList.PeerTube.serviceId) {
|
||||||
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val json = sharedPreferences.getString(
|
||||||
|
context.getString(R.string.peertube_selected_instance_key),
|
||||||
|
null
|
||||||
|
) ?: return
|
||||||
|
|
||||||
|
val jsonObject = runCatching { JsonParser.`object`().from(json) }
|
||||||
|
.getOrElse { return@initService }
|
||||||
|
|
||||||
|
ServiceList.PeerTube.instance = PeertubeInstance(
|
||||||
|
jsonObject.getString("url"),
|
||||||
|
jsonObject.getString("name")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun initServices(context: Context) {
|
||||||
|
ServiceList.all().forEach { initService(context, it.serviceId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for {@link StreamType}.
|
|
||||||
*/
|
|
||||||
public final class StreamTypeUtil {
|
|
||||||
private StreamTypeUtil() {
|
|
||||||
// No impl pls
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the {@link StreamType} of a stream is a livestream.
|
|
||||||
*
|
|
||||||
* @param streamType the stream type of the stream
|
|
||||||
* @return whether the stream type is {@link StreamType#AUDIO_STREAM},
|
|
||||||
* {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM}
|
|
||||||
*/
|
|
||||||
public static boolean isAudio(final StreamType streamType) {
|
|
||||||
return streamType == StreamType.AUDIO_STREAM
|
|
||||||
|| streamType == StreamType.AUDIO_LIVE_STREAM
|
|
||||||
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the {@link StreamType} of a stream is a livestream.
|
|
||||||
*
|
|
||||||
* @param streamType the stream type of the stream
|
|
||||||
* @return whether the stream type is {@link StreamType#VIDEO_STREAM},
|
|
||||||
* {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM}
|
|
||||||
*/
|
|
||||||
public static boolean isVideo(final StreamType streamType) {
|
|
||||||
return streamType == StreamType.VIDEO_STREAM
|
|
||||||
|| streamType == StreamType.LIVE_STREAM
|
|
||||||
|| streamType == StreamType.POST_LIVE_STREAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the {@link StreamType} of a stream is a livestream.
|
|
||||||
*
|
|
||||||
* @param streamType the stream type of the stream
|
|
||||||
* @return whether the stream type is {@link StreamType#LIVE_STREAM} or
|
|
||||||
* {@link StreamType#AUDIO_LIVE_STREAM}
|
|
||||||
*/
|
|
||||||
public static boolean isLiveStream(final StreamType streamType) {
|
|
||||||
return streamType == StreamType.LIVE_STREAM
|
|
||||||
|| streamType == StreamType.AUDIO_LIVE_STREAM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.kt
Normal file
54
app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for [StreamType].
|
||||||
|
*/
|
||||||
|
object StreamTypeUtil {
|
||||||
|
/**
|
||||||
|
* Check if the [StreamType] of a stream is a livestream.
|
||||||
|
*
|
||||||
|
* @param streamType the stream type of the stream
|
||||||
|
* @return whether the stream type is [StreamType.AUDIO_STREAM],
|
||||||
|
* [StreamType.AUDIO_LIVE_STREAM] or [StreamType.POST_LIVE_AUDIO_STREAM]
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun isAudio(streamType: StreamType): Boolean {
|
||||||
|
return streamType == StreamType.AUDIO_STREAM ||
|
||||||
|
streamType == StreamType.AUDIO_LIVE_STREAM ||
|
||||||
|
streamType == StreamType.POST_LIVE_AUDIO_STREAM
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the [StreamType] of a stream is a livestream.
|
||||||
|
*
|
||||||
|
* @param streamType the stream type of the stream
|
||||||
|
* @return whether the stream type is [StreamType.VIDEO_STREAM],
|
||||||
|
* [StreamType.LIVE_STREAM] or [StreamType.POST_LIVE_STREAM]
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun isVideo(streamType: StreamType): Boolean {
|
||||||
|
return streamType == StreamType.VIDEO_STREAM ||
|
||||||
|
streamType == StreamType.LIVE_STREAM ||
|
||||||
|
streamType == StreamType.POST_LIVE_STREAM
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the [StreamType] of a stream is a livestream.
|
||||||
|
*
|
||||||
|
* @param streamType the stream type of the stream
|
||||||
|
* @return whether the stream type is [StreamType.LIVE_STREAM] or
|
||||||
|
* [StreamType.AUDIO_LIVE_STREAM]
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun isLiveStream(streamType: StreamType): Boolean {
|
||||||
|
return streamType == StreamType.LIVE_STREAM ||
|
||||||
|
streamType == StreamType.AUDIO_LIVE_STREAM
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
android.enableJetifier=false
|
|
||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
||||||
android.nonTransitiveRClass=true
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
||||||
systemProp.file.encoding=utf-8
|
systemProp.file.encoding=utf-8
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ groupie = "2.10.1"
|
|||||||
jsoup = "1.22.1"
|
jsoup = "1.22.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junit-ext = "1.3.0"
|
junit-ext = "1.3.0"
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.3.0"
|
||||||
ksp = "2.3.4"
|
ksp = "2.3.5"
|
||||||
ktlint = "1.8.0"
|
ktlint = "1.8.0"
|
||||||
leakcanary = "2.14"
|
leakcanary = "2.14"
|
||||||
lifecycle = "2.9.4" # Newer versions require minSdk >= 23
|
lifecycle = "2.9.4" # Newer versions require minSdk >= 23
|
||||||
|
|||||||
Reference in New Issue
Block a user