diff --git a/LICENSE b/LICENSE
index 94a9ed024..f288702d2 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 797e76997..7ec17044e 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 28cdbf020..f9c99819c 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 122660d64..000000000
--- 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 9895bace7..6ae5cf936 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 @@ public class MainActivity extends AppCompatActivity {
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 @@ public class MainActivity extends AppCompatActivity {
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 36de1ecfc..000000000
--- 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 000000000..060114974
--- /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 fde006a60..c7604e512 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.local.dialog.PlaylistDialog;
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 final class QueueItemMenuUtil {
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 9d6e44f04..adef3c0e4 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 @@ public class RouterActivity extends AppCompatActivity {
.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 1e5bd8799..50a3984e3 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 a04de8abc..c1dd38389 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 showLicense(context, license) { alertDialog ->
+ alertDialog.setPositiveButton(R.string.ok) { dialog, _ ->
+ dialog.dismiss()
+ }
+ }
+ }
+
+ 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, 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(license.name)
- alert.setView(webView)
- Localization.assureCorrectAppLanguage(context)
- alert.setNegativeButton(
- context.getString(R.string.ok)
- ) { dialog, _ -> dialog.dismiss() }
- alert.show()
- }
- }
- }
- @JvmStatic
- fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
- return if (context == null) {
- Disposable.empty()
- } else {
- Observable.fromCallable { getFormattedLicense(context, component.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(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 0a765ed4e..150d4a8e5 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 @@ package org.schabi.newpipe.database.history.dao;
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 abstract class StreamHistoryDAO implements HistoryDAO {
+ " 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 9798ec72d..47b6f4dd9 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 5c954ad64..f5c226908 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.FilenameUtils;
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 class DownloadDialog extends DialogFragment
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 class DownloadDialog extends DialogFragment
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 8e7a455b4..976173373 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 cbd44566e..6b17803c4 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 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager;
*/
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 2fe615764..5016a49f6 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 @@ class StackItem implements Serializable {
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 2d9abc6dc..0af5ec99e 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 final class VideoDetailFragment
activity,
info.getVideoStreams(),
info.getVideoOnlyStreams(),
+ false,
false);
selectedVideoStreamIndex = ListHelper
.getDefaultResolutionIndex(activity, sortedVideoStreams);
@@ -1994,9 +1995,7 @@ public final class VideoDetailFragment
// 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 @@ public final class VideoDetailFragment
// 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 @@ public final class VideoDetailFragment
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 final class VideoDetailFragment
}
}
- 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 3c2e65bb7..27e5a8571 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.appcompat.app.ActionBar;
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 abstract class BaseListFragment extends BaseStateFragment
}
}
- @Override
- public void onDetach() {
- super.onDetach();
- }
-
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -220,14 +206,10 @@ public abstract class BaseListFragment extends BaseStateFragment
//////////////////////////////////////////////////////////////////////////*/
@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 @@ public abstract class BaseListFragment extends BaseStateFragment
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 @@ public abstract class BaseListFragment extends BaseStateFragment
@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 abstract class BaseListFragment extends BaseStateFragment
@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 abstract class BaseListFragment extends BaseStateFragment
}
});
- infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() {
+ infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final PlaylistInfoItem selectedItem) {
try {
@@ -315,22 +296,99 @@ public abstract class BaseListFragment extends BaseStateFragment
}
});
- 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 OnScrollBelowItemsListener() {
+ 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 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 @@ public abstract class BaseListFragment extends BaseStateFragment
}
}
- protected void showStreamDialog(final StreamInfoItem item) {
- final Context context = getContext();
- final Activity activity = getActivity();
- if (context == null || context.getResources() == null || activity == null) {
- return;
+ 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);
}
- 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);
- }
-
- StreamDialogEntry.setEnabledEntries(entries);
-
- new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
- (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -418,6 +433,12 @@ public abstract class BaseListFragment extends BaseStateFragment
// 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 e98dc9fda..35424437d 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 androidx.annotation.NonNull;
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.core.Single;
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 abstract class BaseListInfoFragment
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 abstract class BaseListInfoFragment
@SuppressWarnings("unchecked")
public void readFrom(@NonNull final Queue
*/
-public class KioskFragment extends BaseListInfoFragment {
+public class KioskFragment extends BaseListInfoFragment {
@State
String kioskId = "";
String kioskTranslatedName;
@@ -145,7 +146,7 @@ public class KioskFragment extends BaseListInfoFragment {
}
@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 640d08064..5bf20c144 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 android.view.ViewGroup;
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.ServiceList;
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.core.Single;
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 class PlaylistFragment extends BaseListInfoFragment {
//////////////////////////////////////////////////////////////////////////*/
@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 @@ public class PlaylistFragment extends BaseListInfoFragment {
}
@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;
+ 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);
}
-
- 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
- ));
- }
- 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 class PlaylistFragment extends BaseListInfoFragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
- protected Single loadMoreItemsLogic() {
+ protected Single> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
}
@@ -328,9 +289,14 @@ public class PlaylistFragment extends BaseListInfoFragment {
&& (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 @@ public class PlaylistFragment extends BaseListInfoFragment {
}
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 3cfcfd470..fb983b01e 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.view.ViewGroup;
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 class SuggestionListAdapter
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 6532417c0..f0ece69f3 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 android.view.ViewGroup;
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.ktx.ViewUtils;
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 class RelatedItemsFragment extends BaseListInfoFragment
// 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 class RelatedItemsFragment extends BaseListInfoFragment
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 class RelatedItemsFragment extends BaseListInfoFragment
}
@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 class RelatedItemsFragment extends BaseListInfoFragment
}
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
- disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -137,11 +125,13 @@ public class RelatedItemsFragment extends BaseListInfoFragment
@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 @@ public class RelatedItemsFragment extends BaseListInfoFragment
@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 c485337f0..000000000
--- 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 56bc63384..fb27593e7 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 @@ package org.schabi.newpipe.info_list;
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.annotation.Nullable;
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 org.schabi.newpipe.util.OnClickGesture;
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 class InfoListAdapter extends RecyclerView.Adapter 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 class InfoListAdapter extends RecyclerView.Adapter 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 class InfoListAdapter extends RecyclerView.Adapter 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 class InfoListAdapter extends RecyclerView.Adapter 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 class InfoListAdapter extends RecyclerView.Adapter 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 class InfoListAdapter extends RecyclerView.Adapter 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 000000000..7e87318ee
--- /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 000000000..9d82e3b58
--- /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 78acb752b..aa4f4c9f0 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.local.history.HistoryRecordManager;
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 cb47efa92..6e4773c09 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.text.util.Linkify;
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 org.schabi.newpipe.util.PicassoHelper;
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 5d81c0069..05e2fdac0 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 class LocalItemListAdapter extends RecyclerView.Adapter() {
@@ -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 2cbf9ad05..e21963c16 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 61e5c7d9e..6b9580802 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 e7ccd07d2..709a16b68 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 abstract class HistoryEntryAdapter
}
@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 73682d5d5..01df34292 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.databinding.StatisticPlaylistControlBinding;
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 class StatisticsPlaylistFragment
@Override
public void held(final LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {
- showStreamDialog((StreamStatisticsEntry) selectedItem);
+ showInfoItemDialog((StreamStatisticsEntry) selectedItem);
}
}
});
@@ -328,66 +323,30 @@ public class StatisticsPlaylistFragment
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<>();
+ try {
+ 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);
- }
+ // 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);
}
-
- 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
- ));
- }
- 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 feb5b2f96..0eb56d716 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.databinding.PlaylistControlBinding;
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.disposables.Disposable;
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 class LocalPlaylistFragment extends BaseLocalListFragment removeWatchedStreams(false))
.setNeutralButton(
R.string.remove_watched_popup_yes_and_partially_watched_videos,
@@ -743,70 +738,39 @@ public class LocalPlaylistFragment extends BaseLocalListFragment 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
);
+
+ // 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);
}
- 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();
}
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 a3d8b0567..da8e1070a 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 class ImportConfirmationDialog extends DialogFragment {
}
@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 d49df6303..54ba1c6dc 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 50e8aae6a..1f3ab71eb 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 8e3aad893..611a1cd30 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.JsonAppendableWriter;
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 final class ImportExportJsonHelper {
/**
* @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 e0c5ab083..53e6ce591 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 androidx.recyclerview.widget.RecyclerView;
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.PermissionHelper;
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 final class PlayQueueActivity extends AppCompatActivity
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 final class PlayQueueActivity extends AppCompatActivity
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 @@ public final class PlayQueueActivity extends AppCompatActivity
//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 e0debc4e7..1051f678f 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.provider.Settings;
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.FrameLayout;
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.NonNull;
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 androidx.recyclerview.widget.RecyclerView;
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.MediaSource;
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 com.squareup.picasso.Target;
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.error.UserAction;
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.event.PlayerServiceEventListener;
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.playqueue.PlayQueueItemTouchCallback;
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.ArrayList;
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 @@ public final class Player implements
}
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 @@ public final class Player implements
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 final class Player implements
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 final class Player implements
*/
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 final class Player implements
animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
}
+ @NonNull
@Override
public FastSeekDirection getFastSeekDirection(
@NonNull final DisplayPortion portion
@@ -658,6 +682,7 @@ public final class Player implements
//////////////////////////////////////////////////////////////////////////*/
//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 final class Player implements
}, 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 @@ public final class Player implements
+ /*//////////////////////////////////////////////////////////////////////////
+ // 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 final class Player implements
}
@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 final class Player implements
}
@Override
- public void onCues(final List cues) {
+ public void onCues(@NonNull final List cues) {
binding.subtitleView.onCues(cues);
}
//endregion
@@ -2999,18 +3050,19 @@ public final class Player implements
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 @@ public final class Player implements
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 @@ public final class Player implements
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 @@ public final class Player implements
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 final class Player implements
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 final class Player implements
@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 final class Player implements
}
}
- 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 final class Player implements
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 final class Player implements
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 final class Player implements
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 final class Player implements
case KeyEvent.KEYCODE_SPACE:
if (isFullscreen) {
playPause();
+ if (isPlaying()) {
+ hideControls(0, 0);
+ }
+ return true;
}
break;
case KeyEvent.KEYCODE_BACK:
@@ -3780,8 +3825,9 @@ public final class Player implements
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 final class Player implements
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 final class Player implements
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}.
+ *