diff --git a/LICENSE b/LICENSE
index 94a9ed024d3..f288702d2fa 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
- Copyright (C) 2007 Free Software Foundation, Inc.
+ Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
- along with this program. If not, see .
+ along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
@@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
-.
+.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
-.
+.
diff --git a/app/build.gradle b/app/build.gradle
index 797e76997df..7ec17044e40 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,15 +9,15 @@ plugins {
android {
compileSdk 31
- buildToolsVersion '30.0.3'
+ buildToolsVersion '31.0.0'
defaultConfig {
applicationId "org.schabi.newpipe"
resValue "string", "app_name", "NewPipe"
minSdk 19
targetSdk 29
- versionCode 983
- versionName "0.22.0"
+ versionCode 984
+ versionName "0.22.1"
multiDexEnabled true
@@ -98,10 +98,10 @@ android {
}
ext {
- checkstyleVersion = '9.2.1'
+ checkstyleVersion = '10.0'
androidxLifecycleVersion = '2.3.1'
- androidxRoomVersion = '2.3.0'
+ androidxRoomVersion = '2.4.2'
androidxWorkVersion = '2.7.1'
icepickVersion = '3.2.0'
@@ -122,7 +122,7 @@ configurations {
}
checkstyle {
- getConfigDirectory().set(rootProject.file("."))
+ getConfigDirectory().set(rootProject.file("checkstyle"))
ignoreFailures false
showViolations true
toolVersion = checkstyleVersion
@@ -194,7 +194,7 @@ dependencies {
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
- ktlint 'com.pinterest:ktlint:0.43.2'
+ ktlint 'com.pinterest:ktlint:0.44.0'
/** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
@@ -202,16 +202,16 @@ dependencies {
/** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.cardview:cardview:1.0.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
- implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
- implementation 'androidx.media:media:1.4.3'
+ implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
+ implementation 'androidx.media:media:1.5.0'
implementation 'androidx.multidex:multidex:2.0.1'
- implementation 'androidx.preference:preference:1.1.1'
+ implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
@@ -221,7 +221,8 @@ dependencies {
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.webkit:webkit:1.4.0'
- implementation 'com.google.android.material:material:1.4.0'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation "androidx.work:work-runtime:${androidxWorkVersion}"
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
@@ -249,8 +250,6 @@ dependencies {
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
- // Circular ImageView
- implementation "de.hdodenhof:circleimageview:3.1.0"
// Image loading
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
implementation "com.squareup.picasso:picasso:2.8"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 28cdbf02038..f9c99819c4a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -381,9 +381,6 @@
-
diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
deleted file mode 100644
index 122660d6431..00000000000
--- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
+++ /dev/null
@@ -1,264 +0,0 @@
-package org.schabi.newpipe;
-
-import android.app.Application;
-import android.app.IntentService;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.Signature;
-import android.net.Uri;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.pm.PackageInfoCompat;
-import androidx.preference.PreferenceManager;
-
-import com.grack.nanojson.JsonObject;
-import com.grack.nanojson.JsonParser;
-import com.grack.nanojson.JsonParserException;
-
-import org.schabi.newpipe.error.ErrorInfo;
-import org.schabi.newpipe.error.ErrorUtil;
-import org.schabi.newpipe.error.UserAction;
-import org.schabi.newpipe.extractor.downloader.Response;
-import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.List;
-
-public final class CheckForNewAppVersion extends IntentService {
- public CheckForNewAppVersion() {
- super("CheckForNewAppVersion");
- }
-
- private static final boolean DEBUG = MainActivity.DEBUG;
- private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
-
- // Public key of the certificate that is used in NewPipe release versions
- private static final String RELEASE_CERT_PUBLIC_KEY_SHA1
- = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
- private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
-
- /**
- * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
- *
- * @param application The application
- * @return String with the APK's SHA1 fingerprint in hexadecimal
- */
- @NonNull
- private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
- final List signatures;
- try {
- signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
- application.getPackageName());
- } catch (final PackageManager.NameNotFoundException e) {
- ErrorUtil.createNotification(application, new ErrorInfo(e,
- UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
- return "";
- }
- if (signatures.isEmpty()) {
- return "";
- }
-
- final X509Certificate c;
- try {
- final byte[] cert = signatures.get(0).toByteArray();
- final InputStream input = new ByteArrayInputStream(cert);
- final CertificateFactory cf = CertificateFactory.getInstance("X509");
- c = (X509Certificate) cf.generateCertificate(input);
- } catch (final CertificateException e) {
- ErrorUtil.createNotification(application, new ErrorInfo(e,
- UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
- return "";
- }
-
- try {
- final MessageDigest md = MessageDigest.getInstance("SHA1");
- final byte[] publicKey = md.digest(c.getEncoded());
- return byte2HexFormatted(publicKey);
- } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
- ErrorUtil.createNotification(application, new ErrorInfo(e,
- UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
- return "";
- }
- }
-
- private static String byte2HexFormatted(final byte[] arr) {
- final StringBuilder str = new StringBuilder(arr.length * 2);
-
- for (int i = 0; i < arr.length; i++) {
- String h = Integer.toHexString(arr[i]);
- final int l = h.length();
- if (l == 1) {
- h = "0" + h;
- }
- if (l > 2) {
- h = h.substring(l - 2, l);
- }
- str.append(h.toUpperCase());
- if (i < (arr.length - 1)) {
- str.append(':');
- }
- }
- return str.toString();
- }
-
- /**
- * Method to compare the current and latest available app version.
- * If a newer version is available, we show the update notification.
- *
- * @param application The application
- * @param versionName Name of new version
- * @param apkLocationUrl Url with the new apk
- * @param versionCode Code of new version
- */
- private static void compareAppVersionAndShowNotification(@NonNull final Application application,
- final String versionName,
- final String apkLocationUrl,
- final int versionCode) {
- if (BuildConfig.VERSION_CODE >= versionCode) {
- return;
- }
-
- // A pending intent to open the apk location url in the browser.
- final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- final PendingIntent pendingIntent
- = PendingIntent.getActivity(application, 0, intent, 0);
-
- final String channelId = application
- .getString(R.string.app_update_notification_channel_id);
- final NotificationCompat.Builder notificationBuilder
- = new NotificationCompat.Builder(application, channelId)
- .setSmallIcon(R.drawable.ic_newpipe_update)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setContentIntent(pendingIntent)
- .setAutoCancel(true)
- .setContentTitle(application
- .getString(R.string.app_update_notification_content_title))
- .setContentText(application
- .getString(R.string.app_update_notification_content_text)
- + " " + versionName);
-
- final NotificationManagerCompat notificationManager
- = NotificationManagerCompat.from(application);
- notificationManager.notify(2000, notificationBuilder.build());
- }
-
- public static boolean isReleaseApk(@NonNull final App app) {
- return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1);
- }
-
- private void checkNewVersion() throws IOException, ReCaptchaException {
- final App app = App.getApp();
-
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
- final NewVersionManager manager = new NewVersionManager();
-
- // Check if the current apk is a github one or not.
- if (!isReleaseApk(app)) {
- return;
- }
-
- // Check if the last request has happened a certain time ago
- // to reduce the number of API requests.
- final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
- if (!manager.isExpired(expiry)) {
- return;
- }
-
- // Make a network request to get latest NewPipe data.
- final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
- handleResponse(response, manager, prefs, app);
- }
-
- private void handleResponse(@NonNull final Response response,
- @NonNull final NewVersionManager manager,
- @NonNull final SharedPreferences prefs,
- @NonNull final App app) {
- try {
- // Store a timestamp which needs to be exceeded,
- // before a new request to the API is made.
- final long newExpiry = manager
- .coerceExpiry(response.getHeader("expires"));
- prefs.edit()
- .putLong(app.getString(R.string.update_expiry_key), newExpiry)
- .apply();
- } catch (final Exception e) {
- if (DEBUG) {
- Log.w(TAG, "Could not extract and save new expiry date", e);
- }
- }
-
- // Parse the json from the response.
- try {
-
- final JsonObject githubStableObject = JsonParser.object()
- .from(response.responseBody()).getObject("flavors")
- .getObject("github").getObject("stable");
-
- final String versionName = githubStableObject
- .getString("version");
- final int versionCode = githubStableObject
- .getInt("version_code");
- final String apkLocationUrl = githubStableObject
- .getString("apk");
-
- compareAppVersionAndShowNotification(app, versionName,
- apkLocationUrl, versionCode);
- } catch (final JsonParserException e) {
- // Most likely something is wrong in data received from NEWPIPE_API_URL.
- // Do not alarm user and fail silently.
- if (DEBUG) {
- Log.w(TAG, "Could not get NewPipe API: invalid json", e);
- }
- }
- }
-
- /**
- * Start a new service which
- * checks if all conditions for performing a version check are met,
- * fetches the API endpoint {@link #NEWPIPE_API_URL} containing info
- * about the latest NewPipe version
- * and displays a notification about ana available update.
- *
- * Following conditions need to be met, before data is request from the server:
- *
- *
The app is signed with the correct signing key (by TeamNewPipe / schabi).
- * If the signing key differs from the one used upstream, the update cannot be installed.
- *
The user enabled searching for and notifying about updates in the settings.
- *
The app did not recently check for updates.
- * We do not want to make unnecessary connections and DOS our servers.
- *
- * Must not be executed when the app is in background.
- */
- public static void startNewVersionCheckService() {
- final Intent intent = new Intent(App.getApp().getApplicationContext(),
- CheckForNewAppVersion.class);
- App.getApp().startService(intent);
- }
-
- @Override
- protected void onHandleIntent(@Nullable final Intent intent) {
- try {
- checkNewVersion();
- } catch (final IOException e) {
- Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e);
- } catch (final ReCaptchaException e) {
- Log.e(TAG, "ReCaptchaException should never happen here.", e);
- }
-
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 9895bace7f5..6ae5cf93661 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -20,7 +20,6 @@
package org.schabi.newpipe;
-import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.content.BroadcastReceiver;
@@ -178,10 +177,9 @@ protected void onPostCreate(final Bundle savedInstanceState) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
- // Start the service which is checking all conditions
+ // Start the worker which is checking all conditions
// and eventually searching for a new version.
- // The service searching for a new NewPipe version must not be started in background.
- startNewVersionCheckService();
+ NewVersionWorker.enqueueNewVersionCheckingWork(app);
}
}
@@ -231,7 +229,7 @@ private void addDrawerMenuForCurrentService() throws ExtractionException {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
- .setIcon(KioskTranslator.getKioskIcon(ks, this));
+ .setIcon(KioskTranslator.getKioskIcon(ks));
kioskId++;
}
diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt b/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt
deleted file mode 100644
index 36de1ecfcbe..00000000000
--- a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.schabi.newpipe
-
-import java.time.Instant
-import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-
-class NewVersionManager {
-
- fun isExpired(expiry: Long): Boolean {
- return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
- }
-
- /**
- * Coerce expiry date time in between 6 hours and 72 hours from now
- *
- * @return Epoch second of expiry date time
- */
- fun coerceExpiry(expiryString: String?): Long {
- val now = ZonedDateTime.now()
- return expiryString?.let {
-
- var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
- expiry = maxOf(expiry, now.plusHours(6))
- expiry = minOf(expiry, now.plusHours(72))
- expiry.toEpochSecond()
- } ?: now.plusHours(6).toEpochSecond()
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
new file mode 100644
index 00000000000..060114974fd
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
@@ -0,0 +1,163 @@
+package org.schabi.newpipe
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.edit
+import androidx.core.net.toUri
+import androidx.preference.PreferenceManager
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import androidx.work.WorkRequest
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.grack.nanojson.JsonParser
+import com.grack.nanojson.JsonParserException
+import org.schabi.newpipe.extractor.downloader.Response
+import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
+import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
+import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
+import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
+import java.io.IOException
+
+class NewVersionWorker(
+ context: Context,
+ workerParams: WorkerParameters
+) : Worker(context, workerParams) {
+
+ /**
+ * Method to compare the current and latest available app version.
+ * If a newer version is available, we show the update notification.
+ *
+ * @param versionName Name of new version
+ * @param apkLocationUrl Url with the new apk
+ * @param versionCode Code of new version
+ */
+ private fun compareAppVersionAndShowNotification(
+ versionName: String,
+ apkLocationUrl: String?,
+ versionCode: Int
+ ) {
+ if (BuildConfig.VERSION_CODE >= versionCode) {
+ return
+ }
+ val app = App.getApp()
+
+ // A pending intent to open the apk location url in the browser.
+ val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ val pendingIntent = PendingIntent.getActivity(app, 0, intent, 0)
+ val channelId = app.getString(R.string.app_update_notification_channel_id)
+ val notificationBuilder = NotificationCompat.Builder(app, channelId)
+ .setSmallIcon(R.drawable.ic_newpipe_update)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ .setContentTitle(app.getString(R.string.app_update_notification_content_title))
+ .setContentText(
+ app.getString(R.string.app_update_notification_content_text) +
+ " " + versionName
+ )
+ val notificationManager = NotificationManagerCompat.from(app)
+ notificationManager.notify(2000, notificationBuilder.build())
+ }
+
+ @Throws(IOException::class, ReCaptchaException::class)
+ private fun checkNewVersion() {
+ // Check if the current apk is a github one or not.
+ if (!isReleaseApk()) {
+ return
+ }
+
+ val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+ // Check if the last request has happened a certain time ago
+ // to reduce the number of API requests.
+ val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
+ if (!isLastUpdateCheckExpired(expiry)) {
+ return
+ }
+
+ // Make a network request to get latest NewPipe data.
+ val response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL)
+ handleResponse(response)
+ }
+
+ private fun handleResponse(response: Response) {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+ try {
+ // Store a timestamp which needs to be exceeded,
+ // before a new request to the API is made.
+ val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
+ prefs.edit {
+ putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
+ }
+ } catch (e: Exception) {
+ if (DEBUG) {
+ Log.w(TAG, "Could not extract and save new expiry date", e)
+ }
+ }
+
+ // Parse the json from the response.
+ try {
+ val githubStableObject = JsonParser.`object`()
+ .from(response.responseBody()).getObject("flavors")
+ .getObject("github").getObject("stable")
+
+ val versionName = githubStableObject.getString("version")
+ val versionCode = githubStableObject.getInt("version_code")
+ val apkLocationUrl = githubStableObject.getString("apk")
+ compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
+ } catch (e: JsonParserException) {
+ // Most likely something is wrong in data received from NEWPIPE_API_URL.
+ // Do not alarm user and fail silently.
+ if (DEBUG) {
+ Log.w(TAG, "Could not get NewPipe API: invalid json", e)
+ }
+ }
+ }
+
+ override fun doWork(): Result {
+ try {
+ checkNewVersion()
+ } catch (e: IOException) {
+ Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e)
+ return Result.failure()
+ } catch (e: ReCaptchaException) {
+ Log.e(TAG, "ReCaptchaException should never happen here.", e)
+ return Result.failure()
+ }
+ return Result.success()
+ }
+
+ companion object {
+ private val DEBUG = MainActivity.DEBUG
+ private val TAG = NewVersionWorker::class.java.simpleName
+ private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json"
+
+ /**
+ * Start a new worker which
+ * checks if all conditions for performing a version check are met,
+ * fetches the API endpoint [.NEWPIPE_API_URL] containing info
+ * about the latest NewPipe version
+ * and displays a notification about ana available update.
+ *
+ * Following conditions need to be met, before data is request from the server:
+ *
+ * * The app is signed with the correct signing key (by TeamNewPipe / schabi).
+ * If the signing key differs from the one used upstream, the update cannot be installed.
+ * * The user enabled searching for and notifying about updates in the settings.
+ * * The app did not recently check for updates.
+ * We do not want to make unnecessary connections and DOS our servers.
+ *
+ */
+ @JvmStatic
+ fun enqueueNewVersionCheckingWork(context: Context) {
+ val workRequest: WorkRequest =
+ OneTimeWorkRequest.Builder(NewVersionWorker::class.java).build()
+ WorkManager.getInstance(context).enqueue(workRequest)
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
index fde006a60f8..c7604e51283 100644
--- a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
@@ -14,7 +14,7 @@
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.NavigationHelper;
-import org.schabi.newpipe.util.SaveUploaderUrlHelper;
+import org.schabi.newpipe.util.SparseItemUtil;
import java.util.Collections;
@@ -62,7 +62,8 @@ public static void openPopupMenu(final PlayQueue playQueue,
return true;
case R.id.menu_item_channel_details:
- SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(context, item,
+ SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
+ item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
index 9d6e44f045b..adef3c0e4ba 100644
--- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
@@ -633,7 +633,7 @@ private void openDownloadDialog() {
.subscribe(result -> {
final List sortedVideoStreams = ListHelper
.getSortedStreamVideosList(this, result.getVideoStreams(),
- result.getVideoOnlyStreams(), false);
+ result.getVideoOnlyStreams(), false, false);
final int selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(this, sortedVideoStreams);
diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
index 1e5bd879959..50a3984e39d 100644
--- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
+++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
@@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
-import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
@@ -21,30 +20,28 @@ import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
class AboutActivity : AppCompatActivity() {
+
override fun onCreate(savedInstanceState: Bundle?) {
Localization.assureCorrectAppLanguage(this)
super.onCreate(savedInstanceState)
ThemeHelper.setTheme(this)
title = getString(R.string.title_activity_about)
+
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(aboutBinding.root)
setSupportActionBar(aboutBinding.aboutToolbar)
- supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
val mAboutStateAdapter = AboutStateAdapter(this)
-
// Set up the ViewPager with the sections adapter.
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
TabLayoutMediator(
aboutBinding.aboutTabLayout,
aboutBinding.aboutViewPager2
- ) { tab: TabLayout.Tab, position: Int ->
- when (position) {
- POS_ABOUT -> tab.setText(R.string.tab_about)
- POS_LICENSE -> tab.setText(R.string.tab_licenses)
- else -> throw IllegalArgumentException("Unknown position for ViewPager2")
- }
+ ) { tab, position ->
+ tab.setText(mAboutStateAdapter.getPageTitle(position))
}.attach()
}
@@ -75,13 +72,14 @@ class AboutActivity : AppCompatActivity() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- val aboutBinding = FragmentAboutBinding.inflate(inflater, container, false)
- aboutBinding.aboutAppVersion.text = BuildConfig.VERSION_NAME
- aboutBinding.aboutGithubLink.openLink(R.string.github_url)
- aboutBinding.aboutDonationLink.openLink(R.string.donation_url)
- aboutBinding.aboutWebsiteLink.openLink(R.string.website_url)
- aboutBinding.aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
- return aboutBinding.root
+ FragmentAboutBinding.inflate(inflater, container, false).apply {
+ aboutAppVersion.text = BuildConfig.VERSION_NAME
+ aboutGithubLink.openLink(R.string.github_url)
+ aboutDonationLink.openLink(R.string.donation_url)
+ aboutWebsiteLink.openLink(R.string.website_url)
+ aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
+ return root
+ }
}
}
@@ -90,17 +88,29 @@ class AboutActivity : AppCompatActivity() {
* one of the sections/tabs/pages.
*/
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
+ private val posAbout = 0
+ private val posLicense = 1
+ private val totalCount = 2
+
override fun createFragment(position: Int): Fragment {
return when (position) {
- POS_ABOUT -> AboutFragment()
- POS_LICENSE -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
+ posAbout -> AboutFragment()
+ posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
}
}
override fun getItemCount(): Int {
// Show 2 total pages.
- return TOTAL_COUNT
+ return totalCount
+ }
+
+ fun getPageTitle(position: Int): Int {
+ return when (position) {
+ posAbout -> R.string.tab_about
+ posLicense -> R.string.tab_licenses
+ else -> throw IllegalArgumentException("Unknown position for ViewPager2")
+ }
}
}
@@ -117,10 +127,6 @@ class AboutActivity : AppCompatActivity() {
"AndroidX", "2005 - 2011", "The Android Open Source Project",
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
),
- SoftwareComponent(
- "CircleImageView", "2014 - 2020", "Henning Dodenhof",
- "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2
- ),
SoftwareComponent(
"ExoPlayer", "2014 - 2020", "Google, Inc.",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
@@ -191,8 +197,5 @@ class AboutActivity : AppCompatActivity() {
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
),
)
- private const val POS_ABOUT = 0
- private const val POS_LICENSE = 1
- private const val TOTAL_COUNT = 2
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
index a04de8abc4d..c1dd38389c5 100644
--- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
+++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
@@ -87,60 +87,50 @@ object LicenseFragmentHelper {
return context.getString(color).substring(3)
}
- @JvmStatic
fun showLicense(context: Context?, license: License): Disposable {
- return if (context == null) {
- Disposable.empty()
- } else {
- Observable.fromCallable { getFormattedLicense(context, license) }
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe { formattedLicense: String ->
- val webViewData = Base64.encodeToString(
- formattedLicense
- .toByteArray(StandardCharsets.UTF_8),
- Base64.NO_PADDING
- )
- val webView = WebView(context)
- webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
- val alert = AlertDialog.Builder(context)
- alert.setTitle(license.name)
- alert.setView(webView)
- Localization.assureCorrectAppLanguage(context)
- alert.setNegativeButton(
- context.getString(R.string.ok)
- ) { dialog, _ -> dialog.dismiss() }
- alert.show()
- }
+ return showLicense(context, license) { alertDialog ->
+ alertDialog.setPositiveButton(R.string.ok) { dialog, _ ->
+ dialog.dismiss()
+ }
}
}
- @JvmStatic
+
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
+ return showLicense(context, component.license) { alertDialog ->
+ alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ ->
+ dialog.dismiss()
+ }
+ alertDialog.setNeutralButton(R.string.open_website_license) { _, _ ->
+ ShareUtils.openUrlInBrowser(context!!, component.link)
+ }
+ }
+ }
+
+ private fun showLicense(
+ context: Context?,
+ license: License,
+ block: (AlertDialog.Builder) -> Unit
+ ): Disposable {
return if (context == null) {
Disposable.empty()
} else {
- Observable.fromCallable { getFormattedLicense(context, component.license) }
+ Observable.fromCallable { getFormattedLicense(context, license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe { formattedLicense: String ->
+ .subscribe { formattedLicense ->
val webViewData = Base64.encodeToString(
- formattedLicense
- .toByteArray(StandardCharsets.UTF_8),
- Base64.NO_PADDING
+ formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING
)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
- val alert = AlertDialog.Builder(context)
- alert.setTitle(component.license.name)
- alert.setView(webView)
- Localization.assureCorrectAppLanguage(context)
- alert.setPositiveButton(
- R.string.dismiss
- ) { dialog, _ -> dialog.dismiss() }
- alert.setNeutralButton(R.string.open_website_license) { _, _ ->
- ShareUtils.openUrlInBrowser(context, component.link)
+
+ AlertDialog.Builder(context).apply {
+ setTitle(license.name)
+ setView(webView)
+ Localization.assureCorrectAppLanguage(context)
+ block(this)
+ show()
}
- alert.show()
}
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java
index 0a765ed4eec..150d4a8e5b5 100644
--- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java
+++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java
@@ -3,6 +3,7 @@
import androidx.annotation.Nullable;
import androidx.room.Dao;
import androidx.room.Query;
+import androidx.room.RewriteQueriesToDropUnusedColumns;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
@@ -67,6 +68,7 @@ public Flowable> listByService(final int serviceId) {
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteStreamHistory(long streamId);
+ @RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM " + STREAM_TABLE
// Select the latest entry and watch count for each stream id on history table
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java
index f4a0a758085..4941d939507 100644
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java
@@ -2,6 +2,7 @@
import androidx.room.Dao;
import androidx.room.Query;
+import androidx.room.RewriteQueriesToDropUnusedColumns;
import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
@@ -52,6 +53,7 @@ default Flowable> listByService(final int serviceId)
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
Flowable getMaximumIndexOf(long playlistId);
+ @RewriteQueriesToDropUnusedColumns
@Transaction
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
// get ids of streams of the given playlist
diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt
index 9798ec72dda..47b6f4dd9aa 100644
--- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt
+++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt
@@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
+import androidx.room.RewriteQueriesToDropUnusedColumns
import androidx.room.Transaction
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
@@ -31,6 +32,7 @@ abstract class SubscriptionDAO : BasicDAO {
)
abstract fun getSubscriptionsFiltered(filter: String): Flowable>
+ @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT * FROM subscriptions s
@@ -47,6 +49,7 @@ abstract class SubscriptionDAO : BasicDAO {
currentGroupId: Long
): Flowable>
+ @RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT * FROM subscriptions s
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index 5c954ad647d..f5c22690836 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -61,6 +61,7 @@
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SecondaryStreamHelper;
+import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper;
@@ -151,7 +152,7 @@ public static DownloadDialog newInstance(final StreamInfo info) {
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
final ArrayList streamsList = new ArrayList<>(ListHelper
.getSortedStreamVideosList(context, info.getVideoStreams(),
- info.getVideoOnlyStreams(), false));
+ info.getVideoOnlyStreams(), false, false));
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
final DownloadDialog instance = newInstance(info);
@@ -321,21 +322,15 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
dialogBinding.threadsCount.setText(String.valueOf(threads));
dialogBinding.threads.setProgress(threads - 1);
- dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
@Override
- public void onProgressChanged(final SeekBar seekbar, final int progress,
+ public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress,
final boolean fromUser) {
final int newProgress = progress + 1;
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
.apply();
dialogBinding.threadsCount.setText(String.valueOf(newProgress));
}
-
- @Override
- public void onStartTrackingTouch(final SeekBar p1) { }
-
- @Override
- public void onStopTrackingTouch(final SeekBar p1) { }
});
fetchStreamsSize();
diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.java
index 8e7a455b4d5..97617337382 100644
--- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java
+++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.java
@@ -29,8 +29,8 @@ public enum UserAction {
NEW_STREAMS_NOTIFICATIONS("new streams notifications"),
PREFERENCES_MIGRATION("migration of preferences"),
SHARE_TO_NEWPIPE("share to newpipe"),
- CHECK_FOR_NEW_APP_VERSION("check for new app version");
-
+ CHECK_FOR_NEW_APP_VERSION("check for new app version"),
+ OPEN_INFO_ITEM_DIALOG("open info item dialog");
private final String message;
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java
index cbd44566e38..6b17803c489 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java
@@ -1,5 +1,6 @@
package org.schabi.newpipe.fragments;
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
@@ -10,7 +11,7 @@
*/
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
@Override
- public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
+ public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
int pastVisibleItems = 0;
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java
index 2fe615764fc..5016a49f60c 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java
@@ -1,5 +1,7 @@
package org.schabi.newpipe.fragments.detail;
+import androidx.annotation.NonNull;
+
import org.schabi.newpipe.player.playqueue.PlayQueue;
import java.io.Serializable;
@@ -46,6 +48,7 @@ public PlayQueue getPlayQueue() {
return playQueue;
}
+ @NonNull
@Override
public String toString() {
return getServiceId() + ":" + getUrl() + " > " + getTitle();
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index 2d9abc6dc67..0af5ec99e57 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -1617,6 +1617,7 @@ public void handleResult(@NonNull final StreamInfo info) {
activity,
info.getVideoStreams(),
info.getVideoOnlyStreams(),
+ false,
false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);
@@ -1994,9 +1995,7 @@ private void showSystemUi() {
// Prevent jumping of the player on devices with cutout
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
- isMultiWindowOrFullscreen()
- ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
- : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
}
activity.getWindow().getDecorView().setSystemUiVisibility(0);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
@@ -2018,9 +2017,7 @@ private void hideSystemUi() {
// Prevent jumping of the player on devices with cutout
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
- isMultiWindowOrFullscreen()
- ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
- : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@@ -2037,7 +2034,7 @@ private void hideSystemUi() {
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
- && isMultiWindowOrFullscreen()) {
+ && (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
@@ -2053,11 +2050,6 @@ public void hideSystemUiIfNeeded() {
}
}
- private boolean isMultiWindowOrFullscreen() {
- return DeviceUtils.isInMultiWindow(activity)
- || (isPlayerAvailable() && player.isFullscreen());
- }
-
private boolean playerIsNotStopped() {
return isPlayerAvailable() && !player.isStopped();
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
index 3c2e65bb7da..27e5a8571e4 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
@@ -1,6 +1,8 @@
package org.schabi.newpipe.fragments.list;
-import android.app.Activity;
+import static org.schabi.newpipe.ktx.ViewUtils.animate;
+import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
+
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
@@ -17,37 +19,26 @@
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.error.ErrorUtil;
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.extractor.stream.StreamType;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
-import org.schabi.newpipe.info_list.InfoItemDialog;
+import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
-import org.schabi.newpipe.player.helper.PlayerHolder;
-import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver;
-import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.views.SuperScrollLayoutManager;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.Queue;
-
-import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
-import static org.schabi.newpipe.ktx.ViewUtils.animate;
-import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
+import java.util.function.Supplier;
public abstract class BaseListFragment extends BaseStateFragment
implements ListViewContract, StateSaver.WriteRead,
@@ -79,11 +70,6 @@ public void onAttach(@NonNull final Context context) {
}
}
- @Override
- public void onDetach() {
- super.onDetach();
- }
-
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -220,14 +206,10 @@ public void onStart() {
//////////////////////////////////////////////////////////////////////////*/
@Nullable
- protected ViewBinding getListHeader() {
+ protected Supplier getListHeaderSupplier() {
return null;
}
- protected ViewBinding getListFooter() {
- return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
- }
-
protected RecyclerView.LayoutManager getListLayoutManager() {
return new SuperScrollLayoutManager(activity);
}
@@ -252,11 +234,10 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) {
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setUseGridVariant(useGrid);
- infoListAdapter.setFooter(getListFooter().getRoot());
- final ViewBinding listHeader = getListHeader();
- if (listHeader != null) {
- infoListAdapter.setHeader(listHeader.getRoot());
+ final Supplier listHeaderSupplier = getListHeaderSupplier();
+ if (listHeaderSupplier != null) {
+ infoListAdapter.setHeaderSupplier(listHeaderSupplier);
}
itemsList.setAdapter(infoListAdapter);
@@ -271,7 +252,7 @@ protected void onItemSelected(final InfoItem selectedItem) {
@Override
protected void initListeners() {
super.initListeners();
- infoListAdapter.setOnStreamSelectedListener(new OnClickGesture() {
+ infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final StreamInfoItem selectedItem) {
onStreamSelected(selectedItem);
@@ -279,11 +260,11 @@ public void selected(final StreamInfoItem selectedItem) {
@Override
public void held(final StreamInfoItem selectedItem) {
- showStreamDialog(selectedItem);
+ showInfoItemDialog(selectedItem);
}
});
- infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() {
+ infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final ChannelInfoItem selectedItem) {
try {
@@ -299,7 +280,7 @@ public void selected(final ChannelInfoItem selectedItem) {
}
});
- infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() {
+ infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final PlaylistInfoItem selectedItem) {
try {
@@ -315,22 +296,99 @@ public void selected(final PlaylistInfoItem selectedItem) {
}
});
- infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture() {
+ infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final CommentsInfoItem selectedItem) {
onItemSelected(selectedItem);
}
});
+ // Ensure that there is always a scroll listener (e.g. when rotating the device)
+ useNormalItemListScrollListener();
+ }
+
+ /**
+ * Removes all listeners and adds the normal scroll listener to the {@link #itemsList}.
+ */
+ protected void useNormalItemListScrollListener() {
+ if (DEBUG) {
+ Log.d(TAG, "useNormalItemListScrollListener called");
+ }
+ itemsList.clearOnScrollListeners();
+ itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener());
+ }
+
+ /**
+ * Removes all listeners and adds the initial scroll listener to the {@link #itemsList}.
+ *
+ * Which tries to load more items when not enough are in the view (not scrollable)
+ * and more are available.
+ *
+ * Note: This method only works because "This callback will also be called if visible
+ * item range changes after a layout calculation. In that case, dx and dy will be 0."
+ * - which might be unexpected because no actual scrolling occurs...
+ *
+ * This listener will be replaced by DefaultItemListOnScrolledDownListener when
+ *
+ *
the view was actually scrolled
+ *
the view is scrollable
+ *
no more items can be loaded
+ *
+ */
+ protected void useInitialItemListLoadScrollListener() {
+ if (DEBUG) {
+ Log.d(TAG, "useInitialItemListLoadScrollListener called");
+ }
itemsList.clearOnScrollListeners();
- itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
+ itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener() {
@Override
- public void onScrolledDown(final RecyclerView recyclerView) {
- onScrollToBottom();
+ public void onScrolled(@NonNull final RecyclerView recyclerView,
+ final int dx, final int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+
+ if (dy != 0) {
+ log("Vertical scroll occurred");
+
+ useNormalItemListScrollListener();
+ return;
+ }
+ if (isLoading.get()) {
+ log("Still loading data -> Skipping");
+ return;
+ }
+ if (!hasMoreItems()) {
+ log("No more items to load");
+
+ useNormalItemListScrollListener();
+ return;
+ }
+ if (itemsList.canScrollVertically(1)
+ || itemsList.canScrollVertically(-1)) {
+ log("View is scrollable");
+
+ useNormalItemListScrollListener();
+ return;
+ }
+
+ log("Loading more data");
+ loadMoreItems();
+ }
+
+ private void log(final String msg) {
+ if (DEBUG) {
+ Log.d(TAG, "initItemListLoadScrollListener - " + msg);
+ }
}
});
}
+ class DefaultItemListOnScrolledDownListener extends OnScrollBelowItemsListener {
+ @Override
+ public void onScrolledDown(final RecyclerView recyclerView) {
+ onScrollToBottom();
+ }
+ }
+
private void onStreamSelected(final StreamInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
@@ -344,55 +402,12 @@ protected void onScrollToBottom() {
}
}
- protected void showStreamDialog(final StreamInfoItem item) {
- final Context context = getContext();
- final Activity activity = getActivity();
- if (context == null || context.getResources() == null || activity == null) {
- return;
- }
- final List entries = new ArrayList<>();
-
- if (PlayerHolder.getInstance().isPlayQueueReady()) {
- entries.add(StreamDialogEntry.enqueue);
-
- if (PlayerHolder.getInstance().getQueueSize() > 1) {
- entries.add(StreamDialogEntry.enqueue_next);
- }
- }
-
- if (item.getStreamType() == StreamType.AUDIO_STREAM) {
- entries.addAll(Arrays.asList(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share
- ));
- } else {
- entries.addAll(Arrays.asList(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.start_here_on_popup,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share
- ));
- }
- entries.add(StreamDialogEntry.open_in_browser);
- if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
- entries.add(StreamDialogEntry.play_with_kodi);
- }
-
- // show "mark as watched" only when watch history is enabled
- if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
- entries.add(
- StreamDialogEntry.mark_as_watched
- );
- }
- if (!isNullOrEmpty(item.getUploaderUrl())) {
- entries.add(StreamDialogEntry.show_channel_details);
+ protected void showInfoItemDialog(final StreamInfoItem item) {
+ try {
+ new InfoItemDialog.Builder(getActivity(), getContext(), this, item).create().show();
+ } catch (final IllegalArgumentException e) {
+ InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
}
-
- StreamDialogEntry.setEnabledEntries(entries);
-
- new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
- (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -418,6 +433,12 @@ public void onCreateOptionsMenu(@NonNull final Menu menu,
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
+ @Override
+ protected void startLoading(final boolean forceLoad) {
+ useInitialItemListLoadScrollListener();
+ super.startLoading(forceLoad);
+ }
+
protected abstract void loadMoreItems();
protected abstract boolean hasMoreItems();
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java
index e98dc9fdadf..35424437da7 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java
@@ -9,6 +9,7 @@
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
+import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
@@ -27,8 +28,8 @@
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
-public abstract class BaseListInfoFragment
- extends BaseListFragment {
+public abstract class BaseListInfoFragment>
+ extends BaseListFragment> {
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
@@ -37,7 +38,7 @@ public abstract class BaseListInfoFragment
protected String url;
private final UserAction errorUserAction;
- protected I currentInfo;
+ protected L currentInfo;
protected Page currentNextPage;
protected Disposable currentWorker;
@@ -65,7 +66,7 @@ public void onResume() {
super.onResume();
// Check if it was loading when the fragment was stopped/paused,
if (wasLoading.getAndSet(false)) {
- if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) {
+ if (hasMoreItems() && !infoListAdapter.getItemsList().isEmpty()) {
loadMoreItems();
} else {
doInitialLoadLogic();
@@ -97,7 +98,7 @@ public void writeTo(final Queue
*/
-public class KioskFragment extends BaseListInfoFragment {
+public class KioskFragment extends BaseListInfoFragment {
@State
String kioskId = "";
String kioskTranslatedName;
@@ -145,7 +146,7 @@ public Single loadResult(final boolean forceReload) {
}
@Override
- public Single loadMoreItemsLogic() {
+ public Single> loadMoreItemsLogic() {
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage);
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
index 640d08064d7..5bf20c144f3 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
@@ -1,7 +1,10 @@
package org.schabi.newpipe.fragments.list.playlist;
-import android.app.Activity;
+import static org.schabi.newpipe.ktx.ViewUtils.animate;
+import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
+
import android.content.Context;
+import android.content.res.ColorStateList;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@@ -15,7 +18,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
-import androidx.viewbinding.ViewBinding;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.shape.CornerFamily;
+import com.google.android.material.shape.ShapeAppearanceModel;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@@ -33,26 +39,23 @@
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
-import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
-import org.schabi.newpipe.info_list.InfoItemDialog;
+import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
-import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
-import org.schabi.newpipe.util.PicassoHelper;
-import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
+import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.util.external_communication.ShareUtils;
-import org.schabi.newpipe.util.StreamDialogEntry;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
@@ -60,11 +63,7 @@
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
-import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
-import static org.schabi.newpipe.ktx.ViewUtils.animate;
-import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
-
-public class PlaylistFragment extends BaseListInfoFragment {
+public class PlaylistFragment extends BaseListInfoFragment {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
@@ -120,12 +119,12 @@ public View onCreateView(@NonNull final LayoutInflater inflater,
//////////////////////////////////////////////////////////////////////////*/
@Override
- protected ViewBinding getListHeader() {
+ protected Supplier getListHeaderSupplier() {
headerBinding = PlaylistHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
- return headerBinding;
+ return headerBinding::getRoot;
}
@Override
@@ -140,60 +139,22 @@ private PlayQueue getPlayQueueStartingAt(final StreamInfoItem infoItem) {
}
@Override
- protected void showStreamDialog(final StreamInfoItem item) {
+ protected void showInfoItemDialog(final StreamInfoItem item) {
final Context context = getContext();
- final Activity activity = getActivity();
- if (context == null || context.getResources() == null || activity == null) {
- return;
- }
-
- final ArrayList entries = new ArrayList<>();
-
- if (PlayerHolder.getInstance().isPlayQueueReady()) {
- entries.add(StreamDialogEntry.enqueue);
-
- if (PlayerHolder.getInstance().getQueueSize() > 1) {
- entries.add(StreamDialogEntry.enqueue_next);
- }
- }
-
- if (item.getStreamType() == StreamType.AUDIO_STREAM) {
- entries.addAll(Arrays.asList(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share
- ));
- } else {
- entries.addAll(Arrays.asList(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.start_here_on_popup,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share
- ));
+ try {
+ final InfoItemDialog.Builder dialogBuilder =
+ new InfoItemDialog.Builder(getActivity(), context, this, item);
+
+ dialogBuilder
+ .setAction(
+ StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
+ (f, infoItem) -> NavigationHelper.playOnBackgroundPlayer(
+ context, getPlayQueueStartingAt(infoItem), true))
+ .create()
+ .show();
+ } catch (final IllegalArgumentException e) {
+ InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
}
- entries.add(StreamDialogEntry.open_in_browser);
- if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
- entries.add(StreamDialogEntry.play_with_kodi);
- }
-
- // show "mark as watched" only when watch history is enabled
- if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
- entries.add(
- StreamDialogEntry.mark_as_watched
- );
- }
- if (!isNullOrEmpty(item.getUploaderUrl())) {
- entries.add(StreamDialogEntry.show_channel_details);
- }
-
- StreamDialogEntry.setEnabledEntries(entries);
-
- StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
- NavigationHelper.playOnBackgroundPlayer(context,
- getPlayQueueStartingAt(infoItem), true));
-
- new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
- (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
}
@Override
@@ -249,7 +210,7 @@ public void onDestroy() {
//////////////////////////////////////////////////////////////////////////*/
@Override
- protected Single loadMoreItemsLogic() {
+ protected Single> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
}
@@ -328,9 +289,14 @@ public void handleResult(@NonNull final PlaylistInfo result) {
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
- headerBinding.uploaderAvatarView.setDisableCircularTransformation(true);
- headerBinding.uploaderAvatarView.setBorderColor(
- getResources().getColor(R.color.transparent_background_color));
+ final ShapeAppearanceModel model = ShapeAppearanceModel.builder()
+ .setAllCorners(CornerFamily.ROUNDED, 0f)
+ .build(); // this turns the image back into a square
+ headerBinding.uploaderAvatarView.setShapeAppearanceModel(model);
+ headerBinding.uploaderAvatarView.setStrokeColor(
+ ColorStateList.valueOf(ContextCompat.getColor(
+ requireContext(), R.color.transparent_background_color))
+ );
headerBinding.uploaderAvatarView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(),
R.drawable.ic_radio)
@@ -413,7 +379,7 @@ private Flowable getUpdateProcessor(
}
private Subscriber> getPlaylistBookmarkSubscriber() {
- return new Subscriber>() {
+ return new Subscriber<>() {
@Override
public void onSubscribe(final Subscription s) {
if (bookmarkReactor != null) {
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java
index 3cfcfd47086..fb983b01e26 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java
@@ -7,6 +7,7 @@
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
@@ -34,8 +35,10 @@ public void setListener(final OnSuggestionItemSelected listener) {
this.listener = listener;
}
+ @NonNull
@Override
- public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
+ public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
+ final int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context)
.inflate(R.layout.item_search_suggestion, parent, false));
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java
index 6532417c064..f0ece69f37c 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java
@@ -1,6 +1,5 @@
package org.schabi.newpipe.fragments.list.videos;
-import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -12,11 +11,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
-import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
import org.schabi.newpipe.error.UserAction;
+import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
@@ -24,14 +23,14 @@
import org.schabi.newpipe.util.RelatedItemInfo;
import java.io.Serializable;
+import java.util.function.Supplier;
import io.reactivex.rxjava3.core.Single;
-import io.reactivex.rxjava3.disposables.CompositeDisposable;
-public class RelatedItemsFragment extends BaseListInfoFragment
+public class RelatedItemsFragment extends BaseListInfoFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String INFO_KEY = "related_info_key";
- private final CompositeDisposable disposables = new CompositeDisposable();
+
private RelatedItemInfo relatedItemInfo;
/*//////////////////////////////////////////////////////////////////////////
@@ -54,11 +53,6 @@ public RelatedItemsFragment() {
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
- @Override
- public void onAttach(@NonNull final Context context) {
- super.onAttach(context);
- }
-
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@@ -66,12 +60,6 @@ public View onCreateView(@NonNull final LayoutInflater inflater,
return inflater.inflate(R.layout.fragment_related_items, container, false);
}
- @Override
- public void onDestroy() {
- super.onDestroy();
- disposables.clear();
- }
-
@Override
public void onDestroyView() {
headerBinding = null;
@@ -79,26 +67,27 @@ public void onDestroyView() {
}
@Override
- protected ViewBinding getListHeader() {
- if (relatedItemInfo != null && relatedItemInfo.getRelatedItems() != null) {
- headerBinding = RelatedItemsHeaderBinding
- .inflate(activity.getLayoutInflater(), itemsList, false);
-
- final SharedPreferences pref = PreferenceManager
- .getDefaultSharedPreferences(requireContext());
- final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
- headerBinding.autoplaySwitch.setChecked(autoplay);
- headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
- PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
- .putBoolean(getString(R.string.auto_queue_key), b).apply());
- return headerBinding;
- } else {
+ protected Supplier getListHeaderSupplier() {
+ if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
return null;
}
+
+ headerBinding = RelatedItemsHeaderBinding
+ .inflate(activity.getLayoutInflater(), itemsList, false);
+
+ final SharedPreferences pref = PreferenceManager
+ .getDefaultSharedPreferences(requireContext());
+ final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
+ headerBinding.autoplaySwitch.setChecked(autoplay);
+ headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
+ PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
+ .putBoolean(getString(R.string.auto_queue_key), b).apply());
+
+ return headerBinding::getRoot;
}
@Override
- protected Single loadMoreItemsLogic() {
+ protected Single> loadMoreItemsLogic() {
return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
}
@@ -128,7 +117,6 @@ public void handleResult(@NonNull final RelatedItemInfo result) {
}
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
- disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -137,11 +125,13 @@ public void handleResult(@NonNull final RelatedItemInfo result) {
@Override
public void setTitle(final String title) {
+ // Nothing to do - override parent
}
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
+ // Nothing to do - override parent
}
private void setInitialData(final StreamInfo info) {
@@ -169,11 +159,10 @@ protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String s) {
- final SharedPreferences pref =
- PreferenceManager.getDefaultSharedPreferences(requireContext());
- final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
if (headerBinding != null) {
- headerBinding.autoplaySwitch.setChecked(autoplay);
+ headerBinding.autoplaySwitch.setChecked(
+ sharedPreferences.getBoolean(
+ getString(R.string.auto_queue_key), false));
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java
deleted file mode 100644
index c485337f0fc..00000000000
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package org.schabi.newpipe.info_list;
-
-import android.app.Activity;
-import android.content.DialogInterface;
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-
-import org.schabi.newpipe.R;
-import org.schabi.newpipe.extractor.stream.StreamInfoItem;
-
-public class InfoItemDialog {
- private final AlertDialog dialog;
-
- public InfoItemDialog(@NonNull final Activity activity,
- @NonNull final StreamInfoItem info,
- @NonNull final String[] commands,
- @NonNull final DialogInterface.OnClickListener actions) {
- this(activity, commands, actions, info.getName(), info.getUploaderName());
- }
-
- public InfoItemDialog(@NonNull final Activity activity,
- @NonNull final String[] commands,
- @NonNull final DialogInterface.OnClickListener actions,
- @NonNull final String title,
- @Nullable final String additionalDetail) {
-
- final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
- bannerView.setSelected(true);
-
- final TextView titleView = bannerView.findViewById(R.id.itemTitleView);
- titleView.setText(title);
-
- final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
- if (additionalDetail != null) {
- detailsView.setText(additionalDetail);
- detailsView.setVisibility(View.VISIBLE);
- } else {
- detailsView.setVisibility(View.GONE);
- }
-
- dialog = new AlertDialog.Builder(activity)
- .setCustomTitle(bannerView)
- .setItems(commands, actions)
- .create();
- }
-
- public void show() {
- dialog.show();
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
index 56bc633843c..fb27593e7e0 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java
@@ -2,6 +2,7 @@
import android.content.Context;
import android.util.Log;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -10,7 +11,7 @@
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import org.schabi.newpipe.database.stream.model.StreamStateEntity;
+import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
@@ -34,6 +35,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Supplier;
/*
* Created by Christian Schabesberger on 01.08.16.
@@ -74,18 +76,20 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList;
+ private final List infoItemList;
private final HistoryRecordManager recordManager;
private boolean useMiniVariant = false;
private boolean useGridVariant = false;
private boolean showFooter = false;
- private View header = null;
- private View footer = null;
+
+ private Supplier headerSupplier = null;
public InfoListAdapter(final Context context) {
- this.recordManager = new HistoryRecordManager(context);
+ layoutInflater = LayoutInflater.from(context);
+ recordManager = new HistoryRecordManager(context);
infoItemBuilder = new InfoItemBuilder(context);
infoItemList = new ArrayList<>();
}
@@ -129,12 +133,12 @@ public void addInfoItemList(@Nullable final List extends InfoItem> data) {
if (DEBUG) {
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
+ "infoItemList.size() = " + infoItemList.size() + ", "
- + "header = " + header + ", footer = " + footer + ", "
+ + "hasHeader = " + hasHeader() + ", "
+ "showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
- if (footer != null && showFooter) {
+ if (showFooter) {
final int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(offsetStart, footerNow);
@@ -145,43 +149,6 @@ public void addInfoItemList(@Nullable final List extends InfoItem> data) {
}
}
- public void setInfoItemList(final List extends InfoItem> data) {
- infoItemList.clear();
- infoItemList.addAll(data);
- notifyDataSetChanged();
- }
-
- public void addInfoItem(@Nullable final InfoItem data) {
- if (data == null) {
- return;
- }
- if (DEBUG) {
- Log.d(TAG, "addInfoItem() before > infoItemList.size() = "
- + infoItemList.size() + ", thread = " + Thread.currentThread());
- }
-
- final int positionInserted = sizeConsideringHeaderOffset();
- infoItemList.add(data);
-
- if (DEBUG) {
- Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", "
- + "infoItemList.size() = " + infoItemList.size() + ", "
- + "header = " + header + ", footer = " + footer + ", "
- + "showFooter = " + showFooter);
- }
- notifyItemInserted(positionInserted);
-
- if (footer != null && showFooter) {
- final int footerNow = sizeConsideringHeaderOffset();
- notifyItemMoved(positionInserted, footerNow);
-
- if (DEBUG) {
- Log.d(TAG, "addInfoItem() footer from " + positionInserted
- + " to " + footerNow);
- }
- }
- }
-
public void clearStreamItemList() {
if (infoItemList.isEmpty()) {
return;
@@ -190,16 +157,16 @@ public void clearStreamItemList() {
notifyDataSetChanged();
}
- public void setHeader(final View header) {
- final boolean changed = header != this.header;
- this.header = header;
+ public void setHeaderSupplier(@Nullable final Supplier headerSupplier) {
+ final boolean changed = headerSupplier != this.headerSupplier;
+ this.headerSupplier = headerSupplier;
if (changed) {
notifyDataSetChanged();
}
}
- public void setFooter(final View view) {
- this.footer = view;
+ protected boolean hasHeader() {
+ return this.headerSupplier != null;
}
public void showFooter(final boolean show) {
@@ -219,48 +186,49 @@ public void showFooter(final boolean show) {
}
private int sizeConsideringHeaderOffset() {
- final int i = infoItemList.size() + (header != null ? 1 : 0);
+ final int i = infoItemList.size() + (hasHeader() ? 1 : 0);
if (DEBUG) {
Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
}
return i;
}
- public ArrayList getItemsList() {
+ public List getItemsList() {
return infoItemList;
}
@Override
public int getItemCount() {
int count = infoItemList.size();
- if (header != null) {
+ if (hasHeader()) {
count++;
}
- if (footer != null && showFooter) {
+ if (showFooter) {
count++;
}
if (DEBUG) {
Log.d(TAG, "getItemCount() called with: "
+ "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", "
- + "header = " + header + ", footer = " + footer + ", "
+ + "hasHeader = " + hasHeader() + ", "
+ "showFooter = " + showFooter);
}
return count;
}
+ @SuppressWarnings("FinalParameters")
@Override
public int getItemViewType(int position) {
if (DEBUG) {
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
}
- if (header != null && position == 0) {
+ if (hasHeader() && position == 0) {
return HEADER_TYPE;
- } else if (header != null) {
+ } else if (hasHeader()) {
position--;
}
- if (footer != null && position == infoItemList.size() && showFooter) {
+ if (position == infoItemList.size() && showFooter) {
return FOOTER_TYPE;
}
final InfoItem item = infoItemList.get(position);
@@ -290,10 +258,16 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup paren
+ "parent = [" + parent + "], type = [" + type + "]");
}
switch (type) {
+ // #4475 and #3368
+ // Always create a new instance otherwise the same instance
+ // is sometimes reused which causes a crash
case HEADER_TYPE:
- return new HFHolder(header);
+ return new HFHolder(headerSupplier.get());
case FOOTER_TYPE:
- return new HFHolder(footer);
+ return new HFHolder(PignateFooterBinding
+ .inflate(layoutInflater, parent, false)
+ .getRoot()
+ );
case MINI_STREAM_HOLDER_TYPE:
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
case STREAM_HOLDER_TYPE:
@@ -322,42 +296,17 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup paren
}
@Override
- public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
+ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
+ final int position) {
if (DEBUG) {
Log.d(TAG, "onBindViewHolder() called with: "
+ "holder = [" + holder.getClass().getSimpleName() + "], "
+ "position = [" + position + "]");
}
if (holder instanceof InfoItemHolder) {
- // If header isn't null, offset the items by -1
- if (header != null) {
- position--;
- }
-
- ((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
- } else if (holder instanceof HFHolder && position == 0 && header != null) {
- ((HFHolder) holder).view = header;
- } else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset()
- && footer != null && showFooter) {
- ((HFHolder) holder).view = footer;
- }
- }
-
- @Override
- public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position,
- @NonNull final List payloads) {
- if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
- for (final Object payload : payloads) {
- if (payload instanceof StreamStateEntity) {
- ((InfoItemHolder) holder).updateState(infoItemList
- .get(header == null ? position : position - 1), recordManager);
- } else if (payload instanceof Boolean) {
- ((InfoItemHolder) holder).updateState(infoItemList
- .get(header == null ? position : position - 1), recordManager);
- }
- }
- } else {
- onBindViewHolder(holder, position);
+ ((InfoItemHolder) holder).updateFromItem(
+ // If header is present, offset the items by -1
+ infoItemList.get(hasHeader() ? position - 1 : position), recordManager);
}
}
@@ -371,12 +320,9 @@ public int getSpanSize(final int position) {
};
}
- public static class HFHolder extends RecyclerView.ViewHolder {
- public View view;
-
+ static class HFHolder extends RecyclerView.ViewHolder {
HFHolder(final View v) {
super(v);
- view = v;
}
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
new file mode 100644
index 00000000000..5a266c0a860
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
@@ -0,0 +1,356 @@
+package org.schabi.newpipe.info_list.dialog;
+
+import static org.schabi.newpipe.MainActivity.DEBUG;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
+
+import org.schabi.newpipe.App;
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.error.ErrorInfo;
+import org.schabi.newpipe.error.ErrorUtil;
+import org.schabi.newpipe.error.UserAction;
+import org.schabi.newpipe.extractor.InfoItem;
+import org.schabi.newpipe.extractor.stream.StreamInfoItem;
+import org.schabi.newpipe.extractor.stream.StreamType;
+import org.schabi.newpipe.player.helper.PlayerHolder;
+import org.schabi.newpipe.util.external_communication.KoreUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * Dialog for a {@link StreamInfoItem}.
+ * The dialog's content are actions that can be performed on the {@link StreamInfoItem}.
+ * This dialog is mostly used for longpress context menus.
+ */
+public final class InfoItemDialog {
+ private static final String TAG = Build.class.getSimpleName();
+ /**
+ * Ideally, {@link InfoItemDialog} would extend {@link AlertDialog}.
+ * However, extending {@link AlertDialog} requires many additional lines
+ * and brings more complexity to this class, especially the constructor.
+ * To circumvent this, an {@link AlertDialog.Builder} is used in the constructor.
+ * Its result is stored in this class variable to allow access via the {@link #show()} method.
+ */
+ private final AlertDialog dialog;
+
+ private InfoItemDialog(@NonNull final Activity activity,
+ @NonNull final Fragment fragment,
+ @NonNull final StreamInfoItem info,
+ @NonNull final List entries) {
+
+ // Create the dialog's title
+ final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
+ bannerView.setSelected(true);
+
+ final TextView titleView = bannerView.findViewById(R.id.itemTitleView);
+ titleView.setText(info.getName());
+
+ final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
+ if (info.getUploaderName() != null) {
+ detailsView.setText(info.getUploaderName());
+ detailsView.setVisibility(View.VISIBLE);
+ } else {
+ detailsView.setVisibility(View.GONE);
+ }
+
+ // Get the entry's descriptions which are displayed in the dialog
+ final String[] items = entries.stream()
+ .map(entry -> entry.getString(activity)).toArray(String[]::new);
+
+ // Call an entry's action / onClick method when the entry is selected.
+ final DialogInterface.OnClickListener action = (d, index) ->
+ entries.get(index).action.onClick(fragment, info);
+
+ dialog = new AlertDialog.Builder(activity)
+ .setCustomTitle(bannerView)
+ .setItems(items, action)
+ .create();
+
+ }
+
+ public void show() {
+ dialog.show();
+ }
+
+ /**
+ *
Builder to generate a {@link InfoItemDialog} for a {@link StreamInfoItem}.
+ * Use {@link #addEntry(StreamDialogDefaultEntry)}
+ * and {@link #addAllEntries(StreamDialogDefaultEntry...)} to add options to the dialog.
+ *
+ * Custom actions for entries can be set using
+ * {@link #setAction(StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}.
+ */
+ public static class Builder {
+ @NonNull private final Activity activity;
+ @NonNull private final Context context;
+ @NonNull private final StreamInfoItem infoItem;
+ @NonNull private final Fragment fragment;
+ @NonNull private final List entries = new ArrayList<>();
+ private final boolean addDefaultEntriesAutomatically;
+
+ /**
+ *
Create a {@link Builder builder} instance for a {@link StreamInfoItem}
+ * that automatically adds the some default entries
+ * at the top and bottom of the dialog.
+ * Please note that some entries are not added depending on the user's preferences,
+ * the item's {@link StreamType} and the current player state.
+ *
+ * @param activity
+ * @param context
+ * @param fragment
+ * @param infoItem the item for this dialog; all entries and their actions work with
+ * this {@link StreamInfoItem}
+ * @throws IllegalArgumentException if activity, context
+ * or resources is null
+ */
+ public Builder(final Activity activity,
+ final Context context,
+ @NonNull final Fragment fragment,
+ @NonNull final StreamInfoItem infoItem) {
+ this(activity, context, fragment, infoItem, true);
+ }
+
+ /**
+ *
Create an instance of this {@link Builder} for a {@link StreamInfoItem}.
+ *
If {@code addDefaultEntriesAutomatically} is set to {@code true},
+ * some default entries are added to the top and bottom of the dialog.
+ * Please note that some entries are not added depending on the user's preferences,
+ * the item's {@link StreamType} and the current player state.
+ *
+ * @param activity
+ * @param context
+ * @param fragment
+ * @param infoItem
+ * @param addDefaultEntriesAutomatically
+ * whether default entries added with {@link #addDefaultBeginningEntries()}
+ * and {@link #addDefaultEndEntries()} are added automatically when generating
+ * the {@link InfoItemDialog}.
+ *
+ * Entries added with {@link #addEntry(StreamDialogDefaultEntry)} and
+ * {@link #addAllEntries(StreamDialogDefaultEntry...)} are added in between.
+ * @throws IllegalArgumentException if activity, context
+ * or resources is null
+ */
+ public Builder(final Activity activity,
+ final Context context,
+ @NonNull final Fragment fragment,
+ @NonNull final StreamInfoItem infoItem,
+ final boolean addDefaultEntriesAutomatically) {
+ if (activity == null || context == null || context.getResources() == null) {
+ if (DEBUG) {
+ Log.d(TAG, "activity, context or resources is null: activity = "
+ + activity + ", context = " + context);
+ }
+ throw new IllegalArgumentException("activity, context or resources is null");
+ }
+ this.activity = activity;
+ this.context = context;
+ this.fragment = fragment;
+ this.infoItem = infoItem;
+ this.addDefaultEntriesAutomatically = addDefaultEntriesAutomatically;
+ if (addDefaultEntriesAutomatically) {
+ addDefaultBeginningEntries();
+ }
+ }
+
+ /**
+ * Adds a new entry and appends it to the current entry list.
+ * @param entry the entry to add
+ * @return the current {@link Builder} instance
+ */
+ public Builder addEntry(@NonNull final StreamDialogDefaultEntry entry) {
+ entries.add(entry.toStreamDialogEntry());
+ return this;
+ }
+
+ /**
+ * Adds new entries. These are appended to the current entry list.
+ * @param newEntries the entries to add
+ * @return the current {@link Builder} instance
+ */
+ public Builder addAllEntries(@NonNull final StreamDialogDefaultEntry... newEntries) {
+ Stream.of(newEntries).forEach(this::addEntry);
+ return this;
+ }
+
+ /**
+ *
Change an entries' action that is called when the entry is selected.
+ *
Warning: Only use this method when the entry has been already added.
+ * Changing the action of an entry which has not been added to the Builder yet
+ * does not have an effect.
+ * @param entry the entry to change
+ * @param action the action to perform when the entry is selected
+ * @return the current {@link Builder} instance
+ */
+ public Builder setAction(@NonNull final StreamDialogDefaultEntry entry,
+ @NonNull final StreamDialogEntry.StreamDialogEntryAction action) {
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).resource == entry.resource) {
+ entries.set(i, new StreamDialogEntry(entry.resource, action));
+ return this;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds {@link StreamDialogDefaultEntry#ENQUEUE} if the player is open and
+ * {@link StreamDialogDefaultEntry#ENQUEUE_NEXT} if there are multiple streams
+ * in the play queue.
+ * @return the current {@link Builder} instance
+ */
+ public Builder addEnqueueEntriesIfNeeded() {
+ if (PlayerHolder.getInstance().isPlayQueueReady()) {
+ addEntry(StreamDialogDefaultEntry.ENQUEUE);
+
+ if (PlayerHolder.getInstance().getQueueSize() > 1) {
+ addEntry(StreamDialogDefaultEntry.ENQUEUE_NEXT);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds the {@link StreamDialogDefaultEntry#START_HERE_ON_BACKGROUND}.
+ * If the {@link #infoItem} is not a pure audio (live) stream,
+ * {@link StreamDialogDefaultEntry#START_HERE_ON_POPUP} is added, too.
+ * @return the current {@link Builder} instance
+ */
+ public Builder addStartHereEntries() {
+ addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
+ if (infoItem.getStreamType() != StreamType.AUDIO_STREAM
+ && infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
+ addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
+ }
+ return this;
+ }
+
+ /**
+ * Adds {@link StreamDialogDefaultEntry#MARK_AS_WATCHED} if the watch history is enabled
+ * and the stream is not a livestream.
+ * @return the current {@link Builder} instance
+ */
+ public Builder addMarkAsWatchedEntryIfNeeded() {
+ final boolean isWatchHistoryEnabled = PreferenceManager
+ .getDefaultSharedPreferences(context)
+ .getBoolean(context.getString(R.string.enable_watch_history_key), false);
+ if (isWatchHistoryEnabled
+ && infoItem.getStreamType() != StreamType.LIVE_STREAM
+ && infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
+ addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
+ }
+ return this;
+ }
+
+ /**
+ * Adds the {@link StreamDialogDefaultEntry#PLAY_WITH_KODI} entry if it is needed.
+ * @return the current {@link Builder} instance
+ */
+ public Builder addPlayWithKodiEntryIfNeeded() {
+ if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
+ addEntry(StreamDialogDefaultEntry.PLAY_WITH_KODI);
+ }
+ return this;
+ }
+
+ /**
+ * Add the entries which are usually at the top of the action list.
+ *
+ * This method adds the "enqueue" (see {@link #addEnqueueEntriesIfNeeded()})
+ * and "start here" (see {@link #addStartHereEntries()} entries.
+ * @return the current {@link Builder} instance
+ */
+ public Builder addDefaultBeginningEntries() {
+ addEnqueueEntriesIfNeeded();
+ addStartHereEntries();
+ return this;
+ }
+
+ /**
+ * Add the entries which are usually at the bottom of the action list.
+ * @return the current {@link Builder} instance
+ */
+ public Builder addDefaultEndEntries() {
+ addAllEntries(
+ StreamDialogDefaultEntry.APPEND_PLAYLIST,
+ StreamDialogDefaultEntry.SHARE,
+ StreamDialogDefaultEntry.OPEN_IN_BROWSER
+ );
+ addPlayWithKodiEntryIfNeeded();
+ addMarkAsWatchedEntryIfNeeded();
+ addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS);
+ return this;
+ }
+
+ /**
+ * Creates the {@link InfoItemDialog}.
+ * @return a new instance of {@link InfoItemDialog}
+ */
+ public InfoItemDialog create() {
+ if (addDefaultEntriesAutomatically) {
+ addDefaultEndEntries();
+ }
+ return new InfoItemDialog(this.activity, this.fragment, this.infoItem, this.entries);
+ }
+
+ public static void reportErrorDuringInitialization(final Throwable throwable,
+ final InfoItem item) {
+ ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
+ throwable,
+ UserAction.OPEN_INFO_ITEM_DIALOG,
+ "none",
+ item.getServiceId()));
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java
new file mode 100644
index 00000000000..7e87318ee39
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java
@@ -0,0 +1,142 @@
+package org.schabi.newpipe.info_list.dialog;
+
+import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment;
+import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse;
+import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.database.stream.model.StreamEntity;
+import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
+import org.schabi.newpipe.local.dialog.PlaylistDialog;
+import org.schabi.newpipe.local.history.HistoryRecordManager;
+import org.schabi.newpipe.util.NavigationHelper;
+import org.schabi.newpipe.util.external_communication.KoreUtils;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+
+import java.util.Collections;
+
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+
+/**
+ *
+ * This enum provides entries that are accepted
+ * by the {@link InfoItemDialog.Builder}.
+ *
+ *
+ * These entries contain a String {@link #resource} which is displayed in the dialog and
+ * a default {@link #action} that is executed
+ * when the entry is selected (via onClick()).
+ *
+ * They action can be overridden by using the Builder's
+ * {@link InfoItemDialog.Builder#setAction(
+ * StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}
+ * method.
+ *
+ */
+public enum StreamDialogDefaultEntry {
+ SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) ->
+ fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(),
+ item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url))
+ ),
+
+ /**
+ * Enqueues the stream automatically to the current PlayerType.
+ */
+ ENQUEUE(R.string.enqueue_stream, (fragment, item) ->
+ fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
+ NavigationHelper.enqueueOnPlayer(fragment.getContext(), singlePlayQueue))
+ ),
+
+ /**
+ * Enqueues the stream automatically to the current PlayerType
+ * after the currently playing stream.
+ */
+ ENQUEUE_NEXT(R.string.enqueue_next_stream, (fragment, item) ->
+ fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
+ NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), singlePlayQueue))
+ ),
+
+ START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragment, item) ->
+ fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
+ NavigationHelper.playOnBackgroundPlayer(
+ fragment.getContext(), singlePlayQueue, true))),
+
+ START_HERE_ON_POPUP(R.string.start_here_on_popup, (fragment, item) ->
+ fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue ->
+ NavigationHelper.playOnPopupPlayer(fragment.getContext(), singlePlayQueue, true))),
+
+ SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragment, item) -> {
+ throw new UnsupportedOperationException("This needs to be implemented manually "
+ + "by using InfoItemDialog.Builder.setAction()");
+ }),
+
+ DELETE(R.string.delete, (fragment, item) -> {
+ throw new UnsupportedOperationException("This needs to be implemented manually "
+ + "by using InfoItemDialog.Builder.setAction()");
+ }),
+
+ /**
+ * Opens a {@link PlaylistDialog} to either append the stream to a playlist
+ * or create a new playlist if there are no local playlists.
+ */
+ APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
+ PlaylistDialog.createCorrespondingDialog(
+ fragment.getContext(),
+ Collections.singletonList(new StreamEntity(item)),
+ dialog -> dialog.show(
+ fragment.getParentFragmentManager(),
+ "StreamDialogEntry@"
+ + (dialog instanceof PlaylistAppendDialog ? "append" : "create")
+ + "_playlist"
+ )
+ )
+ ),
+
+ PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> {
+ final Uri videoUrl = Uri.parse(item.getUrl());
+ try {
+ NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
+ } catch (final Exception e) {
+ KoreUtils.showInstallKoreDialog(fragment.requireActivity());
+ }
+ }),
+
+ SHARE(R.string.share, (fragment, item) ->
+ ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
+ item.getThumbnailUrl())),
+
+ OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
+ ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
+
+
+ MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) ->
+ new HistoryRecordManager(fragment.getContext())
+ .markAsWatched(item)
+ .onErrorComplete()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe()
+ );
+
+
+ @StringRes
+ public final int resource;
+ @NonNull
+ public final StreamDialogEntry.StreamDialogEntryAction action;
+
+ StreamDialogDefaultEntry(@StringRes final int resource,
+ @NonNull final StreamDialogEntry.StreamDialogEntryAction action) {
+ this.resource = resource;
+ this.action = action;
+ }
+
+ @NonNull
+ public StreamDialogEntry toStreamDialogEntry() {
+ return new StreamDialogEntry(resource, action);
+ }
+
+}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java
new file mode 100644
index 00000000000..9d82e3b5829
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java
@@ -0,0 +1,31 @@
+package org.schabi.newpipe.info_list.dialog;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.Fragment;
+
+import org.schabi.newpipe.extractor.stream.StreamInfoItem;
+
+public class StreamDialogEntry {
+
+ @StringRes
+ public final int resource;
+ @NonNull
+ public final StreamDialogEntryAction action;
+
+ public StreamDialogEntry(@StringRes final int resource,
+ @NonNull final StreamDialogEntryAction action) {
+ this.resource = resource;
+ this.action = action;
+ }
+
+ public String getString(@NonNull final Context context) {
+ return context.getString(resource);
+ }
+
+ public interface StreamDialogEntryAction {
+ void onClick(Fragment fragment, StreamInfoItem infoItem);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
index 78acb752b54..aa4f4c9f023 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
@@ -1,6 +1,7 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
+import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
@@ -11,10 +12,8 @@
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization;
-import de.hdodenhof.circleimageview.CircleImageView;
-
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
- public final CircleImageView itemThumbnailView;
+ public final ImageView itemThumbnailView;
public final TextView itemTitleView;
private final TextView itemAdditionalDetailView;
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java
index cb47efa9252..6e4773c09d4 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java
@@ -7,6 +7,7 @@
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
@@ -28,8 +29,6 @@
import java.util.regex.Matcher;
-import de.hdodenhof.circleimageview.CircleImageView;
-
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final String TAG = "CommentsMiniIIHolder";
@@ -40,7 +39,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private final int commentVerticalPadding;
private final RelativeLayout itemRoot;
- public final CircleImageView itemThumbnailView;
+ public final ImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemPublishedTime;
diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java
index 5d81c0069c0..05e2fdac083 100644
--- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java
@@ -228,6 +228,7 @@ public int getItemCount() {
return count;
}
+ @SuppressWarnings("FinalParameters")
@Override
public int getItemViewType(int position) {
if (DEBUG) {
@@ -300,6 +301,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup paren
}
}
+ @SuppressWarnings("FinalParameters")
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
if (DEBUG) {
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
index c2d4474f897..d43a0981c11 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
@@ -97,7 +97,7 @@ public void readFrom(@NonNull final Queue savedObjects) {
}
@Override
- public void onSaveInstanceState(final Bundle outState) {
+ public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
if (getActivity() != null) {
savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(),
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index e6da0d545fc..e97629f31a9 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -50,7 +50,6 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Item
-import com.xwray.groupie.OnAsyncUpdateListener
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
@@ -68,25 +67,21 @@ import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
-import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.fragments.BaseStateFragment
-import org.schabi.newpipe.info_list.InfoItemDialog
+import org.schabi.newpipe.info_list.dialog.InfoItemDialog
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.ktx.slideUp
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
-import org.schabi.newpipe.player.helper.PlayerHolder
import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
-import org.schabi.newpipe.util.StreamDialogEntry
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
-import java.util.ArrayList
import java.util.function.Consumer
class FeedFragment : BaseStateFragment() {
@@ -143,7 +138,7 @@ class FeedFragment : BaseStateFragment() {
val factory = FeedViewModel.Factory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
- viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
+ viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
groupAdapter = GroupieAdapter().apply {
setOnItemClickListener(listenerStreamItem)
@@ -356,53 +351,12 @@ class FeedFragment : BaseStateFragment() {
feedBinding.loadingProgressBar.max = progressState.maxProgress
}
- private fun showStreamDialog(item: StreamInfoItem) {
+ private fun showInfoItemDialog(item: StreamInfoItem) {
val context = context
val activity: Activity? = getActivity()
if (context == null || context.resources == null || activity == null) return
- val entries = ArrayList()
- if (PlayerHolder.getInstance().isPlayQueueReady) {
- entries.add(StreamDialogEntry.enqueue)
-
- if (PlayerHolder.getInstance().queueSize > 1) {
- entries.add(StreamDialogEntry.enqueue_next)
- }
- }
-
- if (item.streamType == StreamType.AUDIO_STREAM) {
- entries.addAll(
- listOf(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share,
- StreamDialogEntry.open_in_browser
- )
- )
- } else {
- entries.addAll(
- listOf(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.start_here_on_popup,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share,
- StreamDialogEntry.open_in_browser
- )
- )
- }
-
- // show "mark as watched" only when watch history is enabled
- if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) {
- entries.add(
- StreamDialogEntry.mark_as_watched
- )
- }
- entries.add(StreamDialogEntry.show_channel_details)
-
- StreamDialogEntry.setEnabledEntries(entries)
- InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
- StreamDialogEntry.clickOn(which, this, item)
- }.show()
+ InfoItemDialog.Builder(activity, context, this, item).create().show()
}
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
@@ -418,7 +372,7 @@ class FeedFragment : BaseStateFragment() {
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
if (item is StreamItem && !isRefreshing) {
- showStreamDialog(item.streamWithState.stream.toStreamInfoItem())
+ showInfoItemDialog(item.streamWithState.stream.toStreamInfoItem())
return true
}
return false
@@ -438,14 +392,11 @@ class FeedFragment : BaseStateFragment() {
// This need to be saved in a variable as the update occurs async
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
- groupAdapter.updateAsync(
- loadedState.items, false,
- OnAsyncUpdateListener {
- oldOldestSubscriptionUpdate?.run {
- highlightNewItemsAfter(oldOldestSubscriptionUpdate)
- }
+ groupAdapter.updateAsync(loadedState.items, false) {
+ oldOldestSubscriptionUpdate?.run {
+ highlightNewItemsAfter(oldOldestSubscriptionUpdate)
}
- )
+ }
listState?.run {
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
@@ -497,8 +448,7 @@ class FeedFragment : BaseStateFragment() {
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
- {
- subscriptionEntity ->
+ { subscriptionEntity ->
handleFeedNotAvailable(
subscriptionEntity,
t.cause,
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
index 2cbf9ad05b0..e21963c1651 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
@@ -56,7 +56,7 @@ class FeedViewModel(
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
- var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
+ val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
feedDatabaseManager
.getStreams(groupId, showPlayedItems)
.blockingGet(arrayListOf())
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt
index 61e5c7d9ed8..6b9580802ec 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt
@@ -3,7 +3,6 @@ package org.schabi.newpipe.local.feed.notifications
import android.content.Context
import android.util.Log
import androidx.core.app.NotificationCompat
-import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java
index e7ccd07d29e..709a16b68b6 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java
@@ -84,7 +84,7 @@ public void onBindViewHolder(final VH holder, final int position) {
}
@Override
- public void onViewRecycled(final VH holder) {
+ public void onViewRecycled(@NonNull final VH holder) {
super.onViewRecycled(holder);
holder.itemView.setOnClickListener(null);
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
index 73682d5d524..01df342920b 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
@@ -1,6 +1,5 @@
package org.schabi.newpipe.local.history;
-import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
@@ -29,20 +28,16 @@
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import org.schabi.newpipe.info_list.InfoItemDialog;
+import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.local.BaseLocalListFragment;
-import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.settings.HistorySettingsFragment;
-import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
-import org.schabi.newpipe.util.StreamDialogEntry;
+import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -154,7 +149,7 @@ public void selected(final LocalItem selectedItem) {
@Override
public void held(final LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {
- showStreamDialog((StreamStatisticsEntry) selectedItem);
+ showInfoItemDialog((StreamStatisticsEntry) selectedItem);
}
}
});
@@ -328,66 +323,30 @@ private PlayQueue getPlayQueueStartingAt(final StreamStatisticsEntry infoItem) {
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
}
- private void showStreamDialog(final StreamStatisticsEntry item) {
+ private void showInfoItemDialog(final StreamStatisticsEntry item) {
final Context context = getContext();
- final Activity activity = getActivity();
- if (context == null || context.getResources() == null || activity == null) {
- return;
- }
final StreamInfoItem infoItem = item.toStreamInfoItem();
- final ArrayList entries = new ArrayList<>();
-
- if (PlayerHolder.getInstance().isPlayQueueReady()) {
- entries.add(StreamDialogEntry.enqueue);
-
- if (PlayerHolder.getInstance().getQueueSize() > 1) {
- entries.add(StreamDialogEntry.enqueue_next);
- }
- }
-
- if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
- entries.addAll(Arrays.asList(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.delete,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share
- ));
- } else {
- entries.addAll(Arrays.asList(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.start_here_on_popup,
- StreamDialogEntry.delete,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share
- ));
+ try {
+ final InfoItemDialog.Builder dialogBuilder =
+ new InfoItemDialog.Builder(getActivity(), context, this, infoItem);
+
+ // set entries in the middle; the others are added automatically
+ dialogBuilder
+ .addEntry(StreamDialogDefaultEntry.DELETE)
+ .setAction(
+ StreamDialogDefaultEntry.DELETE,
+ (f, i) -> deleteEntry(
+ Math.max(itemListAdapter.getItemsList().indexOf(item), 0)))
+ .setAction(
+ StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
+ (f, i) -> NavigationHelper.playOnBackgroundPlayer(
+ context, getPlayQueueStartingAt(item), true))
+ .create()
+ .show();
+ } catch (final IllegalArgumentException e) {
+ InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem);
}
- entries.add(StreamDialogEntry.open_in_browser);
- if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
- entries.add(StreamDialogEntry.play_with_kodi);
- }
-
- // show "mark as watched" only when watch history is enabled
- if (StreamDialogEntry.shouldAddMarkAsWatched(
- item.getStreamEntity().getStreamType(),
- context
- )) {
- entries.add(
- StreamDialogEntry.mark_as_watched
- );
- }
- entries.add(StreamDialogEntry.show_channel_details);
-
- StreamDialogEntry.setEnabledEntries(entries);
-
- StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
- NavigationHelper
- .playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true));
- StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) ->
- deleteEntry(Math.max(itemListAdapter.getItemsList().indexOf(item), 0)));
-
- new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context),
- (dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show();
}
private void deleteEntry(final int index) {
diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
index feb5b2f9672..0eb56d7169c 100644
--- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
@@ -1,6 +1,8 @@
package org.schabi.newpipe.local.playlist;
-import android.app.Activity;
+import static org.schabi.newpipe.ktx.ViewUtils.animate;
+import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
+
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
@@ -38,22 +40,18 @@
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import org.schabi.newpipe.info_list.InfoItemDialog;
+import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
-import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
-import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
-import org.schabi.newpipe.util.StreamDialogEntry;
+import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -68,9 +66,6 @@
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
-import static org.schabi.newpipe.ktx.ViewUtils.animate;
-import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
-
public class LocalPlaylistFragment extends BaseLocalListFragment, Void> {
// Save the list 10 seconds after the last change occurred
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
@@ -182,7 +177,7 @@ public void selected(final LocalItem selectedItem) {
@Override
public void held(final LocalItem selectedItem) {
if (selectedItem instanceof PlaylistStreamEntry) {
- showStreamItemDialog((PlaylistStreamEntry) selectedItem);
+ showInfoItemDialog((PlaylistStreamEntry) selectedItem);
}
}
@@ -355,7 +350,7 @@ public boolean onOptionsItemSelected(final MenuItem item) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
.setTitle(R.string.remove_watched_popup_title)
- .setPositiveButton(R.string.yes,
+ .setPositiveButton(R.string.ok,
(DialogInterface d, int id) -> removeWatchedStreams(false))
.setNeutralButton(
R.string.remove_watched_popup_yes_and_partially_watched_videos,
@@ -743,70 +738,39 @@ private PlayQueue getPlayQueueStartingAt(final PlaylistStreamEntry infoItem) {
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
}
- protected void showStreamItemDialog(final PlaylistStreamEntry item) {
- final Context context = getContext();
- final Activity activity = getActivity();
- if (context == null || context.getResources() == null || activity == null) {
- return;
- }
+ protected void showInfoItemDialog(final PlaylistStreamEntry item) {
final StreamInfoItem infoItem = item.toStreamInfoItem();
- final ArrayList entries = new ArrayList<>();
+ try {
+ final Context context = getContext();
+ final InfoItemDialog.Builder dialogBuilder =
+ new InfoItemDialog.Builder(getActivity(), context, this, infoItem);
- if (PlayerHolder.getInstance().isPlayQueueReady()) {
- entries.add(StreamDialogEntry.enqueue);
-
- if (PlayerHolder.getInstance().getQueueSize() > 1) {
- entries.add(StreamDialogEntry.enqueue_next);
- }
- }
- if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
- entries.addAll(Arrays.asList(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.set_as_playlist_thumbnail,
- StreamDialogEntry.delete,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share
- ));
- } else {
- entries.addAll(Arrays.asList(
- StreamDialogEntry.start_here_on_background,
- StreamDialogEntry.start_here_on_popup,
- StreamDialogEntry.set_as_playlist_thumbnail,
- StreamDialogEntry.delete,
- StreamDialogEntry.append_playlist,
- StreamDialogEntry.share
- ));
- }
- entries.add(StreamDialogEntry.open_in_browser);
- if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
- entries.add(StreamDialogEntry.play_with_kodi);
- }
-
- // show "mark as watched" only when watch history is enabled
- if (StreamDialogEntry.shouldAddMarkAsWatched(
- item.getStreamEntity().getStreamType(),
- context
- )) {
- entries.add(
- StreamDialogEntry.mark_as_watched
+ // add entries in the middle
+ dialogBuilder.addAllEntries(
+ StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL,
+ StreamDialogDefaultEntry.DELETE
);
- }
- entries.add(StreamDialogEntry.show_channel_details);
-
- StreamDialogEntry.setEnabledEntries(entries);
- StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
- NavigationHelper.playOnBackgroundPlayer(context,
- getPlayQueueStartingAt(item), true));
- StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction(
- (fragment, infoItemDuplicate) ->
- changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl()));
- StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) ->
- deleteItem(item));
-
- new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context),
- (dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show();
+ // set custom actions
+ // all entries modified below have already been added within the builder
+ dialogBuilder
+ .setAction(
+ StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
+ (f, i) -> NavigationHelper.playOnBackgroundPlayer(
+ context, getPlayQueueStartingAt(item), true))
+ .setAction(
+ StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL,
+ (f, i) ->
+ changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl()))
+ .setAction(
+ StreamDialogDefaultEntry.DELETE,
+ (f, i) -> deleteItem(item))
+ .create()
+ .show();
+ } catch (final IllegalArgumentException e) {
+ InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem);
+ }
}
private void setInitialData(final long pid, final String title) {
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java
index a3d8b0567a3..da8e1070a99 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java
@@ -61,7 +61,7 @@ public void onCreate(@Nullable final Bundle savedInstanceState) {
}
@Override
- public void onSaveInstanceState(final Bundle outState) {
+ public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt
index d49df630363..54ba1c6dc53 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt
@@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable
-import io.reactivex.rxjava3.functions.BiFunction
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
@@ -33,9 +32,8 @@ class FeedGroupDialogViewModel(
private var subscriptionsFlowable = Flowable
.combineLatest(
filterSubscriptions.startWithItem(initialQuery),
- toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped),
- BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) }
- )
+ toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped)
+ ) { t1: String, t2: Boolean -> Filter(t1, t2) }
.distinctUntilChanged()
.switchMap { (query, showOnlyUngrouped) ->
subscriptionManager.getSubscriptions(groupId, query, showOnlyUngrouped)
@@ -56,9 +54,8 @@ class FeedGroupDialogViewModel(
private var subscriptionsDisposable = Flowable
.combineLatest(
- subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId),
- BiFunction { t1: List, t2: List -> t1 to t2.toSet() }
- )
+ subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId)
+ ) { t1: List, t2: List -> t1 to t2.toSet() }
.subscribeOn(Schedulers.io())
.subscribe(mutableSubscriptionsLiveData::postValue)
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt
index 50e8aae6a80..1f3ab71eb62 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt
@@ -57,15 +57,12 @@ class FeedGroupReorderDialog : DialogFragment() {
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
- viewModel.dialogEventLiveData.observe(
- viewLifecycleOwner,
- Observer {
- when (it) {
- ProcessingEvent -> disableInput()
- SuccessEvent -> dismiss()
- }
+ viewModel.dialogEventLiveData.observe(viewLifecycleOwner) {
+ when (it) {
+ ProcessingEvent -> disableInput()
+ SuccessEvent -> dismiss()
}
- )
+ }
binding.feedGroupsList.layoutManager = LinearLayoutManager(requireContext())
binding.feedGroupsList.adapter = groupAdapter
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java
index 8e3aad89364..611a1cd30bc 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java
@@ -25,7 +25,6 @@
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
-import com.grack.nanojson.JsonSink;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
@@ -125,10 +124,11 @@ public static void writeTo(final List items, final OutputStrea
/**
* @see #writeTo(List, OutputStream, ImportExportEventListener)
* @param items the list of subscriptions items
- * @param writer the output {@link JsonSink}
+ * @param writer the output {@link JsonAppendableWriter}
* @param eventListener listener for the events generated
*/
- public static void writeTo(final List items, final JsonSink writer,
+ public static void writeTo(final List items,
+ final JsonAppendableWriter writer,
@Nullable final ImportExportEventListener eventListener) {
if (eventListener != null) {
eventListener.onSizeReceived(items.size());
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
index e0c5ab08366..53e6ce59101 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
@@ -1,7 +1,10 @@
package org.schabi.newpipe.player;
+import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
+import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+
import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
@@ -23,11 +26,9 @@
import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
-import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
@@ -42,13 +43,6 @@
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
-import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
public final class PlayQueueActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
@@ -129,7 +123,7 @@ public boolean onOptionsItemSelected(final MenuItem item) {
NavigationHelper.openSettings(this);
return true;
case R.id.action_append_playlist:
- appendAllToPlaylist();
+ player.onAddToPlaylistClicked(getSupportFragmentManager());
return true;
case R.id.action_playback_speed:
openPlaybackParameterDialog();
@@ -443,24 +437,6 @@ public void onStopTrackingTouch(final SeekBar seekBar) {
seeking = false;
}
- ////////////////////////////////////////////////////////////////////////////
- // Playlist append
- ////////////////////////////////////////////////////////////////////////////
-
- private void appendAllToPlaylist() {
- if (player != null && player.getPlayQueue() != null) {
- openPlaylistAppendDialog(player.getPlayQueue().getStreams());
- }
- }
-
- private void openPlaylistAppendDialog(final List playQueueItems) {
- PlaylistDialog.createCorrespondingDialog(
- getApplicationContext(),
- playQueueItems.stream().map(StreamEntity::new).collect(Collectors.toList()),
- dialog -> dialog.show(getSupportFragmentManager(), TAG)
- );
- }
-
////////////////////////////////////////////////////////////////////////////
// Binding Service Listener
////////////////////////////////////////////////////////////////////////////
@@ -624,7 +600,6 @@ private void onMaybeMuteChanged() {
//2) Icon change accordingly to current App Theme
// using rootView.getContext() because getApplicationContext() didn't work
- final Context context = queueControlBinding.getRoot().getContext();
item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index e0debc4e7b1..1051f678f3f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -73,7 +73,6 @@
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
-import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -89,7 +88,6 @@
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
-import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
@@ -99,12 +97,15 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
+import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.widget.AppCompatImageButton;
+import androidx.appcompat.widget.PopupMenu;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.GestureDetectorCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
+import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
@@ -112,6 +113,7 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.RenderersFactory;
@@ -122,6 +124,7 @@
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
@@ -136,6 +139,7 @@
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
import org.schabi.newpipe.error.ErrorInfo;
@@ -144,11 +148,13 @@
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamSegment;
+import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
import org.schabi.newpipe.ktx.AnimationType;
+import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.event.DisplayPortion;
@@ -158,9 +164,10 @@
import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.LoadController;
import org.schabi.newpipe.player.helper.MediaSessionManager;
-import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener;
+import org.schabi.newpipe.player.listeners.view.QualityClickListener;
import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener;
@@ -175,6 +182,7 @@
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
+import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
import org.schabi.newpipe.util.DeviceUtils;
@@ -193,6 +201,8 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
@@ -521,9 +531,12 @@ private void initPlayer(final boolean playOnReady) {
}
private void initListeners() {
+ binding.qualityTextView.setOnClickListener(
+ new QualityClickListener(this, qualityPopupMenu));
+ binding.playbackSpeed.setOnClickListener(
+ new PlaybackSpeedClickListener(this, playbackSpeedPopupMenu));
+
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
- binding.playbackSpeed.setOnClickListener(this);
- binding.qualityTextView.setOnClickListener(this);
binding.captionTextView.setOnClickListener(this);
binding.resizeTextView.setOnClickListener(this);
binding.playbackLiveSync.setOnClickListener(this);
@@ -532,10 +545,15 @@ private void initListeners() {
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
binding.getRoot().setOnTouchListener(playerGestureListener);
- binding.queueButton.setOnClickListener(this);
- binding.segmentsButton.setOnClickListener(this);
- binding.repeatButton.setOnClickListener(this);
- binding.shuffleButton.setOnClickListener(this);
+ binding.queueButton.setOnClickListener(v -> onQueueClicked());
+ binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
+ binding.repeatButton.setOnClickListener(v -> onRepeatClicked());
+ binding.shuffleButton.setOnClickListener(v -> onShuffleClicked());
+ binding.addToPlaylistButton.setOnClickListener(v -> {
+ if (getParentActivity() != null) {
+ onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager());
+ }
+ });
binding.playPauseButton.setOnClickListener(this);
binding.playPreviousButton.setOnClickListener(this);
@@ -580,11 +598,17 @@ public void onChange(final boolean selfChange) {
v.getPaddingTop(),
v.getPaddingRight(),
v.getPaddingBottom());
- binding.fastSeekOverlay.setPadding(
- v.getPaddingLeft(),
- v.getPaddingTop(),
- v.getPaddingRight(),
- v.getPaddingBottom());
+
+ // If we added padding to the fast seek overlay, too, it would not go under the
+ // system ui. Instead we apply negative margins equal to the window insets of
+ // the opposite side, so that the view covers all of the player (overflowing on
+ // some sides) and its center coincides with the center of other controls.
+ final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams)
+ binding.fastSeekOverlay.getLayoutParams();
+ fastSeekParams.leftMargin = -v.getPaddingRight();
+ fastSeekParams.topMargin = -v.getPaddingBottom();
+ fastSeekParams.rightMargin = -v.getPaddingLeft();
+ fastSeekParams.bottomMargin = -v.getPaddingTop();
});
}
@@ -593,8 +617,7 @@ public void onChange(final boolean selfChange) {
*/
private void setupPlayerSeekOverlay() {
binding.fastSeekOverlay
- .seekSecondsSupplier(
- () -> (int) (retrieveSeekDurationFromPreferences(this) / 1000.0f))
+ .seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(this) / 1000)
.performListener(new PlayerFastSeekOverlay.PerformListener() {
@Override
@@ -607,6 +630,7 @@ public void onDoubleTapEnd() {
animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
}
+ @NonNull
@Override
public FastSeekDirection getFastSeekDirection(
@NonNull final DisplayPortion portion
@@ -658,6 +682,7 @@ public void seek(final boolean forward) {
//////////////////////////////////////////////////////////////////////////*/
//region Playback initialization via intent
+ @SuppressWarnings("MethodLength")
public void handleIntent(@NonNull final Intent intent) {
// fail fast if no play queue was provided
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
@@ -1910,7 +1935,7 @@ public void hideControls(final long duration, final long delay) {
}, delay);
}
- private void showHideShadow(final boolean show, final long duration) {
+ public void showHideShadow(final boolean show, final long duration) {
animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
@@ -2378,6 +2403,32 @@ private void setShuffleButton(@NonNull final ImageButton button, final boolean s
+ /*//////////////////////////////////////////////////////////////////////////
+ // Playlist append
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Playlist append
+
+ public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManager) {
+ if (DEBUG) {
+ Log.d(TAG, "onAddToPlaylistClicked() called");
+ }
+
+ if (getPlayQueue() != null) {
+ PlaylistDialog.createCorrespondingDialog(
+ getContext(),
+ getPlayQueue()
+ .getStreams()
+ .stream()
+ .map(StreamEntity::new)
+ .collect(Collectors.toList()),
+ dialog -> dialog.show(fragmentManager, TAG)
+ );
+ }
+ }
+ //endregion
+
+
+
/*//////////////////////////////////////////////////////////////////////////
// Mute / Unmute
//////////////////////////////////////////////////////////////////////////*/
@@ -2443,9 +2494,9 @@ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playba
}
@Override
- public void onPositionDiscontinuity(
- final PositionInfo oldPosition, final PositionInfo newPosition,
- @DiscontinuityReason final int discontinuityReason) {
+ public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
+ @NonNull final PositionInfo newPosition,
+ @DiscontinuityReason final int discontinuityReason) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
+ "discontinuityReason = [" + discontinuityReason + "]");
@@ -2493,7 +2544,7 @@ public void onRenderedFirstFrame() {
}
@Override
- public void onCues(final List cues) {
+ public void onCues(@NonNull final List cues) {
binding.subtitleView.onCues(cues);
}
//endregion
@@ -2999,18 +3050,19 @@ private void maybeUpdateCurrentMetadata() {
final MediaSourceTag metadata;
try {
- metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
- } catch (IndexOutOfBoundsException | ClassCastException error) {
+ final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem();
+ if (currentMediaItem == null || currentMediaItem.playbackProperties == null
+ || currentMediaItem.playbackProperties.tag == null) {
+ return;
+ }
+ metadata = (MediaSourceTag) currentMediaItem.playbackProperties.tag;
+ } catch (final IndexOutOfBoundsException | ClassCastException ex) {
if (DEBUG) {
- Log.d(TAG, "Could not update metadata: " + error.getMessage());
- error.printStackTrace();
+ Log.d(TAG, "Could not update metadata", ex);
}
return;
}
- if (metadata == null) {
- return;
- }
maybeAutoQueueNextStream(metadata);
if (currentMetadata == metadata) {
@@ -3119,6 +3171,7 @@ private void onQueueClicked() {
binding.itemsListHeaderDuration.setVisibility(View.VISIBLE);
binding.shuffleButton.setVisibility(View.VISIBLE);
binding.repeatButton.setVisibility(View.VISIBLE);
+ binding.addToPlaylistButton.setVisibility(View.VISIBLE);
hideControls(0, 0);
binding.itemsListPanel.requestFocus();
@@ -3156,6 +3209,7 @@ private void onSegmentsClicked() {
binding.itemsListHeaderDuration.setVisibility(View.GONE);
binding.shuffleButton.setVisibility(View.GONE);
binding.repeatButton.setVisibility(View.GONE);
+ binding.addToPlaylistButton.setVisibility(View.GONE);
hideControls(0, 0);
binding.itemsListPanel.requestFocus();
@@ -3184,6 +3238,7 @@ private void buildSegments() {
binding.shuffleButton.setVisibility(View.GONE);
binding.repeatButton.setVisibility(View.GONE);
+ binding.addToPlaylistButton.setVisibility(View.GONE);
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
}
@@ -3203,6 +3258,9 @@ public void closeItemsList() {
binding.itemsListPanel.setTranslationY(
-binding.itemsListPanel.getHeight() * 5);
});
+
+ // clear focus, otherwise a white rectangle remains on top of the player
+ binding.itemsListClose.clearFocus();
binding.playPauseButton.requestFocus();
}
}
@@ -3286,7 +3344,27 @@ public void onStartDrag(final PlayQueueItemHolder viewHolder) {
@Override // own playback listener
@Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
- return (isAudioOnly ? audioResolver : videoResolver).resolve(info);
+ if (audioPlayerSelected()) {
+ return audioResolver.resolve(info);
+ }
+
+ if (isAudioOnly && videoResolver.getStreamSourceType().orElse(
+ SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY)
+ == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY) {
+ // If the current info has only video streams with audio and if the stream is played as
+ // audio, we need to use the audio resolver, otherwise the video stream will be played
+ // in background.
+ return audioResolver.resolve(info);
+ }
+
+ // Even if the stream is played in background, we need to use the video resolver if the
+ // info played is separated video-only and audio-only streams; otherwise, if the audio
+ // resolver was called when the app was in background, the app will only stream audio when
+ // the user come back to the app and will never fetch the video stream.
+ // Note that the video is not fetched when the app is in background because the video
+ // renderer is fully disabled (see useVideoSource method), except for HLS streams
+ // (see https://github.com/google/ExoPlayer/issues/9282).
+ return videoResolver.resolve(info);
}
public void disablePreloadingOfCurrentTrack() {
@@ -3538,37 +3616,6 @@ public void onDismiss(@Nullable final PopupMenu menu) {
}
}
- private void onQualitySelectorClicked() {
- if (DEBUG) {
- Log.d(TAG, "onQualitySelectorClicked() called");
- }
- qualityPopupMenu.show();
- isSomePopupMenuVisible = true;
-
- final VideoStream videoStream = getSelectedVideoStream();
- if (videoStream != null) {
- final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " "
- + videoStream.resolution;
- binding.qualityTextView.setText(qualityText);
- }
-
- saveWasPlaying();
- }
-
- private void onPlaybackSpeedClicked() {
- if (DEBUG) {
- Log.d(TAG, "onPlaybackSpeedClicked() called");
- }
- if (videoPlayerSelected()) {
- PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch(),
- getPlaybackSkipSilence(), this::setPlaybackParameters)
- .show(getParentActivity().getSupportFragmentManager(), null);
- } else {
- playbackSpeedPopupMenu.show();
- isSomePopupMenuVisible = true;
- }
- }
-
private void onCaptionClicked() {
if (DEBUG) {
Log.d(TAG, "onCaptionClicked() called");
@@ -3673,11 +3720,7 @@ public void onClick(final View v) {
if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]");
}
- if (v.getId() == binding.qualityTextView.getId()) {
- onQualitySelectorClicked();
- } else if (v.getId() == binding.playbackSpeed.getId()) {
- onPlaybackSpeedClicked();
- } else if (v.getId() == binding.resizeTextView.getId()) {
+ if (v.getId() == binding.resizeTextView.getId()) {
onResizeClicked();
} else if (v.getId() == binding.captionTextView.getId()) {
onCaptionClicked();
@@ -3689,18 +3732,6 @@ public void onClick(final View v) {
playPrevious();
} else if (v.getId() == binding.playNextButton.getId()) {
playNext();
- } else if (v.getId() == binding.queueButton.getId()) {
- onQueueClicked();
- return;
- } else if (v.getId() == binding.segmentsButton.getId()) {
- onSegmentsClicked();
- return;
- } else if (v.getId() == binding.repeatButton.getId()) {
- onRepeatClicked();
- return;
- } else if (v.getId() == binding.shuffleButton.getId()) {
- onShuffleClicked();
- return;
} else if (v.getId() == binding.moreOptionsButton.getId()) {
onMoreOptionsClicked();
} else if (v.getId() == binding.share.getId()) {
@@ -3729,23 +3760,33 @@ public void onClick(final View v) {
context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER));
}
- if (currentState != STATE_COMPLETED) {
- controlsVisibilityHandler.removeCallbacksAndMessages(null);
- showHideShadow(true, DEFAULT_CONTROLS_DURATION);
- animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.ALPHA, 0, () -> {
- if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) {
- if (v.getId() == binding.playPauseButton.getId()
- // Hide controls in fullscreen immediately
- || (v.getId() == binding.screenRotationButton.getId()
- && isFullscreen)) {
- hideControls(0, 0);
- } else {
- hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
- }
- }
- });
+ manageControlsAfterOnClick(v);
+ }
+
+ /**
+ * Manages the controls after a click occurred on the player UI.
+ * @param v – The view that was clicked
+ */
+ public void manageControlsAfterOnClick(@NonNull final View v) {
+ if (currentState == STATE_COMPLETED) {
+ return;
}
+
+ controlsVisibilityHandler.removeCallbacksAndMessages(null);
+ showHideShadow(true, DEFAULT_CONTROLS_DURATION);
+ animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.ALPHA, 0, () -> {
+ if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) {
+ if (v.getId() == binding.playPauseButton.getId()
+ // Hide controls in fullscreen immediately
+ || (v.getId() == binding.screenRotationButton.getId()
+ && isFullscreen)) {
+ hideControls(0, 0);
+ } else {
+ hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
+ }
+ }
+ });
}
@Override
@@ -3767,6 +3808,10 @@ public boolean onKeyDown(final int keyCode) {
case KeyEvent.KEYCODE_SPACE:
if (isFullscreen) {
playPause();
+ if (isPlaying()) {
+ hideControls(0, 0);
+ }
+ return true;
}
break;
case KeyEvent.KEYCODE_BACK:
@@ -3780,8 +3825,9 @@ public boolean onKeyDown(final int keyCode) {
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_CENTER:
- if (binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) {
- // do not interfere with focus in playlist etc.
+ if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus())
+ || isQueueVisible) {
+ // do not interfere with focus in playlist and play queue etc.
return false;
}
@@ -3789,15 +3835,13 @@ public boolean onKeyDown(final int keyCode) {
return true;
}
- if (!isControlsVisible()) {
- if (!isQueueVisible) {
- binding.playPauseButton.requestFocus();
- }
+ if (isControlsVisible()) {
+ hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
+ } else {
+ binding.playPauseButton.requestFocus();
showControlsThenHide();
showSystemUIPartially();
return true;
- } else {
- hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
}
break;
}
@@ -4141,19 +4185,125 @@ public AppCompatActivity getParentActivity() {
return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
}
- private void useVideoSource(final boolean video) {
- if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) {
+ private void useVideoSource(final boolean videoEnabled) {
+ if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
return;
}
- isAudioOnly = !video;
- // When a user returns from background controls could be hidden
- // but systemUI will be shown 100%. Hide it
+ isAudioOnly = !videoEnabled;
+ // When a user returns from background, controls could be hidden but SystemUI will be shown
+ // 100%. Hide it.
if (!isAudioOnly && !isControlsVisible()) {
hideSystemUIIfNeeded();
}
+
+ // The current metadata may be null sometimes (for e.g. when using an unstable connection
+ // in livestreams) so we will be not able to execute the block below.
+ // Reload the play queue manager in this case, which is the behavior when we don't know the
+ // index of the video renderer or playQueueManagerReloadingNeeded returns true.
+ if (currentMetadata == null) {
+ reloadPlayQueueManager();
+ setRecovery();
+ return;
+ }
+
+ final int videoRenderIndex = getVideoRendererIndex();
+ final StreamInfo info = currentMetadata.getMetadata();
+
+ // In the case we don't know the source type, fallback to the one with video with audio or
+ // audio-only source.
+ final SourceType sourceType = videoResolver.getStreamSourceType().orElse(
+ SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY);
+
+ if (playQueueManagerReloadingNeeded(sourceType, info, videoRenderIndex)) {
+ reloadPlayQueueManager();
+ } else {
+ final StreamType streamType = info.getStreamType();
+ if (streamType == StreamType.AUDIO_STREAM
+ || streamType == StreamType.AUDIO_LIVE_STREAM) {
+ // Nothing to do more than setting the recovery position
+ setRecovery();
+ return;
+ }
+
+ final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull(
+ trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex);
+ if (videoEnabled) {
+ // Clearing the null selection override enable again the video stream (and its
+ // fetching).
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .clearSelectionOverride(videoRenderIndex, videoTrackGroupArray));
+ } else {
+ // Using setRendererDisabled still fetch the video stream in background, contrary
+ // to setSelectionOverride with a null override.
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null));
+ }
+ }
+
setRecovery();
- reloadPlayQueueManager();
+ }
+
+ /**
+ * Return whether the play queue manager needs to be reloaded when switching player type.
+ *
+ *
+ * The play queue manager needs to be reloaded if the video renderer index is not known and if
+ * the content is not an audio content, but also if none of the following cases is met:
+ *
+ *
+ *
the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
+ * {@link StreamType#AUDIO_LIVE_STREAM audio live stream};
+ *
the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
+ * {@link SourceType#LIVE_STREAM live source};
+ *
the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
+ * with a separated audio source} or has no audio-only streams available and is a
+ * {@link StreamType#LIVE_STREAM live stream} or a
+ * {@link StreamType#LIVE_STREAM live stream}.
+ *