diff --git a/LICENSE b/LICENSE index 94a9ed024d3..f288702d2fa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found. GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see -. +. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. +. diff --git a/app/build.gradle b/app/build.gradle index 797e76997df..7ec17044e40 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,15 +9,15 @@ plugins { android { compileSdk 31 - buildToolsVersion '30.0.3' + buildToolsVersion '31.0.0' defaultConfig { applicationId "org.schabi.newpipe" resValue "string", "app_name", "NewPipe" minSdk 19 targetSdk 29 - versionCode 983 - versionName "0.22.0" + versionCode 984 + versionName "0.22.1" multiDexEnabled true @@ -98,10 +98,10 @@ android { } ext { - checkstyleVersion = '9.2.1' + checkstyleVersion = '10.0' androidxLifecycleVersion = '2.3.1' - androidxRoomVersion = '2.3.0' + androidxRoomVersion = '2.4.2' androidxWorkVersion = '2.7.1' icepickVersion = '3.2.0' @@ -122,7 +122,7 @@ configurations { } checkstyle { - getConfigDirectory().set(rootProject.file(".")) + getConfigDirectory().set(rootProject.file("checkstyle")) ignoreFailures false showViolations true toolVersion = checkstyleVersion @@ -194,7 +194,7 @@ dependencies { /** Checkstyle **/ checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" - ktlint 'com.pinterest:ktlint:0.43.2' + ktlint 'com.pinterest:ktlint:0.44.0' /** Kotlin **/ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" @@ -202,16 +202,16 @@ dependencies { /** AndroidX **/ implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.fragment:fragment-ktx:1.3.6' implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" - implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' - implementation 'androidx.media:media:1.4.3' + implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' + implementation 'androidx.media:media:1.5.0' implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation "androidx.room:room-rxjava3:${androidxRoomVersion}" @@ -221,7 +221,8 @@ dependencies { // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.webkit:webkit:1.4.0' - implementation 'com.google.android.material:material:1.4.0' + implementation 'com.google.android.material:material:1.5.0' + implementation "androidx.work:work-runtime:${androidxWorkVersion}" implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" @@ -249,8 +250,6 @@ dependencies { implementation "com.github.lisawray.groupie:groupie:${groupieVersion}" implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" - // Circular ImageView - implementation "de.hdodenhof:circleimageview:3.1.0" // Image loading //noinspection GradleDependency --> 2.8 is the last version, not 2.71828! implementation "com.squareup.picasso:picasso:2.8" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 28cdbf02038..f9c99819c4a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -381,9 +381,6 @@ - diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java deleted file mode 100644 index 122660d6431..00000000000 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ /dev/null @@ -1,264 +0,0 @@ -package org.schabi.newpipe; - -import android.app.Application; -import android.app.IntentService; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.Signature; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.pm.PackageInfoCompat; -import androidx.preference.PreferenceManager; - -import com.grack.nanojson.JsonObject; -import com.grack.nanojson.JsonParser; -import com.grack.nanojson.JsonParserException; - -import org.schabi.newpipe.error.ErrorInfo; -import org.schabi.newpipe.error.ErrorUtil; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.downloader.Response; -import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.List; - -public final class CheckForNewAppVersion extends IntentService { - public CheckForNewAppVersion() { - super("CheckForNewAppVersion"); - } - - private static final boolean DEBUG = MainActivity.DEBUG; - private static final String TAG = CheckForNewAppVersion.class.getSimpleName(); - - // Public key of the certificate that is used in NewPipe release versions - private static final String RELEASE_CERT_PUBLIC_KEY_SHA1 - = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; - private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json"; - - /** - * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133. - * - * @param application The application - * @return String with the APK's SHA1 fingerprint in hexadecimal - */ - @NonNull - private static String getCertificateSHA1Fingerprint(@NonNull final Application application) { - final List signatures; - try { - signatures = PackageInfoCompat.getSignatures(application.getPackageManager(), - application.getPackageName()); - } catch (final PackageManager.NameNotFoundException e) { - ErrorUtil.createNotification(application, new ErrorInfo(e, - UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")); - return ""; - } - if (signatures.isEmpty()) { - return ""; - } - - final X509Certificate c; - try { - final byte[] cert = signatures.get(0).toByteArray(); - final InputStream input = new ByteArrayInputStream(cert); - final CertificateFactory cf = CertificateFactory.getInstance("X509"); - c = (X509Certificate) cf.generateCertificate(input); - } catch (final CertificateException e) { - ErrorUtil.createNotification(application, new ErrorInfo(e, - UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error")); - return ""; - } - - try { - final MessageDigest md = MessageDigest.getInstance("SHA1"); - final byte[] publicKey = md.digest(c.getEncoded()); - return byte2HexFormatted(publicKey); - } catch (NoSuchAlgorithmException | CertificateEncodingException e) { - ErrorUtil.createNotification(application, new ErrorInfo(e, - UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key")); - return ""; - } - } - - private static String byte2HexFormatted(final byte[] arr) { - final StringBuilder str = new StringBuilder(arr.length * 2); - - for (int i = 0; i < arr.length; i++) { - String h = Integer.toHexString(arr[i]); - final int l = h.length(); - if (l == 1) { - h = "0" + h; - } - if (l > 2) { - h = h.substring(l - 2, l); - } - str.append(h.toUpperCase()); - if (i < (arr.length - 1)) { - str.append(':'); - } - } - return str.toString(); - } - - /** - * Method to compare the current and latest available app version. - * If a newer version is available, we show the update notification. - * - * @param application The application - * @param versionName Name of new version - * @param apkLocationUrl Url with the new apk - * @param versionCode Code of new version - */ - private static void compareAppVersionAndShowNotification(@NonNull final Application application, - final String versionName, - final String apkLocationUrl, - final int versionCode) { - if (BuildConfig.VERSION_CODE >= versionCode) { - return; - } - - // A pending intent to open the apk location url in the browser. - final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final PendingIntent pendingIntent - = PendingIntent.getActivity(application, 0, intent, 0); - - final String channelId = application - .getString(R.string.app_update_notification_channel_id); - final NotificationCompat.Builder notificationBuilder - = new NotificationCompat.Builder(application, channelId) - .setSmallIcon(R.drawable.ic_newpipe_update) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .setContentTitle(application - .getString(R.string.app_update_notification_content_title)) - .setContentText(application - .getString(R.string.app_update_notification_content_text) - + " " + versionName); - - final NotificationManagerCompat notificationManager - = NotificationManagerCompat.from(application); - notificationManager.notify(2000, notificationBuilder.build()); - } - - public static boolean isReleaseApk(@NonNull final App app) { - return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1); - } - - private void checkNewVersion() throws IOException, ReCaptchaException { - final App app = App.getApp(); - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); - final NewVersionManager manager = new NewVersionManager(); - - // Check if the current apk is a github one or not. - if (!isReleaseApk(app)) { - return; - } - - // Check if the last request has happened a certain time ago - // to reduce the number of API requests. - final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0); - if (!manager.isExpired(expiry)) { - return; - } - - // Make a network request to get latest NewPipe data. - final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL); - handleResponse(response, manager, prefs, app); - } - - private void handleResponse(@NonNull final Response response, - @NonNull final NewVersionManager manager, - @NonNull final SharedPreferences prefs, - @NonNull final App app) { - try { - // Store a timestamp which needs to be exceeded, - // before a new request to the API is made. - final long newExpiry = manager - .coerceExpiry(response.getHeader("expires")); - prefs.edit() - .putLong(app.getString(R.string.update_expiry_key), newExpiry) - .apply(); - } catch (final Exception e) { - if (DEBUG) { - Log.w(TAG, "Could not extract and save new expiry date", e); - } - } - - // Parse the json from the response. - try { - - final JsonObject githubStableObject = JsonParser.object() - .from(response.responseBody()).getObject("flavors") - .getObject("github").getObject("stable"); - - final String versionName = githubStableObject - .getString("version"); - final int versionCode = githubStableObject - .getInt("version_code"); - final String apkLocationUrl = githubStableObject - .getString("apk"); - - compareAppVersionAndShowNotification(app, versionName, - apkLocationUrl, versionCode); - } catch (final JsonParserException e) { - // Most likely something is wrong in data received from NEWPIPE_API_URL. - // Do not alarm user and fail silently. - if (DEBUG) { - Log.w(TAG, "Could not get NewPipe API: invalid json", e); - } - } - } - - /** - * Start a new service which - * checks if all conditions for performing a version check are met, - * fetches the API endpoint {@link #NEWPIPE_API_URL} containing info - * about the latest NewPipe version - * and displays a notification about ana available update. - *
- * Following conditions need to be met, before data is request from the server: - *
    - *
  • The app is signed with the correct signing key (by TeamNewPipe / schabi). - * If the signing key differs from the one used upstream, the update cannot be installed.
  • - *
  • The user enabled searching for and notifying about updates in the settings.
  • - *
  • The app did not recently check for updates. - * We do not want to make unnecessary connections and DOS our servers.
  • - *
- * Must not be executed when the app is in background. - */ - public static void startNewVersionCheckService() { - final Intent intent = new Intent(App.getApp().getApplicationContext(), - CheckForNewAppVersion.class); - App.getApp().startService(intent); - } - - @Override - protected void onHandleIntent(@Nullable final Intent intent) { - try { - checkNewVersion(); - } catch (final IOException e) { - Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e); - } catch (final ReCaptchaException e) { - Log.e(TAG, "ReCaptchaException should never happen here.", e); - } - - } -} diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 9895bace7f5..6ae5cf93661 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -20,7 +20,6 @@ package org.schabi.newpipe; -import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import android.content.BroadcastReceiver; @@ -178,10 +177,9 @@ protected void onPostCreate(final Bundle savedInstanceState) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) { - // Start the service which is checking all conditions + // Start the worker which is checking all conditions // and eventually searching for a new version. - // The service searching for a new NewPipe version must not be started in background. - startNewVersionCheckService(); + NewVersionWorker.enqueueNewVersionCheckingWork(app); } } @@ -231,7 +229,7 @@ private void addDrawerMenuForCurrentService() throws ExtractionException { drawerLayoutBinding.navigation.getMenu() .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator .getTranslatedKioskName(ks, this)) - .setIcon(KioskTranslator.getKioskIcon(ks, this)); + .setIcon(KioskTranslator.getKioskIcon(ks)); kioskId++; } diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt b/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt deleted file mode 100644 index 36de1ecfcbe..00000000000 --- a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.schabi.newpipe - -import java.time.Instant -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -class NewVersionManager { - - fun isExpired(expiry: Long): Boolean { - return Instant.ofEpochSecond(expiry).isBefore(Instant.now()) - } - - /** - * Coerce expiry date time in between 6 hours and 72 hours from now - * - * @return Epoch second of expiry date time - */ - fun coerceExpiry(expiryString: String?): Long { - val now = ZonedDateTime.now() - return expiryString?.let { - - var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString)) - expiry = maxOf(expiry, now.plusHours(6)) - expiry = minOf(expiry, now.plusHours(72)) - expiry.toEpochSecond() - } ?: now.plusHours(6).toEpochSecond() - } -} diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt new file mode 100644 index 00000000000..060114974fd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt @@ -0,0 +1,163 @@ +package org.schabi.newpipe + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.edit +import androidx.core.net.toUri +import androidx.preference.PreferenceManager +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkRequest +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.grack.nanojson.JsonParser +import com.grack.nanojson.JsonParserException +import org.schabi.newpipe.extractor.downloader.Response +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException +import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry +import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired +import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk +import java.io.IOException + +class NewVersionWorker( + context: Context, + workerParams: WorkerParameters +) : Worker(context, workerParams) { + + /** + * Method to compare the current and latest available app version. + * If a newer version is available, we show the update notification. + * + * @param versionName Name of new version + * @param apkLocationUrl Url with the new apk + * @param versionCode Code of new version + */ + private fun compareAppVersionAndShowNotification( + versionName: String, + apkLocationUrl: String?, + versionCode: Int + ) { + if (BuildConfig.VERSION_CODE >= versionCode) { + return + } + val app = App.getApp() + + // A pending intent to open the apk location url in the browser. + val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri()) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val pendingIntent = PendingIntent.getActivity(app, 0, intent, 0) + val channelId = app.getString(R.string.app_update_notification_channel_id) + val notificationBuilder = NotificationCompat.Builder(app, channelId) + .setSmallIcon(R.drawable.ic_newpipe_update) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setContentTitle(app.getString(R.string.app_update_notification_content_title)) + .setContentText( + app.getString(R.string.app_update_notification_content_text) + + " " + versionName + ) + val notificationManager = NotificationManagerCompat.from(app) + notificationManager.notify(2000, notificationBuilder.build()) + } + + @Throws(IOException::class, ReCaptchaException::class) + private fun checkNewVersion() { + // Check if the current apk is a github one or not. + if (!isReleaseApk()) { + return + } + + val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) + // Check if the last request has happened a certain time ago + // to reduce the number of API requests. + val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0) + if (!isLastUpdateCheckExpired(expiry)) { + return + } + + // Make a network request to get latest NewPipe data. + val response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL) + handleResponse(response) + } + + private fun handleResponse(response: Response) { + val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) + try { + // Store a timestamp which needs to be exceeded, + // before a new request to the API is made. + val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires")) + prefs.edit { + putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry) + } + } catch (e: Exception) { + if (DEBUG) { + Log.w(TAG, "Could not extract and save new expiry date", e) + } + } + + // Parse the json from the response. + try { + val githubStableObject = JsonParser.`object`() + .from(response.responseBody()).getObject("flavors") + .getObject("github").getObject("stable") + + val versionName = githubStableObject.getString("version") + val versionCode = githubStableObject.getInt("version_code") + val apkLocationUrl = githubStableObject.getString("apk") + compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode) + } catch (e: JsonParserException) { + // Most likely something is wrong in data received from NEWPIPE_API_URL. + // Do not alarm user and fail silently. + if (DEBUG) { + Log.w(TAG, "Could not get NewPipe API: invalid json", e) + } + } + } + + override fun doWork(): Result { + try { + checkNewVersion() + } catch (e: IOException) { + Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e) + return Result.failure() + } catch (e: ReCaptchaException) { + Log.e(TAG, "ReCaptchaException should never happen here.", e) + return Result.failure() + } + return Result.success() + } + + companion object { + private val DEBUG = MainActivity.DEBUG + private val TAG = NewVersionWorker::class.java.simpleName + private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json" + + /** + * Start a new worker which + * checks if all conditions for performing a version check are met, + * fetches the API endpoint [.NEWPIPE_API_URL] containing info + * about the latest NewPipe version + * and displays a notification about ana available update. + *

+ * Following conditions need to be met, before data is request from the server: + * + * * The app is signed with the correct signing key (by TeamNewPipe / schabi). + * If the signing key differs from the one used upstream, the update cannot be installed. + * * The user enabled searching for and notifying about updates in the settings. + * * The app did not recently check for updates. + * We do not want to make unnecessary connections and DOS our servers. + * + */ + @JvmStatic + fun enqueueNewVersionCheckingWork(context: Context) { + val workRequest: WorkRequest = + OneTimeWorkRequest.Builder(NewVersionWorker::class.java).build() + WorkManager.getInstance(context).enqueue(workRequest) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java index fde006a60f8..c7604e51283 100644 --- a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java +++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java @@ -14,7 +14,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.SaveUploaderUrlHelper; +import org.schabi.newpipe.util.SparseItemUtil; import java.util.Collections; @@ -62,7 +62,8 @@ public static void openPopupMenu(final PlayQueue playQueue, return true; case R.id.menu_item_channel_details: - SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(context, item, + SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(), + item.getUrl(), item.getUploaderUrl(), // An intent must be used here. // Opening with FragmentManager transactions is not working, // as PlayQueueActivity doesn't use fragments. diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 9d6e44f045b..adef3c0e4ba 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -633,7 +633,7 @@ private void openDownloadDialog() { .subscribe(result -> { final List sortedVideoStreams = ListHelper .getSortedStreamVideosList(this, result.getVideoStreams(), - result.getVideoOnlyStreams(), false); + result.getVideoOnlyStreams(), false, false); final int selectedVideoStreamIndex = ListHelper .getDefaultResolutionIndex(this, sortedVideoStreams); diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt index 1e5bd879959..50a3984e39d 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt @@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import org.schabi.newpipe.BuildConfig import org.schabi.newpipe.R @@ -21,30 +20,28 @@ import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.external_communication.ShareUtils class AboutActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { Localization.assureCorrectAppLanguage(this) super.onCreate(savedInstanceState) ThemeHelper.setTheme(this) title = getString(R.string.title_activity_about) + val aboutBinding = ActivityAboutBinding.inflate(layoutInflater) setContentView(aboutBinding.root) setSupportActionBar(aboutBinding.aboutToolbar) - supportActionBar!!.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + // Create the adapter that will return a fragment for each of the three // primary sections of the activity. val mAboutStateAdapter = AboutStateAdapter(this) - // Set up the ViewPager with the sections adapter. aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter TabLayoutMediator( aboutBinding.aboutTabLayout, aboutBinding.aboutViewPager2 - ) { tab: TabLayout.Tab, position: Int -> - when (position) { - POS_ABOUT -> tab.setText(R.string.tab_about) - POS_LICENSE -> tab.setText(R.string.tab_licenses) - else -> throw IllegalArgumentException("Unknown position for ViewPager2") - } + ) { tab, position -> + tab.setText(mAboutStateAdapter.getPageTitle(position)) }.attach() } @@ -75,13 +72,14 @@ class AboutActivity : AppCompatActivity() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - val aboutBinding = FragmentAboutBinding.inflate(inflater, container, false) - aboutBinding.aboutAppVersion.text = BuildConfig.VERSION_NAME - aboutBinding.aboutGithubLink.openLink(R.string.github_url) - aboutBinding.aboutDonationLink.openLink(R.string.donation_url) - aboutBinding.aboutWebsiteLink.openLink(R.string.website_url) - aboutBinding.aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url) - return aboutBinding.root + FragmentAboutBinding.inflate(inflater, container, false).apply { + aboutAppVersion.text = BuildConfig.VERSION_NAME + aboutGithubLink.openLink(R.string.github_url) + aboutDonationLink.openLink(R.string.donation_url) + aboutWebsiteLink.openLink(R.string.website_url) + aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url) + return root + } } } @@ -90,17 +88,29 @@ class AboutActivity : AppCompatActivity() { * one of the sections/tabs/pages. */ private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { + private val posAbout = 0 + private val posLicense = 1 + private val totalCount = 2 + override fun createFragment(position: Int): Fragment { return when (position) { - POS_ABOUT -> AboutFragment() - POS_LICENSE -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS) + posAbout -> AboutFragment() + posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS) else -> throw IllegalArgumentException("Unknown position for ViewPager2") } } override fun getItemCount(): Int { // Show 2 total pages. - return TOTAL_COUNT + return totalCount + } + + fun getPageTitle(position: Int): Int { + return when (position) { + posAbout -> R.string.tab_about + posLicense -> R.string.tab_licenses + else -> throw IllegalArgumentException("Unknown position for ViewPager2") + } } } @@ -117,10 +127,6 @@ class AboutActivity : AppCompatActivity() { "AndroidX", "2005 - 2011", "The Android Open Source Project", "https://developer.android.com/jetpack", StandardLicenses.APACHE2 ), - SoftwareComponent( - "CircleImageView", "2014 - 2020", "Henning Dodenhof", - "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2 - ), SoftwareComponent( "ExoPlayer", "2014 - 2020", "Google, Inc.", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2 @@ -191,8 +197,5 @@ class AboutActivity : AppCompatActivity() { "https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT ), ) - private const val POS_ABOUT = 0 - private const val POS_LICENSE = 1 - private const val TOTAL_COUNT = 2 } } diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt index a04de8abc4d..c1dd38389c5 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt @@ -87,60 +87,50 @@ object LicenseFragmentHelper { return context.getString(color).substring(3) } - @JvmStatic fun showLicense(context: Context?, license: License): Disposable { - return if (context == null) { - Disposable.empty() - } else { - Observable.fromCallable { getFormattedLicense(context, license) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { formattedLicense: String -> - val webViewData = Base64.encodeToString( - formattedLicense - .toByteArray(StandardCharsets.UTF_8), - Base64.NO_PADDING - ) - val webView = WebView(context) - webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") - val alert = AlertDialog.Builder(context) - alert.setTitle(license.name) - alert.setView(webView) - Localization.assureCorrectAppLanguage(context) - alert.setNegativeButton( - context.getString(R.string.ok) - ) { dialog, _ -> dialog.dismiss() } - alert.show() - } + return showLicense(context, license) { alertDialog -> + alertDialog.setPositiveButton(R.string.ok) { dialog, _ -> + dialog.dismiss() + } } } - @JvmStatic + fun showLicense(context: Context?, component: SoftwareComponent): Disposable { + return showLicense(context, component.license) { alertDialog -> + alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ -> + dialog.dismiss() + } + alertDialog.setNeutralButton(R.string.open_website_license) { _, _ -> + ShareUtils.openUrlInBrowser(context!!, component.link) + } + } + } + + private fun showLicense( + context: Context?, + license: License, + block: (AlertDialog.Builder) -> Unit + ): Disposable { return if (context == null) { Disposable.empty() } else { - Observable.fromCallable { getFormattedLicense(context, component.license) } + Observable.fromCallable { getFormattedLicense(context, license) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { formattedLicense: String -> + .subscribe { formattedLicense -> val webViewData = Base64.encodeToString( - formattedLicense - .toByteArray(StandardCharsets.UTF_8), - Base64.NO_PADDING + formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING ) val webView = WebView(context) webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") - val alert = AlertDialog.Builder(context) - alert.setTitle(component.license.name) - alert.setView(webView) - Localization.assureCorrectAppLanguage(context) - alert.setPositiveButton( - R.string.dismiss - ) { dialog, _ -> dialog.dismiss() } - alert.setNeutralButton(R.string.open_website_license) { _, _ -> - ShareUtils.openUrlInBrowser(context, component.link) + + AlertDialog.Builder(context).apply { + setTitle(license.name) + setView(webView) + Localization.assureCorrectAppLanguage(context) + block(this) + show() } - alert.show() } } } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 0a765ed4eec..150d4a8e5b5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -3,6 +3,7 @@ import androidx.annotation.Nullable; import androidx.room.Dao; import androidx.room.Query; +import androidx.room.RewriteQueriesToDropUnusedColumns; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; @@ -67,6 +68,7 @@ public Flowable> listByService(final int serviceId) { @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") public abstract int deleteStreamHistory(long streamId); + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM " + STREAM_TABLE // Select the latest entry and watch count for each stream id on history table diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index f4a0a758085..4941d939507 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -2,6 +2,7 @@ import androidx.room.Dao; import androidx.room.Query; +import androidx.room.RewriteQueriesToDropUnusedColumns; import androidx.room.Transaction; import org.schabi.newpipe.database.BasicDAO; @@ -52,6 +53,7 @@ default Flowable> listByService(final int serviceId) + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") Flowable getMaximumIndexOf(long playlistId); + @RewriteQueriesToDropUnusedColumns @Transaction @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " // get ids of streams of the given playlist diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 9798ec72dda..47b6f4dd9aa 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns import androidx.room.Transaction import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Maybe @@ -31,6 +32,7 @@ abstract class SubscriptionDAO : BasicDAO { ) abstract fun getSubscriptionsFiltered(filter: String): Flowable> + @RewriteQueriesToDropUnusedColumns @Query( """ SELECT * FROM subscriptions s @@ -47,6 +49,7 @@ abstract class SubscriptionDAO : BasicDAO { currentGroupId: Long ): Flowable> + @RewriteQueriesToDropUnusedColumns @Query( """ SELECT * FROM subscriptions s diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 5c954ad647d..f5c22690836 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -61,6 +61,7 @@ import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.SecondaryStreamHelper; +import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; @@ -151,7 +152,7 @@ public static DownloadDialog newInstance(final StreamInfo info) { public static DownloadDialog newInstance(final Context context, final StreamInfo info) { final ArrayList streamsList = new ArrayList<>(ListHelper .getSortedStreamVideosList(context, info.getVideoStreams(), - info.getVideoOnlyStreams(), false)); + info.getVideoOnlyStreams(), false, false)); final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList); final DownloadDialog instance = newInstance(info); @@ -321,21 +322,15 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved final int threads = prefs.getInt(getString(R.string.default_download_threads), 3); dialogBinding.threadsCount.setText(String.valueOf(threads)); dialogBinding.threads.setProgress(threads - 1); - dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() { @Override - public void onProgressChanged(final SeekBar seekbar, final int progress, + public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress, final boolean fromUser) { final int newProgress = progress + 1; prefs.edit().putInt(getString(R.string.default_download_threads), newProgress) .apply(); dialogBinding.threadsCount.setText(String.valueOf(newProgress)); } - - @Override - public void onStartTrackingTouch(final SeekBar p1) { } - - @Override - public void onStopTrackingTouch(final SeekBar p1) { } }); fetchStreamsSize(); diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.java index 8e7a455b4d5..97617337382 100644 --- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.java @@ -29,8 +29,8 @@ public enum UserAction { NEW_STREAMS_NOTIFICATIONS("new streams notifications"), PREFERENCES_MIGRATION("migration of preferences"), SHARE_TO_NEWPIPE("share to newpipe"), - CHECK_FOR_NEW_APP_VERSION("check for new app version"); - + CHECK_FOR_NEW_APP_VERSION("check for new app version"), + OPEN_INFO_ITEM_DIALOG("open info item dialog"); private final String message; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java index cbd44566e38..6b17803c489 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; @@ -10,7 +11,7 @@ */ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener { @Override - public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) { + public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { super.onScrolled(recyclerView, dx, dy); if (dy > 0) { int pastVisibleItems = 0; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java index 2fe615764fc..5016a49f60c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.fragments.detail; +import androidx.annotation.NonNull; + import org.schabi.newpipe.player.playqueue.PlayQueue; import java.io.Serializable; @@ -46,6 +48,7 @@ public PlayQueue getPlayQueue() { return playQueue; } + @NonNull @Override public String toString() { return getServiceId() + ":" + getUrl() + " > " + getTitle(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 2d9abc6dc67..0af5ec99e57 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1617,6 +1617,7 @@ public void handleResult(@NonNull final StreamInfo info) { activity, info.getVideoStreams(), info.getVideoOnlyStreams(), + false, false); selectedVideoStreamIndex = ListHelper .getDefaultResolutionIndex(activity, sortedVideoStreams); @@ -1994,9 +1995,7 @@ private void showSystemUi() { // Prevent jumping of the player on devices with cutout if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { activity.getWindow().getAttributes().layoutInDisplayCutoutMode = - isMultiWindowOrFullscreen() - ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; } activity.getWindow().getDecorView().setSystemUiVisibility(0); activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); @@ -2018,9 +2017,7 @@ private void hideSystemUi() { // Prevent jumping of the player on devices with cutout if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { activity.getWindow().getAttributes().layoutInDisplayCutoutMode = - isMultiWindowOrFullscreen() - ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; } int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN @@ -2037,7 +2034,7 @@ private void hideSystemUi() { activity.getWindow().getDecorView().setSystemUiVisibility(visibility); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && isMultiWindowOrFullscreen()) { + && (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) { activity.getWindow().setStatusBarColor(Color.TRANSPARENT); activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); } @@ -2053,11 +2050,6 @@ public void hideSystemUiIfNeeded() { } } - private boolean isMultiWindowOrFullscreen() { - return DeviceUtils.isInMultiWindow(activity) - || (isPlayerAvailable() && player.isFullscreen()); - } - private boolean playerIsNotStopped() { return isPlayerAvailable() && !player.isStopped(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 3c2e65bb7da..27e5a8571e4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.fragments.list; -import android.app.Activity; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; + import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; @@ -17,37 +19,26 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import androidx.viewbinding.ViewBinding; import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.PignateFooterBinding; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.player.helper.PlayerHolder; -import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; -import org.schabi.newpipe.util.StreamDialogEntry; import org.schabi.newpipe.views.SuperScrollLayoutManager; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Queue; - -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; +import java.util.function.Supplier; public abstract class BaseListFragment extends BaseStateFragment implements ListViewContract, StateSaver.WriteRead, @@ -79,11 +70,6 @@ public void onAttach(@NonNull final Context context) { } } - @Override - public void onDetach() { - super.onDetach(); - } - @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -220,14 +206,10 @@ public void onStart() { //////////////////////////////////////////////////////////////////////////*/ @Nullable - protected ViewBinding getListHeader() { + protected Supplier getListHeaderSupplier() { return null; } - protected ViewBinding getListFooter() { - return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false); - } - protected RecyclerView.LayoutManager getListLayoutManager() { return new SuperScrollLayoutManager(activity); } @@ -252,11 +234,10 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); infoListAdapter.setUseGridVariant(useGrid); - infoListAdapter.setFooter(getListFooter().getRoot()); - final ViewBinding listHeader = getListHeader(); - if (listHeader != null) { - infoListAdapter.setHeader(listHeader.getRoot()); + final Supplier listHeaderSupplier = getListHeaderSupplier(); + if (listHeaderSupplier != null) { + infoListAdapter.setHeaderSupplier(listHeaderSupplier); } itemsList.setAdapter(infoListAdapter); @@ -271,7 +252,7 @@ protected void onItemSelected(final InfoItem selectedItem) { @Override protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnClickGesture() { + infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<>() { @Override public void selected(final StreamInfoItem selectedItem) { onStreamSelected(selectedItem); @@ -279,11 +260,11 @@ public void selected(final StreamInfoItem selectedItem) { @Override public void held(final StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + showInfoItemDialog(selectedItem); } }); - infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { + infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() { @Override public void selected(final ChannelInfoItem selectedItem) { try { @@ -299,7 +280,7 @@ public void selected(final ChannelInfoItem selectedItem) { } }); - infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() { + infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() { @Override public void selected(final PlaylistInfoItem selectedItem) { try { @@ -315,22 +296,99 @@ public void selected(final PlaylistInfoItem selectedItem) { } }); - infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture() { + infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() { @Override public void selected(final CommentsInfoItem selectedItem) { onItemSelected(selectedItem); } }); + // Ensure that there is always a scroll listener (e.g. when rotating the device) + useNormalItemListScrollListener(); + } + + /** + * Removes all listeners and adds the normal scroll listener to the {@link #itemsList}. + */ + protected void useNormalItemListScrollListener() { + if (DEBUG) { + Log.d(TAG, "useNormalItemListScrollListener called"); + } + itemsList.clearOnScrollListeners(); + itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener()); + } + + /** + * Removes all listeners and adds the initial scroll listener to the {@link #itemsList}. + *
+ * Which tries to load more items when not enough are in the view (not scrollable) + * and more are available. + *
+ * Note: This method only works because "This callback will also be called if visible + * item range changes after a layout calculation. In that case, dx and dy will be 0." + * - which might be unexpected because no actual scrolling occurs... + *
+ * This listener will be replaced by DefaultItemListOnScrolledDownListener when + *
    + *
  • the view was actually scrolled
  • + *
  • the view is scrollable
  • + *
  • no more items can be loaded
  • + *
+ */ + protected void useInitialItemListLoadScrollListener() { + if (DEBUG) { + Log.d(TAG, "useInitialItemListLoadScrollListener called"); + } itemsList.clearOnScrollListeners(); - itemsList.addOnScrollListener(new OnScrollBelowItemsListener() { + itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener() { @Override - public void onScrolledDown(final RecyclerView recyclerView) { - onScrollToBottom(); + public void onScrolled(@NonNull final RecyclerView recyclerView, + final int dx, final int dy) { + super.onScrolled(recyclerView, dx, dy); + + if (dy != 0) { + log("Vertical scroll occurred"); + + useNormalItemListScrollListener(); + return; + } + if (isLoading.get()) { + log("Still loading data -> Skipping"); + return; + } + if (!hasMoreItems()) { + log("No more items to load"); + + useNormalItemListScrollListener(); + return; + } + if (itemsList.canScrollVertically(1) + || itemsList.canScrollVertically(-1)) { + log("View is scrollable"); + + useNormalItemListScrollListener(); + return; + } + + log("Loading more data"); + loadMoreItems(); + } + + private void log(final String msg) { + if (DEBUG) { + Log.d(TAG, "initItemListLoadScrollListener - " + msg); + } } }); } + class DefaultItemListOnScrolledDownListener extends OnScrollBelowItemsListener { + @Override + public void onScrolledDown(final RecyclerView recyclerView) { + onScrollToBottom(); + } + } + private void onStreamSelected(final StreamInfoItem selectedItem) { onItemSelected(selectedItem); NavigationHelper.openVideoDetailFragment(requireContext(), getFM(), @@ -344,55 +402,12 @@ protected void onScrollToBottom() { } } - protected void showStreamDialog(final StreamInfoItem item) { - final Context context = getContext(); - final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) { - return; - } - final List entries = new ArrayList<>(); - - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } - } - - if (item.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) { - entries.add( - StreamDialogEntry.mark_as_watched - ); - } - if (!isNullOrEmpty(item.getUploaderUrl())) { - entries.add(StreamDialogEntry.show_channel_details); + protected void showInfoItemDialog(final StreamInfoItem item) { + try { + new InfoItemDialog.Builder(getActivity(), getContext(), this, item).create().show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, item); } - - StreamDialogEntry.setEnabledEntries(entries); - - new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show(); } /*////////////////////////////////////////////////////////////////////////// @@ -418,6 +433,12 @@ public void onCreateOptionsMenu(@NonNull final Menu menu, // Load and handle //////////////////////////////////////////////////////////////////////////*/ + @Override + protected void startLoading(final boolean forceLoad) { + useInitialItemListLoadScrollListener(); + super.startLoading(forceLoad); + } + protected abstract void loadMoreItems(); protected abstract boolean hasMoreItems(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index e98dc9fdadf..35424437da7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -9,6 +9,7 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.Page; @@ -27,8 +28,8 @@ import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public abstract class BaseListInfoFragment - extends BaseListFragment { +public abstract class BaseListInfoFragment> + extends BaseListFragment> { @State protected int serviceId = Constants.NO_SERVICE_ID; @State @@ -37,7 +38,7 @@ public abstract class BaseListInfoFragment protected String url; private final UserAction errorUserAction; - protected I currentInfo; + protected L currentInfo; protected Page currentNextPage; protected Disposable currentWorker; @@ -65,7 +66,7 @@ public void onResume() { super.onResume(); // Check if it was loading when the fragment was stopped/paused, if (wasLoading.getAndSet(false)) { - if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) { + if (hasMoreItems() && !infoListAdapter.getItemsList().isEmpty()) { loadMoreItems(); } else { doInitialLoadLogic(); @@ -97,7 +98,7 @@ public void writeTo(final Queue objectsToSave) { @SuppressWarnings("unchecked") public void readFrom(@NonNull final Queue savedObjects) throws Exception { super.readFrom(savedObjects); - currentInfo = (I) savedObjects.poll(); + currentInfo = (L) savedObjects.poll(); currentNextPage = (Page) savedObjects.poll(); } @@ -105,6 +106,7 @@ public void readFrom(@NonNull final Queue savedObjects) throws Exception // Load and handle //////////////////////////////////////////////////////////////////////////*/ + @Override protected void doInitialLoadLogic() { if (DEBUG) { Log.d(TAG, "doInitialLoadLogic() called"); @@ -123,7 +125,7 @@ protected void doInitialLoadLogic() { * @param forceLoad allow or disallow the result to come from the cache * @return Rx {@link Single} containing the {@link ListInfo} */ - protected abstract Single loadResult(boolean forceLoad); + protected abstract Single loadResult(boolean forceLoad); @Override public void startLoading(final boolean forceLoad) { @@ -139,7 +141,7 @@ public void startLoading(final boolean forceLoad) { currentWorker = loadResult(forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe((@NonNull I result) -> { + .subscribe((@NonNull L result) -> { isLoading.set(false); currentInfo = result; currentNextPage = result.getNextPage(); @@ -156,8 +158,9 @@ public void startLoading(final boolean forceLoad) { * * @return Rx {@link Single} containing the {@link ListExtractor.InfoItemsPage} */ - protected abstract Single loadMoreItemsLogic(); + protected abstract Single> loadMoreItemsLogic(); + @Override protected void loadMoreItems() { isLoading.set(true); @@ -171,9 +174,9 @@ protected void loadMoreItems() { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doFinally(this::allowDownwardFocusScroll) - .subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> { + .subscribe(infoItemsPage -> { isLoading.set(false); - handleNextItems(InfoItemsPage); + handleNextItems(infoItemsPage); }, (@NonNull Throwable throwable) -> dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable, errorUserAction, "Loading more items: " + url, serviceId))); @@ -192,7 +195,7 @@ private void allowDownwardFocusScroll() { } @Override - public void handleNextItems(final ListExtractor.InfoItemsPage result) { + public void handleNextItems(final ListExtractor.InfoItemsPage result) { super.handleNextItems(result); currentNextPage = result.getNextPage(); @@ -216,14 +219,14 @@ protected boolean hasMoreItems() { //////////////////////////////////////////////////////////////////////////*/ @Override - public void handleResult(@NonNull final I result) { + public void handleResult(@NonNull final L result) { super.handleResult(result); name = result.getName(); setTitle(name); if (infoListAdapter.getItemsList().isEmpty()) { - if (result.getRelatedItems().size() > 0) { + if (!result.getRelatedItems().isEmpty()) { infoListAdapter.addInfoItemList(result.getRelatedItems()); showListFooter(hasMoreItems()); } else { @@ -240,7 +243,7 @@ public void handleResult(@NonNull final I result) { final List errors = new ArrayList<>(result.getErrors()); // handling ContentNotSupportedException not to show the error but an appropriate string // so that crashes won't be sent uselessly and the user will understand what happened - errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException); + errors.removeIf(ContentNotSupportedException.class::isInstance); if (!errors.isEmpty()) { dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index ccc2be7b461..869503b5bed 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -22,7 +22,6 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.core.content.ContextCompat; -import androidx.viewbinding.ViewBinding; import com.google.android.material.snackbar.Snackbar; import com.jakewharton.rxbinding4.view.RxView; @@ -36,7 +35,6 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; @@ -51,13 +49,14 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.util.external_communication.ShareUtils; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; @@ -69,7 +68,7 @@ import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.schedulers.Schedulers; -public class ChannelFragment extends BaseListInfoFragment +public class ChannelFragment extends BaseListInfoFragment implements View.OnClickListener { private static final int BUTTON_DEBOUNCE_INTERVAL = 100; @@ -150,12 +149,12 @@ public void onDestroy() { //////////////////////////////////////////////////////////////////////////*/ @Override - protected ViewBinding getListHeader() { + protected Supplier getListHeaderSupplier() { headerBinding = ChannelHeaderBinding .inflate(activity.getLayoutInflater(), itemsList, false); playlistControlBinding = headerBinding.playlistControl; - return headerBinding; + return headerBinding::getRoot; } @Override @@ -189,13 +188,6 @@ public void onCreateOptionsMenu(@NonNull final Menu menu, } } - private void openRssFeed() { - final ChannelInfo info = currentInfo; - if (info != null) { - ShareUtils.openUrlInBrowser(requireContext(), info.getFeedUrl(), false); - } - } - @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { @@ -208,7 +200,10 @@ public boolean onOptionsItemSelected(final MenuItem item) { setNotify(value); break; case R.id.menu_item_rss: - openRssFeed(); + if (currentInfo != null) { + ShareUtils.openUrlInBrowser( + requireContext(), currentInfo.getFeedUrl(), false); + } break; case R.id.menu_item_openInBrowser: if (currentInfo != null) { @@ -438,7 +433,7 @@ private void showNotifySnackbar() { //////////////////////////////////////////////////////////////////////////*/ @Override - protected Single loadMoreItemsLogic() { + protected Single> loadMoreItemsLogic() { return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage); } @@ -575,12 +570,11 @@ private PlayQueue getPlayQueue() { } private PlayQueue getPlayQueue(final int index) { - final List streamItems = new ArrayList<>(); - for (final InfoItem i : infoListAdapter.getItemsList()) { - if (i instanceof StreamInfoItem) { - streamItems.add((StreamInfoItem) i); - } - } + final List streamItems = infoListAdapter.getItemsList().stream() + .filter(StreamInfoItem.class::isInstance) + .map(StreamInfoItem.class::cast) + .collect(Collectors.toList()); + return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(), currentInfo.getNextPage(), streamItems, index); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java index 3d11e90c0d5..3b092cc2885 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java @@ -15,6 +15,7 @@ import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.comments.CommentsInfo; +import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.util.ExtractorHelper; @@ -22,7 +23,7 @@ import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; -public class CommentsFragment extends BaseListInfoFragment { +public class CommentsFragment extends BaseListInfoFragment { private final CompositeDisposable disposables = new CompositeDisposable(); private TextView emptyStateDesc; @@ -67,7 +68,7 @@ public void onDestroy() { //////////////////////////////////////////////////////////////////////////*/ @Override - protected Single loadMoreItemsLogic() { + protected Single> loadMoreItemsLogic() { return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java index c25f18e8b72..0b01627d6e9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java @@ -21,6 +21,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; import org.schabi.newpipe.extractor.localization.ContentCountry; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.KioskTranslator; @@ -53,7 +54,7 @@ *

*/ -public class KioskFragment extends BaseListInfoFragment { +public class KioskFragment extends BaseListInfoFragment { @State String kioskId = ""; String kioskTranslatedName; @@ -145,7 +146,7 @@ public Single loadResult(final boolean forceReload) { } @Override - public Single loadMoreItemsLogic() { + public Single> loadMoreItemsLogic() { return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 640d08064d7..5bf20c144f3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -1,7 +1,10 @@ package org.schabi.newpipe.fragments.list.playlist; -import android.app.Activity; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; + import android.content.Context; +import android.content.res.ColorStateList; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; @@ -15,7 +18,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; -import androidx.viewbinding.ViewBinding; +import androidx.core.content.ContextCompat; + +import com.google.android.material.shape.CornerFamily; +import com.google.android.material.shape.ShapeAppearanceModel; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -33,26 +39,23 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.player.MainPlayer.PlayerType; -import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.PicassoHelper; -import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PicassoHelper; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.util.external_communication.ShareUtils; -import org.schabi.newpipe.util.StreamDialogEntry; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable; @@ -60,11 +63,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; - -public class PlaylistFragment extends BaseListInfoFragment { +public class PlaylistFragment extends BaseListInfoFragment { private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG"; @@ -120,12 +119,12 @@ public View onCreateView(@NonNull final LayoutInflater inflater, //////////////////////////////////////////////////////////////////////////*/ @Override - protected ViewBinding getListHeader() { + protected Supplier getListHeaderSupplier() { headerBinding = PlaylistHeaderBinding .inflate(activity.getLayoutInflater(), itemsList, false); playlistControlBinding = headerBinding.playlistControl; - return headerBinding; + return headerBinding::getRoot; } @Override @@ -140,60 +139,22 @@ private PlayQueue getPlayQueueStartingAt(final StreamInfoItem infoItem) { } @Override - protected void showStreamDialog(final StreamInfoItem item) { + protected void showInfoItemDialog(final StreamInfoItem item) { final Context context = getContext(); - final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) { - return; - } - - final ArrayList entries = new ArrayList<>(); - - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } - } - - if (item.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); + try { + final InfoItemDialog.Builder dialogBuilder = + new InfoItemDialog.Builder(getActivity(), context, this, item); + + dialogBuilder + .setAction( + StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND, + (f, infoItem) -> NavigationHelper.playOnBackgroundPlayer( + context, getPlayQueueStartingAt(infoItem), true)) + .create() + .show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, item); } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) { - entries.add( - StreamDialogEntry.mark_as_watched - ); - } - if (!isNullOrEmpty(item.getUploaderUrl())) { - entries.add(StreamDialogEntry.show_channel_details); - } - - StreamDialogEntry.setEnabledEntries(entries); - - StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) -> - NavigationHelper.playOnBackgroundPlayer(context, - getPlayQueueStartingAt(infoItem), true)); - - new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show(); } @Override @@ -249,7 +210,7 @@ public void onDestroy() { //////////////////////////////////////////////////////////////////////////*/ @Override - protected Single loadMoreItemsLogic() { + protected Single> loadMoreItemsLogic() { return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage); } @@ -328,9 +289,14 @@ public void handleResult(@NonNull final PlaylistInfo result) { && (YoutubeParsingHelper.isYoutubeMixId(result.getId()) || YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) { // this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown - headerBinding.uploaderAvatarView.setDisableCircularTransformation(true); - headerBinding.uploaderAvatarView.setBorderColor( - getResources().getColor(R.color.transparent_background_color)); + final ShapeAppearanceModel model = ShapeAppearanceModel.builder() + .setAllCorners(CornerFamily.ROUNDED, 0f) + .build(); // this turns the image back into a square + headerBinding.uploaderAvatarView.setShapeAppearanceModel(model); + headerBinding.uploaderAvatarView.setStrokeColor( + ColorStateList.valueOf(ContextCompat.getColor( + requireContext(), R.color.transparent_background_color)) + ); headerBinding.uploaderAvatarView.setImageDrawable( AppCompatResources.getDrawable(requireContext(), R.drawable.ic_radio) @@ -413,7 +379,7 @@ private Flowable getUpdateProcessor( } private Subscriber> getPlaylistBookmarkSubscriber() { - return new Subscriber>() { + return new Subscriber<>() { @Override public void onSubscribe(final Subscription s) { if (bookmarkReactor != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index 3cfcfd47086..fb983b01e26 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -7,6 +7,7 @@ import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import org.schabi.newpipe.R; @@ -34,8 +35,10 @@ public void setListener(final OnSuggestionItemSelected listener) { this.listener = listener; } + @NonNull @Override - public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { + public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent, + final int viewType) { return new SuggestionItemHolder(LayoutInflater.from(context) .inflate(R.layout.item_search_suggestion, parent, false)); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java index 6532417c064..f0ece69f37c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.fragments.list.videos; -import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; @@ -12,11 +11,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; -import androidx.viewbinding.ViewBinding; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; @@ -24,14 +23,14 @@ import org.schabi.newpipe.util.RelatedItemInfo; import java.io.Serializable; +import java.util.function.Supplier; import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -public class RelatedItemsFragment extends BaseListInfoFragment +public class RelatedItemsFragment extends BaseListInfoFragment implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String INFO_KEY = "related_info_key"; - private final CompositeDisposable disposables = new CompositeDisposable(); + private RelatedItemInfo relatedItemInfo; /*////////////////////////////////////////////////////////////////////////// @@ -54,11 +53,6 @@ public RelatedItemsFragment() { // LifeCycle //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - } - @Override public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @@ -66,12 +60,6 @@ public View onCreateView(@NonNull final LayoutInflater inflater, return inflater.inflate(R.layout.fragment_related_items, container, false); } - @Override - public void onDestroy() { - super.onDestroy(); - disposables.clear(); - } - @Override public void onDestroyView() { headerBinding = null; @@ -79,26 +67,27 @@ public void onDestroyView() { } @Override - protected ViewBinding getListHeader() { - if (relatedItemInfo != null && relatedItemInfo.getRelatedItems() != null) { - headerBinding = RelatedItemsHeaderBinding - .inflate(activity.getLayoutInflater(), itemsList, false); - - final SharedPreferences pref = PreferenceManager - .getDefaultSharedPreferences(requireContext()); - final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); - headerBinding.autoplaySwitch.setChecked(autoplay); - headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) -> - PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() - .putBoolean(getString(R.string.auto_queue_key), b).apply()); - return headerBinding; - } else { + protected Supplier getListHeaderSupplier() { + if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) { return null; } + + headerBinding = RelatedItemsHeaderBinding + .inflate(activity.getLayoutInflater(), itemsList, false); + + final SharedPreferences pref = PreferenceManager + .getDefaultSharedPreferences(requireContext()); + final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); + headerBinding.autoplaySwitch.setChecked(autoplay); + headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) -> + PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() + .putBoolean(getString(R.string.auto_queue_key), b).apply()); + + return headerBinding::getRoot; } @Override - protected Single loadMoreItemsLogic() { + protected Single> loadMoreItemsLogic() { return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage); } @@ -128,7 +117,6 @@ public void handleResult(@NonNull final RelatedItemInfo result) { } ViewUtils.slideUp(requireView(), 120, 96, 0.06f); - disposables.clear(); } /*////////////////////////////////////////////////////////////////////////// @@ -137,11 +125,13 @@ public void handleResult(@NonNull final RelatedItemInfo result) { @Override public void setTitle(final String title) { + // Nothing to do - override parent } @Override public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { + // Nothing to do - override parent } private void setInitialData(final StreamInfo info) { @@ -169,11 +159,10 @@ protected void onRestoreInstanceState(@NonNull final Bundle savedState) { @Override public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String s) { - final SharedPreferences pref = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); if (headerBinding != null) { - headerBinding.autoplaySwitch.setChecked(autoplay); + headerBinding.autoplaySwitch.setChecked( + sharedPreferences.getBoolean( + getString(R.string.auto_queue_key), false)); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java deleted file mode 100644 index c485337f0fc..00000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.schabi.newpipe.info_list; - -import android.app.Activity; -import android.content.DialogInterface; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; - -public class InfoItemDialog { - private final AlertDialog dialog; - - public InfoItemDialog(@NonNull final Activity activity, - @NonNull final StreamInfoItem info, - @NonNull final String[] commands, - @NonNull final DialogInterface.OnClickListener actions) { - this(activity, commands, actions, info.getName(), info.getUploaderName()); - } - - public InfoItemDialog(@NonNull final Activity activity, - @NonNull final String[] commands, - @NonNull final DialogInterface.OnClickListener actions, - @NonNull final String title, - @Nullable final String additionalDetail) { - - final View bannerView = View.inflate(activity, R.layout.dialog_title, null); - bannerView.setSelected(true); - - final TextView titleView = bannerView.findViewById(R.id.itemTitleView); - titleView.setText(title); - - final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails); - if (additionalDetail != null) { - detailsView.setText(additionalDetail); - detailsView.setVisibility(View.VISIBLE); - } else { - detailsView.setVisibility(View.GONE); - } - - dialog = new AlertDialog.Builder(activity) - .setCustomTitle(bannerView) - .setItems(commands, actions) - .create(); - } - - public void show() { - dialog.show(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 56bc633843c..fb27593e7e0 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -2,6 +2,7 @@ import android.content.Context; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -10,7 +11,7 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import org.schabi.newpipe.databinding.PignateFooterBinding; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; @@ -34,6 +35,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; /* * Created by Christian Schabesberger on 01.08.16. @@ -74,18 +76,20 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; + private final List infoItemList; private final HistoryRecordManager recordManager; private boolean useMiniVariant = false; private boolean useGridVariant = false; private boolean showFooter = false; - private View header = null; - private View footer = null; + + private Supplier headerSupplier = null; public InfoListAdapter(final Context context) { - this.recordManager = new HistoryRecordManager(context); + layoutInflater = LayoutInflater.from(context); + recordManager = new HistoryRecordManager(context); infoItemBuilder = new InfoItemBuilder(context); infoItemList = new ArrayList<>(); } @@ -129,12 +133,12 @@ public void addInfoItemList(@Nullable final List data) { if (DEBUG) { Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", " + "infoItemList.size() = " + infoItemList.size() + ", " - + "header = " + header + ", footer = " + footer + ", " + + "hasHeader = " + hasHeader() + ", " + "showFooter = " + showFooter); } notifyItemRangeInserted(offsetStart, data.size()); - if (footer != null && showFooter) { + if (showFooter) { final int footerNow = sizeConsideringHeaderOffset(); notifyItemMoved(offsetStart, footerNow); @@ -145,43 +149,6 @@ public void addInfoItemList(@Nullable final List data) { } } - public void setInfoItemList(final List data) { - infoItemList.clear(); - infoItemList.addAll(data); - notifyDataSetChanged(); - } - - public void addInfoItem(@Nullable final InfoItem data) { - if (data == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "addInfoItem() before > infoItemList.size() = " - + infoItemList.size() + ", thread = " + Thread.currentThread()); - } - - final int positionInserted = sizeConsideringHeaderOffset(); - infoItemList.add(data); - - if (DEBUG) { - Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", " - + "infoItemList.size() = " + infoItemList.size() + ", " - + "header = " + header + ", footer = " + footer + ", " - + "showFooter = " + showFooter); - } - notifyItemInserted(positionInserted); - - if (footer != null && showFooter) { - final int footerNow = sizeConsideringHeaderOffset(); - notifyItemMoved(positionInserted, footerNow); - - if (DEBUG) { - Log.d(TAG, "addInfoItem() footer from " + positionInserted - + " to " + footerNow); - } - } - } - public void clearStreamItemList() { if (infoItemList.isEmpty()) { return; @@ -190,16 +157,16 @@ public void clearStreamItemList() { notifyDataSetChanged(); } - public void setHeader(final View header) { - final boolean changed = header != this.header; - this.header = header; + public void setHeaderSupplier(@Nullable final Supplier headerSupplier) { + final boolean changed = headerSupplier != this.headerSupplier; + this.headerSupplier = headerSupplier; if (changed) { notifyDataSetChanged(); } } - public void setFooter(final View view) { - this.footer = view; + protected boolean hasHeader() { + return this.headerSupplier != null; } public void showFooter(final boolean show) { @@ -219,48 +186,49 @@ public void showFooter(final boolean show) { } private int sizeConsideringHeaderOffset() { - final int i = infoItemList.size() + (header != null ? 1 : 0); + final int i = infoItemList.size() + (hasHeader() ? 1 : 0); if (DEBUG) { Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i); } return i; } - public ArrayList getItemsList() { + public List getItemsList() { return infoItemList; } @Override public int getItemCount() { int count = infoItemList.size(); - if (header != null) { + if (hasHeader()) { count++; } - if (footer != null && showFooter) { + if (showFooter) { count++; } if (DEBUG) { Log.d(TAG, "getItemCount() called with: " + "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", " - + "header = " + header + ", footer = " + footer + ", " + + "hasHeader = " + hasHeader() + ", " + "showFooter = " + showFooter); } return count; } + @SuppressWarnings("FinalParameters") @Override public int getItemViewType(int position) { if (DEBUG) { Log.d(TAG, "getItemViewType() called with: position = [" + position + "]"); } - if (header != null && position == 0) { + if (hasHeader() && position == 0) { return HEADER_TYPE; - } else if (header != null) { + } else if (hasHeader()) { position--; } - if (footer != null && position == infoItemList.size() && showFooter) { + if (position == infoItemList.size() && showFooter) { return FOOTER_TYPE; } final InfoItem item = infoItemList.get(position); @@ -290,10 +258,16 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup paren + "parent = [" + parent + "], type = [" + type + "]"); } switch (type) { + // #4475 and #3368 + // Always create a new instance otherwise the same instance + // is sometimes reused which causes a crash case HEADER_TYPE: - return new HFHolder(header); + return new HFHolder(headerSupplier.get()); case FOOTER_TYPE: - return new HFHolder(footer); + return new HFHolder(PignateFooterBinding + .inflate(layoutInflater, parent, false) + .getRoot() + ); case MINI_STREAM_HOLDER_TYPE: return new StreamMiniInfoItemHolder(infoItemBuilder, parent); case STREAM_HOLDER_TYPE: @@ -322,42 +296,17 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup paren } @Override - public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) { + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, + final int position) { if (DEBUG) { Log.d(TAG, "onBindViewHolder() called with: " + "holder = [" + holder.getClass().getSimpleName() + "], " + "position = [" + position + "]"); } if (holder instanceof InfoItemHolder) { - // If header isn't null, offset the items by -1 - if (header != null) { - position--; - } - - ((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager); - } else if (holder instanceof HFHolder && position == 0 && header != null) { - ((HFHolder) holder).view = header; - } else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() - && footer != null && showFooter) { - ((HFHolder) holder).view = footer; - } - } - - @Override - public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position, - @NonNull final List payloads) { - if (!payloads.isEmpty() && holder instanceof InfoItemHolder) { - for (final Object payload : payloads) { - if (payload instanceof StreamStateEntity) { - ((InfoItemHolder) holder).updateState(infoItemList - .get(header == null ? position : position - 1), recordManager); - } else if (payload instanceof Boolean) { - ((InfoItemHolder) holder).updateState(infoItemList - .get(header == null ? position : position - 1), recordManager); - } - } - } else { - onBindViewHolder(holder, position); + ((InfoItemHolder) holder).updateFromItem( + // If header is present, offset the items by -1 + infoItemList.get(hasHeader() ? position - 1 : position), recordManager); } } @@ -371,12 +320,9 @@ public int getSpanSize(final int position) { }; } - public static class HFHolder extends RecyclerView.ViewHolder { - public View view; - + static class HFHolder extends RecyclerView.ViewHolder { HFHolder(final View v) { super(v); - view = v; } } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java new file mode 100644 index 00000000000..5a266c0a860 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java @@ -0,0 +1,356 @@ +package org.schabi.newpipe.info_list.dialog; + +import static org.schabi.newpipe.MainActivity.DEBUG; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.App; +import org.schabi.newpipe.R; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.player.helper.PlayerHolder; +import org.schabi.newpipe.util.external_communication.KoreUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * Dialog for a {@link StreamInfoItem}. + * The dialog's content are actions that can be performed on the {@link StreamInfoItem}. + * This dialog is mostly used for longpress context menus. + */ +public final class InfoItemDialog { + private static final String TAG = Build.class.getSimpleName(); + /** + * Ideally, {@link InfoItemDialog} would extend {@link AlertDialog}. + * However, extending {@link AlertDialog} requires many additional lines + * and brings more complexity to this class, especially the constructor. + * To circumvent this, an {@link AlertDialog.Builder} is used in the constructor. + * Its result is stored in this class variable to allow access via the {@link #show()} method. + */ + private final AlertDialog dialog; + + private InfoItemDialog(@NonNull final Activity activity, + @NonNull final Fragment fragment, + @NonNull final StreamInfoItem info, + @NonNull final List entries) { + + // Create the dialog's title + final View bannerView = View.inflate(activity, R.layout.dialog_title, null); + bannerView.setSelected(true); + + final TextView titleView = bannerView.findViewById(R.id.itemTitleView); + titleView.setText(info.getName()); + + final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails); + if (info.getUploaderName() != null) { + detailsView.setText(info.getUploaderName()); + detailsView.setVisibility(View.VISIBLE); + } else { + detailsView.setVisibility(View.GONE); + } + + // Get the entry's descriptions which are displayed in the dialog + final String[] items = entries.stream() + .map(entry -> entry.getString(activity)).toArray(String[]::new); + + // Call an entry's action / onClick method when the entry is selected. + final DialogInterface.OnClickListener action = (d, index) -> + entries.get(index).action.onClick(fragment, info); + + dialog = new AlertDialog.Builder(activity) + .setCustomTitle(bannerView) + .setItems(items, action) + .create(); + + } + + public void show() { + dialog.show(); + } + + /** + *

Builder to generate a {@link InfoItemDialog} for a {@link StreamInfoItem}.

+ * Use {@link #addEntry(StreamDialogDefaultEntry)} + * and {@link #addAllEntries(StreamDialogDefaultEntry...)} to add options to the dialog. + *
+ * Custom actions for entries can be set using + * {@link #setAction(StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)}. + */ + public static class Builder { + @NonNull private final Activity activity; + @NonNull private final Context context; + @NonNull private final StreamInfoItem infoItem; + @NonNull private final Fragment fragment; + @NonNull private final List entries = new ArrayList<>(); + private final boolean addDefaultEntriesAutomatically; + + /** + *

Create a {@link Builder builder} instance for a {@link StreamInfoItem} + * that automatically adds the some default entries + * at the top and bottom of the dialog.

+ * The dialog has the following structure: + *
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | ENQUEUE                                    |
+         *     | ENQUEUE_NEXT                               |
+         *     | START_ON_BACKGROUND                        |
+         *     | START_ON_POPUP                             |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | entries added manually with                |
+         *     | addEntry() and addAllEntries()             |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | APPEND_PLAYLIST                            |
+         *     | SHARE                                      |
+         *     | OPEN_IN_BROWSER                            |
+         *     | PLAY_WITH_KODI                             |
+         *     | MARK_AS_WATCHED                            |
+         *     | SHOW_CHANNEL_DETAILS                       |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         * 
+ * 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.

+ * The dialog has the following structure: + *
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | ENQUEUE                                    |
+         *     | ENQUEUE_NEXT                               |
+         *     | START_ON_BACKGROUND                        |
+         *     | START_ON_POPUP                             |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | entries added manually with                |
+         *     | addEntry() and addAllEntries()             |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         *     | APPEND_PLAYLIST                            |
+         *     | SHARE                                      |
+         *     | OPEN_IN_BROWSER                            |
+         *     | PLAY_WITH_KODI                             |
+         *     | MARK_AS_WATCHED                            |
+         *     | SHOW_CHANNEL_DETAILS                       |
+         *     + - - - - - - - - - - - - - - - - - - - - - -+
+         * 
+ * Please note that some entries are not added depending on the user's preferences, + * the item's {@link StreamType} and the current player state. + * + * @param activity + * @param context + * @param fragment + * @param infoItem + * @param addDefaultEntriesAutomatically + * whether default entries added with {@link #addDefaultBeginningEntries()} + * and {@link #addDefaultEndEntries()} are added automatically when generating + * the {@link InfoItemDialog}. + *
+ * Entries added with {@link #addEntry(StreamDialogDefaultEntry)} and + * {@link #addAllEntries(StreamDialogDefaultEntry...)} are added in between. + * @throws IllegalArgumentException if activity, context + * or resources is null + */ + public Builder(final Activity activity, + final Context context, + @NonNull final Fragment fragment, + @NonNull final StreamInfoItem infoItem, + final boolean addDefaultEntriesAutomatically) { + if (activity == null || context == null || context.getResources() == null) { + if (DEBUG) { + Log.d(TAG, "activity, context or resources is null: activity = " + + activity + ", context = " + context); + } + throw new IllegalArgumentException("activity, context or resources is null"); + } + this.activity = activity; + this.context = context; + this.fragment = fragment; + this.infoItem = infoItem; + this.addDefaultEntriesAutomatically = addDefaultEntriesAutomatically; + if (addDefaultEntriesAutomatically) { + addDefaultBeginningEntries(); + } + } + + /** + * Adds a new entry and appends it to the current entry list. + * @param entry the entry to add + * @return the current {@link Builder} instance + */ + public Builder addEntry(@NonNull final StreamDialogDefaultEntry entry) { + entries.add(entry.toStreamDialogEntry()); + return this; + } + + /** + * Adds new entries. These are appended to the current entry list. + * @param newEntries the entries to add + * @return the current {@link Builder} instance + */ + public Builder addAllEntries(@NonNull final StreamDialogDefaultEntry... newEntries) { + Stream.of(newEntries).forEach(this::addEntry); + return this; + } + + /** + *

Change an entries' action that is called when the entry is selected.

+ *

Warning: Only use this method when the entry has been already added. + * Changing the action of an entry which has not been added to the Builder yet + * does not have an effect.

+ * @param entry the entry to change + * @param action the action to perform when the entry is selected + * @return the current {@link Builder} instance + */ + public Builder setAction(@NonNull final StreamDialogDefaultEntry entry, + @NonNull final StreamDialogEntry.StreamDialogEntryAction action) { + for (int i = 0; i < entries.size(); i++) { + if (entries.get(i).resource == entry.resource) { + entries.set(i, new StreamDialogEntry(entry.resource, action)); + return this; + } + } + return this; + } + + /** + * Adds {@link StreamDialogDefaultEntry#ENQUEUE} if the player is open and + * {@link StreamDialogDefaultEntry#ENQUEUE_NEXT} if there are multiple streams + * in the play queue. + * @return the current {@link Builder} instance + */ + public Builder addEnqueueEntriesIfNeeded() { + if (PlayerHolder.getInstance().isPlayQueueReady()) { + addEntry(StreamDialogDefaultEntry.ENQUEUE); + + if (PlayerHolder.getInstance().getQueueSize() > 1) { + addEntry(StreamDialogDefaultEntry.ENQUEUE_NEXT); + } + } + return this; + } + + /** + * Adds the {@link StreamDialogDefaultEntry#START_HERE_ON_BACKGROUND}. + * If the {@link #infoItem} is not a pure audio (live) stream, + * {@link StreamDialogDefaultEntry#START_HERE_ON_POPUP} is added, too. + * @return the current {@link Builder} instance + */ + public Builder addStartHereEntries() { + addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND); + if (infoItem.getStreamType() != StreamType.AUDIO_STREAM + && infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) { + addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP); + } + return this; + } + + /** + * Adds {@link StreamDialogDefaultEntry#MARK_AS_WATCHED} if the watch history is enabled + * and the stream is not a livestream. + * @return the current {@link Builder} instance + */ + public Builder addMarkAsWatchedEntryIfNeeded() { + final boolean isWatchHistoryEnabled = PreferenceManager + .getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.enable_watch_history_key), false); + if (isWatchHistoryEnabled + && infoItem.getStreamType() != StreamType.LIVE_STREAM + && infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) { + addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED); + } + return this; + } + + /** + * Adds the {@link StreamDialogDefaultEntry#PLAY_WITH_KODI} entry if it is needed. + * @return the current {@link Builder} instance + */ + public Builder addPlayWithKodiEntryIfNeeded() { + if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { + addEntry(StreamDialogDefaultEntry.PLAY_WITH_KODI); + } + return this; + } + + /** + * Add the entries which are usually at the top of the action list. + *
+ * This method adds the "enqueue" (see {@link #addEnqueueEntriesIfNeeded()}) + * and "start here" (see {@link #addStartHereEntries()} entries. + * @return the current {@link Builder} instance + */ + public Builder addDefaultBeginningEntries() { + addEnqueueEntriesIfNeeded(); + addStartHereEntries(); + return this; + } + + /** + * Add the entries which are usually at the bottom of the action list. + * @return the current {@link Builder} instance + */ + public Builder addDefaultEndEntries() { + addAllEntries( + StreamDialogDefaultEntry.APPEND_PLAYLIST, + StreamDialogDefaultEntry.SHARE, + StreamDialogDefaultEntry.OPEN_IN_BROWSER + ); + addPlayWithKodiEntryIfNeeded(); + addMarkAsWatchedEntryIfNeeded(); + addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS); + return this; + } + + /** + * Creates the {@link InfoItemDialog}. + * @return a new instance of {@link InfoItemDialog} + */ + public InfoItemDialog create() { + if (addDefaultEntriesAutomatically) { + addDefaultEndEntries(); + } + return new InfoItemDialog(this.activity, this.fragment, this.infoItem, this.entries); + } + + public static void reportErrorDuringInitialization(final Throwable throwable, + final InfoItem item) { + ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo( + throwable, + UserAction.OPEN_INFO_ITEM_DIALOG, + "none", + item.getServiceId())); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java new file mode 100644 index 00000000000..7e87318ee39 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java @@ -0,0 +1,142 @@ +package org.schabi.newpipe.info_list.dialog; + +import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment; +import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse; +import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; +import org.schabi.newpipe.local.dialog.PlaylistDialog; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.external_communication.KoreUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; + +import java.util.Collections; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; + +/** + *

+ * This enum provides entries that are accepted + * by the {@link InfoItemDialog.Builder}. + *

+ *

+ * These entries contain a String {@link #resource} which is displayed in the dialog and + * a default {@link #action} that is executed + * when the entry is selected (via onClick()). + *
+ * They action can be overridden by using the Builder's + * {@link InfoItemDialog.Builder#setAction( + * StreamDialogDefaultEntry, StreamDialogEntry.StreamDialogEntryAction)} + * method. + *

+ */ +public enum StreamDialogDefaultEntry { + SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> + fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(), + item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url)) + ), + + /** + * Enqueues the stream automatically to the current PlayerType. + */ + ENQUEUE(R.string.enqueue_stream, (fragment, item) -> + fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + NavigationHelper.enqueueOnPlayer(fragment.getContext(), singlePlayQueue)) + ), + + /** + * Enqueues the stream automatically to the current PlayerType + * after the currently playing stream. + */ + ENQUEUE_NEXT(R.string.enqueue_next_stream, (fragment, item) -> + fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), singlePlayQueue)) + ), + + START_HERE_ON_BACKGROUND(R.string.start_here_on_background, (fragment, item) -> + fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + NavigationHelper.playOnBackgroundPlayer( + fragment.getContext(), singlePlayQueue, true))), + + START_HERE_ON_POPUP(R.string.start_here_on_popup, (fragment, item) -> + fetchItemInfoIfSparse(fragment.requireContext(), item, singlePlayQueue -> + NavigationHelper.playOnPopupPlayer(fragment.getContext(), singlePlayQueue, true))), + + SET_AS_PLAYLIST_THUMBNAIL(R.string.set_as_playlist_thumbnail, (fragment, item) -> { + throw new UnsupportedOperationException("This needs to be implemented manually " + + "by using InfoItemDialog.Builder.setAction()"); + }), + + DELETE(R.string.delete, (fragment, item) -> { + throw new UnsupportedOperationException("This needs to be implemented manually " + + "by using InfoItemDialog.Builder.setAction()"); + }), + + /** + * Opens a {@link PlaylistDialog} to either append the stream to a playlist + * or create a new playlist if there are no local playlists. + */ + APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) -> + PlaylistDialog.createCorrespondingDialog( + fragment.getContext(), + Collections.singletonList(new StreamEntity(item)), + dialog -> dialog.show( + fragment.getParentFragmentManager(), + "StreamDialogEntry@" + + (dialog instanceof PlaylistAppendDialog ? "append" : "create") + + "_playlist" + ) + ) + ), + + PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> { + final Uri videoUrl = Uri.parse(item.getUrl()); + try { + NavigationHelper.playWithKore(fragment.requireContext(), videoUrl); + } catch (final Exception e) { + KoreUtils.showInstallKoreDialog(fragment.requireActivity()); + } + }), + + SHARE(R.string.share, (fragment, item) -> + ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(), + item.getThumbnailUrl())), + + OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) -> + ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())), + + + MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) -> + new HistoryRecordManager(fragment.getContext()) + .markAsWatched(item) + .onErrorComplete() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe() + ); + + + @StringRes + public final int resource; + @NonNull + public final StreamDialogEntry.StreamDialogEntryAction action; + + StreamDialogDefaultEntry(@StringRes final int resource, + @NonNull final StreamDialogEntry.StreamDialogEntryAction action) { + this.resource = resource; + this.action = action; + } + + @NonNull + public StreamDialogEntry toStreamDialogEntry() { + return new StreamDialogEntry(resource, action); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java new file mode 100644 index 00000000000..9d82e3b5829 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogEntry.java @@ -0,0 +1,31 @@ +package org.schabi.newpipe.info_list.dialog; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import org.schabi.newpipe.extractor.stream.StreamInfoItem; + +public class StreamDialogEntry { + + @StringRes + public final int resource; + @NonNull + public final StreamDialogEntryAction action; + + public StreamDialogEntry(@StringRes final int resource, + @NonNull final StreamDialogEntryAction action) { + this.resource = resource; + this.action = action; + } + + public String getString(@NonNull final Context context) { + return context.getString(resource); + } + + public interface StreamDialogEntryAction { + void onClick(Fragment fragment, StreamInfoItem infoItem); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java index 78acb752b54..aa4f4c9f023 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.info_list.holder; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipe.R; @@ -11,10 +12,8 @@ import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.Localization; -import de.hdodenhof.circleimageview.CircleImageView; - public class ChannelMiniInfoItemHolder extends InfoItemHolder { - public final CircleImageView itemThumbnailView; + public final ImageView itemThumbnailView; public final TextView itemTitleView; private final TextView itemAdditionalDetailView; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index cb47efa9252..6e4773c09d4 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -7,6 +7,7 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; @@ -28,8 +29,6 @@ import java.util.regex.Matcher; -import de.hdodenhof.circleimageview.CircleImageView; - public class CommentsMiniInfoItemHolder extends InfoItemHolder { private static final String TAG = "CommentsMiniIIHolder"; @@ -40,7 +39,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { private final int commentVerticalPadding; private final RelativeLayout itemRoot; - public final CircleImageView itemThumbnailView; + public final ImageView itemThumbnailView; private final TextView itemContentView; private final TextView itemLikesCountView; private final TextView itemPublishedTime; diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 5d81c0069c0..05e2fdac083 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -228,6 +228,7 @@ public int getItemCount() { return count; } + @SuppressWarnings("FinalParameters") @Override public int getItemViewType(int position) { if (DEBUG) { @@ -300,6 +301,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup paren } } + @SuppressWarnings("FinalParameters") @Override public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) { if (DEBUG) { diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java index c2d4474f897..d43a0981c11 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java @@ -97,7 +97,7 @@ public void readFrom(@NonNull final Queue savedObjects) { } @Override - public void onSaveInstanceState(final Bundle outState) { + public void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); if (getActivity() != null) { savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(), diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index e6da0d545fc..e97629f31a9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -50,7 +50,6 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.Item -import com.xwray.groupie.OnAsyncUpdateListener import com.xwray.groupie.OnItemClickListener import com.xwray.groupie.OnItemLongClickListener import icepick.State @@ -68,25 +67,21 @@ import org.schabi.newpipe.error.UserAction import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.stream.StreamInfoItem -import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty import org.schabi.newpipe.fragments.BaseStateFragment -import org.schabi.newpipe.info_list.InfoItemDialog +import org.schabi.newpipe.info_list.dialog.InfoItemDialog import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling import org.schabi.newpipe.ktx.slideUp import org.schabi.newpipe.local.feed.item.StreamItem import org.schabi.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.local.subscription.SubscriptionManager -import org.schabi.newpipe.player.helper.PlayerHolder import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.NavigationHelper -import org.schabi.newpipe.util.StreamDialogEntry import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout import java.time.OffsetDateTime -import java.util.ArrayList import java.util.function.Consumer class FeedFragment : BaseStateFragment() { @@ -143,7 +138,7 @@ class FeedFragment : BaseStateFragment() { val factory = FeedViewModel.Factory(requireContext(), groupId) viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java) showPlayedItems = viewModel.getShowPlayedItemsFromPreferences() - viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) }) + viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) } groupAdapter = GroupieAdapter().apply { setOnItemClickListener(listenerStreamItem) @@ -356,53 +351,12 @@ class FeedFragment : BaseStateFragment() { feedBinding.loadingProgressBar.max = progressState.maxProgress } - private fun showStreamDialog(item: StreamInfoItem) { + private fun showInfoItemDialog(item: StreamInfoItem) { val context = context val activity: Activity? = getActivity() if (context == null || context.resources == null || activity == null) return - val entries = ArrayList() - if (PlayerHolder.getInstance().isPlayQueueReady) { - entries.add(StreamDialogEntry.enqueue) - - if (PlayerHolder.getInstance().queueSize > 1) { - entries.add(StreamDialogEntry.enqueue_next) - } - } - - if (item.streamType == StreamType.AUDIO_STREAM) { - entries.addAll( - listOf( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share, - StreamDialogEntry.open_in_browser - ) - ) - } else { - entries.addAll( - listOf( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share, - StreamDialogEntry.open_in_browser - ) - ) - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) { - entries.add( - StreamDialogEntry.mark_as_watched - ) - } - entries.add(StreamDialogEntry.show_channel_details) - - StreamDialogEntry.setEnabledEntries(entries) - InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which -> - StreamDialogEntry.clickOn(which, this, item) - }.show() + InfoItemDialog.Builder(activity, context, this, item).create().show() } private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener { @@ -418,7 +372,7 @@ class FeedFragment : BaseStateFragment() { override fun onItemLongClick(item: Item<*>, view: View): Boolean { if (item is StreamItem && !isRefreshing) { - showStreamDialog(item.streamWithState.stream.toStreamInfoItem()) + showInfoItemDialog(item.streamWithState.stream.toStreamInfoItem()) return true } return false @@ -438,14 +392,11 @@ class FeedFragment : BaseStateFragment() { // This need to be saved in a variable as the update occurs async val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate - groupAdapter.updateAsync( - loadedState.items, false, - OnAsyncUpdateListener { - oldOldestSubscriptionUpdate?.run { - highlightNewItemsAfter(oldOldestSubscriptionUpdate) - } + groupAdapter.updateAsync(loadedState.items, false) { + oldOldestSubscriptionUpdate?.run { + highlightNewItemsAfter(oldOldestSubscriptionUpdate) } - ) + } listState?.run { feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState) @@ -497,8 +448,7 @@ class FeedFragment : BaseStateFragment() { }.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { - subscriptionEntity -> + { subscriptionEntity -> handleFeedNotAvailable( subscriptionEntity, t.cause, diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index 2cbf9ad05b0..e21963c1651 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -56,7 +56,7 @@ class FeedViewModel( .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .map { (event, showPlayedItems, notLoadedCount, oldestUpdate) -> - var streamItems = if (event is SuccessResultEvent || event is IdleEvent) + val streamItems = if (event is SuccessResultEvent || event is IdleEvent) feedDatabaseManager .getStreams(groupId, showPlayedItems) .blockingGet(arrayListOf()) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt index 61e5c7d9ed8..6b9580802ec 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt @@ -3,7 +3,6 @@ package org.schabi.newpipe.local.feed.notifications import android.content.Context import android.util.Log import androidx.core.app.NotificationCompat -import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ForegroundInfo diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java index e7ccd07d29e..709a16b68b6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java @@ -84,7 +84,7 @@ public void onBindViewHolder(final VH holder, final int position) { } @Override - public void onViewRecycled(final VH holder) { + public void onViewRecycled(@NonNull final VH holder) { super.onViewRecycled(holder); holder.itemView.setOnClickListener(null); } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 73682d5d524..01df342920b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.local.history; -import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.Parcelable; @@ -29,20 +28,16 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.local.BaseLocalListFragment; -import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.settings.HistorySettingsFragment; -import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.util.StreamDialogEntry; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -154,7 +149,7 @@ public void selected(final LocalItem selectedItem) { @Override public void held(final LocalItem selectedItem) { if (selectedItem instanceof StreamStatisticsEntry) { - showStreamDialog((StreamStatisticsEntry) selectedItem); + showInfoItemDialog((StreamStatisticsEntry) selectedItem); } } }); @@ -328,66 +323,30 @@ private PlayQueue getPlayQueueStartingAt(final StreamStatisticsEntry infoItem) { return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0)); } - private void showStreamDialog(final StreamStatisticsEntry item) { + private void showInfoItemDialog(final StreamStatisticsEntry item) { final Context context = getContext(); - final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) { - return; - } final StreamInfoItem infoItem = item.toStreamInfoItem(); - final ArrayList entries = new ArrayList<>(); - - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } - } - - if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.delete, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.delete, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); + try { + final InfoItemDialog.Builder dialogBuilder = + new InfoItemDialog.Builder(getActivity(), context, this, infoItem); + + // set entries in the middle; the others are added automatically + dialogBuilder + .addEntry(StreamDialogDefaultEntry.DELETE) + .setAction( + StreamDialogDefaultEntry.DELETE, + (f, i) -> deleteEntry( + Math.max(itemListAdapter.getItemsList().indexOf(item), 0))) + .setAction( + StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND, + (f, i) -> NavigationHelper.playOnBackgroundPlayer( + context, getPlayQueueStartingAt(item), true)) + .create() + .show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem); } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched( - item.getStreamEntity().getStreamType(), - context - )) { - entries.add( - StreamDialogEntry.mark_as_watched - ); - } - entries.add(StreamDialogEntry.show_channel_details); - - StreamDialogEntry.setEnabledEntries(entries); - - StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> - NavigationHelper - .playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true)); - StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) -> - deleteEntry(Math.max(itemListAdapter.getItemsList().indexOf(item), 0))); - - new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show(); } private void deleteEntry(final int index) { diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index feb5b2f9672..0eb56d7169c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.local.playlist; -import android.app.Activity; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; + import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; @@ -38,22 +40,18 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.MainPlayer.PlayerType; -import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.util.StreamDialogEntry; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -68,9 +66,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.PublishSubject; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; - public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { // Save the list 10 seconds after the last change occurred private static final long SAVE_DEBOUNCE_MILLIS = 10000; @@ -182,7 +177,7 @@ public void selected(final LocalItem selectedItem) { @Override public void held(final LocalItem selectedItem) { if (selectedItem instanceof PlaylistStreamEntry) { - showStreamItemDialog((PlaylistStreamEntry) selectedItem); + showInfoItemDialog((PlaylistStreamEntry) selectedItem); } } @@ -355,7 +350,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { new AlertDialog.Builder(requireContext()) .setMessage(R.string.remove_watched_popup_warning) .setTitle(R.string.remove_watched_popup_title) - .setPositiveButton(R.string.yes, + .setPositiveButton(R.string.ok, (DialogInterface d, int id) -> removeWatchedStreams(false)) .setNeutralButton( R.string.remove_watched_popup_yes_and_partially_watched_videos, @@ -743,70 +738,39 @@ private PlayQueue getPlayQueueStartingAt(final PlaylistStreamEntry infoItem) { return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0)); } - protected void showStreamItemDialog(final PlaylistStreamEntry item) { - final Context context = getContext(); - final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) { - return; - } + protected void showInfoItemDialog(final PlaylistStreamEntry item) { final StreamInfoItem infoItem = item.toStreamInfoItem(); - final ArrayList entries = new ArrayList<>(); + try { + final Context context = getContext(); + final InfoItemDialog.Builder dialogBuilder = + new InfoItemDialog.Builder(getActivity(), context, this, infoItem); - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } - } - if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.set_as_playlist_thumbnail, - StreamDialogEntry.delete, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.set_as_playlist_thumbnail, - StreamDialogEntry.delete, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched( - item.getStreamEntity().getStreamType(), - context - )) { - entries.add( - StreamDialogEntry.mark_as_watched + // add entries in the middle + dialogBuilder.addAllEntries( + StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL, + StreamDialogDefaultEntry.DELETE ); - } - entries.add(StreamDialogEntry.show_channel_details); - - StreamDialogEntry.setEnabledEntries(entries); - StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> - NavigationHelper.playOnBackgroundPlayer(context, - getPlayQueueStartingAt(item), true)); - StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction( - (fragment, infoItemDuplicate) -> - changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl())); - StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) -> - deleteItem(item)); - - new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, infoItem)).show(); + // set custom actions + // all entries modified below have already been added within the builder + dialogBuilder + .setAction( + StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND, + (f, i) -> NavigationHelper.playOnBackgroundPlayer( + context, getPlayQueueStartingAt(item), true)) + .setAction( + StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL, + (f, i) -> + changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl())) + .setAction( + StreamDialogDefaultEntry.DELETE, + (f, i) -> deleteItem(item)) + .create() + .show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem); + } } private void setInitialData(final long pid, final String title) { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java index a3d8b0567a3..da8e1070a99 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java @@ -61,7 +61,7 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { } @Override - public void onSaveInstanceState(final Bundle outState) { + public void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt index d49df630363..54ba1c6dc53 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.functions.BiFunction import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.schedulers.Schedulers import org.schabi.newpipe.database.feed.model.FeedGroupEntity @@ -33,9 +32,8 @@ class FeedGroupDialogViewModel( private var subscriptionsFlowable = Flowable .combineLatest( filterSubscriptions.startWithItem(initialQuery), - toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped), - BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) } - ) + toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped) + ) { t1: String, t2: Boolean -> Filter(t1, t2) } .distinctUntilChanged() .switchMap { (query, showOnlyUngrouped) -> subscriptionManager.getSubscriptions(groupId, query, showOnlyUngrouped) @@ -56,9 +54,8 @@ class FeedGroupDialogViewModel( private var subscriptionsDisposable = Flowable .combineLatest( - subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId), - BiFunction { t1: List, t2: List -> t1 to t2.toSet() } - ) + subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId) + ) { t1: List, t2: List -> t1 to t2.toSet() } .subscribeOn(Schedulers.io()) .subscribe(mutableSubscriptionsLiveData::postValue) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt index 50e8aae6a80..1f3ab71eb62 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt @@ -57,15 +57,12 @@ class FeedGroupReorderDialog : DialogFragment() { viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java) viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups)) - viewModel.dialogEventLiveData.observe( - viewLifecycleOwner, - Observer { - when (it) { - ProcessingEvent -> disableInput() - SuccessEvent -> dismiss() - } + viewModel.dialogEventLiveData.observe(viewLifecycleOwner) { + when (it) { + ProcessingEvent -> disableInput() + SuccessEvent -> dismiss() } - ) + } binding.feedGroupsList.layoutManager = LinearLayoutManager(requireContext()) binding.feedGroupsList.adapter = groupAdapter diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java index 8e3aad89364..611a1cd30bc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java @@ -25,7 +25,6 @@ import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; -import com.grack.nanojson.JsonSink; import com.grack.nanojson.JsonWriter; import org.schabi.newpipe.BuildConfig; @@ -125,10 +124,11 @@ public static void writeTo(final List items, final OutputStrea /** * @see #writeTo(List, OutputStream, ImportExportEventListener) * @param items the list of subscriptions items - * @param writer the output {@link JsonSink} + * @param writer the output {@link JsonAppendableWriter} * @param eventListener listener for the events generated */ - public static void writeTo(final List items, final JsonSink writer, + public static void writeTo(final List items, + final JsonAppendableWriter writer, @Nullable final ImportExportEventListener eventListener) { if (eventListener != null) { eventListener.onSizeReceived(items.size()); diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index e0c5ab08366..53e6ce59101 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -1,7 +1,10 @@ package org.schabi.newpipe.player; +import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; +import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + import android.content.ComponentName; -import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; @@ -23,11 +26,9 @@ import com.google.android.exoplayer2.PlaybackParameters; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -42,13 +43,6 @@ import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ThemeHelper; -import java.util.List; -import java.util.stream.Collectors; - -import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; -import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - public final class PlayQueueActivity extends AppCompatActivity implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, PlaybackParameterDialog.Callback { @@ -129,7 +123,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { NavigationHelper.openSettings(this); return true; case R.id.action_append_playlist: - appendAllToPlaylist(); + player.onAddToPlaylistClicked(getSupportFragmentManager()); return true; case R.id.action_playback_speed: openPlaybackParameterDialog(); @@ -443,24 +437,6 @@ public void onStopTrackingTouch(final SeekBar seekBar) { seeking = false; } - //////////////////////////////////////////////////////////////////////////// - // Playlist append - //////////////////////////////////////////////////////////////////////////// - - private void appendAllToPlaylist() { - if (player != null && player.getPlayQueue() != null) { - openPlaylistAppendDialog(player.getPlayQueue().getStreams()); - } - } - - private void openPlaylistAppendDialog(final List playQueueItems) { - PlaylistDialog.createCorrespondingDialog( - getApplicationContext(), - playQueueItems.stream().map(StreamEntity::new).collect(Collectors.toList()), - dialog -> dialog.show(getSupportFragmentManager(), TAG) - ); - } - //////////////////////////////////////////////////////////////////////////// // Binding Service Listener //////////////////////////////////////////////////////////////////////////// @@ -624,7 +600,6 @@ private void onMaybeMuteChanged() { //2) Icon change accordingly to current App Theme // using rootView.getContext() because getApplicationContext() didn't work - final Context context = queueControlBinding.getRoot().getContext(); item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index e0debc4e7b1..1051f678f3f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -73,7 +73,6 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -89,7 +88,6 @@ import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.SeekBar; @@ -99,12 +97,15 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.view.ContextThemeWrapper; import androidx.appcompat.widget.AppCompatImageButton; +import androidx.appcompat.widget.PopupMenu; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.view.GestureDetectorCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; @@ -112,6 +113,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player.PositionInfo; import com.google.android.exoplayer2.RenderersFactory; @@ -122,6 +124,7 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.CaptionStyleCompat; @@ -136,6 +139,7 @@ import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.databinding.PlayerBinding; import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; import org.schabi.newpipe.error.ErrorInfo; @@ -144,11 +148,13 @@ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamSegment; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.info_list.StreamSegmentAdapter; import org.schabi.newpipe.ktx.AnimationType; +import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.event.DisplayPortion; @@ -158,9 +164,10 @@ import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.MediaSessionManager; -import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener; +import org.schabi.newpipe.player.listeners.view.QualityClickListener; import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.PlaybackListener; @@ -175,6 +182,7 @@ import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder; import org.schabi.newpipe.util.DeviceUtils; @@ -193,6 +201,8 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; @@ -521,9 +531,12 @@ private void initPlayer(final boolean playOnReady) { } private void initListeners() { + binding.qualityTextView.setOnClickListener( + new QualityClickListener(this, qualityPopupMenu)); + binding.playbackSpeed.setOnClickListener( + new PlaybackSpeedClickListener(this, playbackSpeedPopupMenu)); + binding.playbackSeekBar.setOnSeekBarChangeListener(this); - binding.playbackSpeed.setOnClickListener(this); - binding.qualityTextView.setOnClickListener(this); binding.captionTextView.setOnClickListener(this); binding.resizeTextView.setOnClickListener(this); binding.playbackLiveSync.setOnClickListener(this); @@ -532,10 +545,15 @@ private void initListeners() { gestureDetector = new GestureDetectorCompat(context, playerGestureListener); binding.getRoot().setOnTouchListener(playerGestureListener); - binding.queueButton.setOnClickListener(this); - binding.segmentsButton.setOnClickListener(this); - binding.repeatButton.setOnClickListener(this); - binding.shuffleButton.setOnClickListener(this); + binding.queueButton.setOnClickListener(v -> onQueueClicked()); + binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked()); + binding.repeatButton.setOnClickListener(v -> onRepeatClicked()); + binding.shuffleButton.setOnClickListener(v -> onShuffleClicked()); + binding.addToPlaylistButton.setOnClickListener(v -> { + if (getParentActivity() != null) { + onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager()); + } + }); binding.playPauseButton.setOnClickListener(this); binding.playPreviousButton.setOnClickListener(this); @@ -580,11 +598,17 @@ public void onChange(final boolean selfChange) { v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom()); - binding.fastSeekOverlay.setPadding( - v.getPaddingLeft(), - v.getPaddingTop(), - v.getPaddingRight(), - v.getPaddingBottom()); + + // If we added padding to the fast seek overlay, too, it would not go under the + // system ui. Instead we apply negative margins equal to the window insets of + // the opposite side, so that the view covers all of the player (overflowing on + // some sides) and its center coincides with the center of other controls. + final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams) + binding.fastSeekOverlay.getLayoutParams(); + fastSeekParams.leftMargin = -v.getPaddingRight(); + fastSeekParams.topMargin = -v.getPaddingBottom(); + fastSeekParams.rightMargin = -v.getPaddingLeft(); + fastSeekParams.bottomMargin = -v.getPaddingTop(); }); } @@ -593,8 +617,7 @@ public void onChange(final boolean selfChange) { */ private void setupPlayerSeekOverlay() { binding.fastSeekOverlay - .seekSecondsSupplier( - () -> (int) (retrieveSeekDurationFromPreferences(this) / 1000.0f)) + .seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(this) / 1000) .performListener(new PlayerFastSeekOverlay.PerformListener() { @Override @@ -607,6 +630,7 @@ public void onDoubleTapEnd() { animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION); } + @NonNull @Override public FastSeekDirection getFastSeekDirection( @NonNull final DisplayPortion portion @@ -658,6 +682,7 @@ public void seek(final boolean forward) { //////////////////////////////////////////////////////////////////////////*/ //region Playback initialization via intent + @SuppressWarnings("MethodLength") public void handleIntent(@NonNull final Intent intent) { // fail fast if no play queue was provided final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY); @@ -1910,7 +1935,7 @@ public void hideControls(final long duration, final long delay) { }, delay); } - private void showHideShadow(final boolean show, final long duration) { + public void showHideShadow(final boolean show, final long duration) { animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null); animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null); animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null); @@ -2378,6 +2403,32 @@ private void setShuffleButton(@NonNull final ImageButton button, final boolean s + /*////////////////////////////////////////////////////////////////////////// + // Playlist append + //////////////////////////////////////////////////////////////////////////*/ + //region Playlist append + + public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManager) { + if (DEBUG) { + Log.d(TAG, "onAddToPlaylistClicked() called"); + } + + if (getPlayQueue() != null) { + PlaylistDialog.createCorrespondingDialog( + getContext(), + getPlayQueue() + .getStreams() + .stream() + .map(StreamEntity::new) + .collect(Collectors.toList()), + dialog -> dialog.show(fragmentManager, TAG) + ); + } + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// // Mute / Unmute //////////////////////////////////////////////////////////////////////////*/ @@ -2443,9 +2494,9 @@ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playba } @Override - public void onPositionDiscontinuity( - final PositionInfo oldPosition, final PositionInfo newPosition, - @DiscontinuityReason final int discontinuityReason) { + public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition, + @NonNull final PositionInfo newPosition, + @DiscontinuityReason final int discontinuityReason) { if (DEBUG) { Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + "discontinuityReason = [" + discontinuityReason + "]"); @@ -2493,7 +2544,7 @@ public void onRenderedFirstFrame() { } @Override - public void onCues(final List cues) { + public void onCues(@NonNull final List cues) { binding.subtitleView.onCues(cues); } //endregion @@ -2999,18 +3050,19 @@ private void maybeUpdateCurrentMetadata() { final MediaSourceTag metadata; try { - metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); - } catch (IndexOutOfBoundsException | ClassCastException error) { + final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem(); + if (currentMediaItem == null || currentMediaItem.playbackProperties == null + || currentMediaItem.playbackProperties.tag == null) { + return; + } + metadata = (MediaSourceTag) currentMediaItem.playbackProperties.tag; + } catch (final IndexOutOfBoundsException | ClassCastException ex) { if (DEBUG) { - Log.d(TAG, "Could not update metadata: " + error.getMessage()); - error.printStackTrace(); + Log.d(TAG, "Could not update metadata", ex); } return; } - if (metadata == null) { - return; - } maybeAutoQueueNextStream(metadata); if (currentMetadata == metadata) { @@ -3119,6 +3171,7 @@ private void onQueueClicked() { binding.itemsListHeaderDuration.setVisibility(View.VISIBLE); binding.shuffleButton.setVisibility(View.VISIBLE); binding.repeatButton.setVisibility(View.VISIBLE); + binding.addToPlaylistButton.setVisibility(View.VISIBLE); hideControls(0, 0); binding.itemsListPanel.requestFocus(); @@ -3156,6 +3209,7 @@ private void onSegmentsClicked() { binding.itemsListHeaderDuration.setVisibility(View.GONE); binding.shuffleButton.setVisibility(View.GONE); binding.repeatButton.setVisibility(View.GONE); + binding.addToPlaylistButton.setVisibility(View.GONE); hideControls(0, 0); binding.itemsListPanel.requestFocus(); @@ -3184,6 +3238,7 @@ private void buildSegments() { binding.shuffleButton.setVisibility(View.GONE); binding.repeatButton.setVisibility(View.GONE); + binding.addToPlaylistButton.setVisibility(View.GONE); binding.itemsListClose.setOnClickListener(view -> closeItemsList()); } @@ -3203,6 +3258,9 @@ public void closeItemsList() { binding.itemsListPanel.setTranslationY( -binding.itemsListPanel.getHeight() * 5); }); + + // clear focus, otherwise a white rectangle remains on top of the player + binding.itemsListClose.clearFocus(); binding.playPauseButton.requestFocus(); } } @@ -3286,7 +3344,27 @@ public void onStartDrag(final PlayQueueItemHolder viewHolder) { @Override // own playback listener @Nullable public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - return (isAudioOnly ? audioResolver : videoResolver).resolve(info); + if (audioPlayerSelected()) { + return audioResolver.resolve(info); + } + + if (isAudioOnly && videoResolver.getStreamSourceType().orElse( + SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY) + == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY) { + // If the current info has only video streams with audio and if the stream is played as + // audio, we need to use the audio resolver, otherwise the video stream will be played + // in background. + return audioResolver.resolve(info); + } + + // Even if the stream is played in background, we need to use the video resolver if the + // info played is separated video-only and audio-only streams; otherwise, if the audio + // resolver was called when the app was in background, the app will only stream audio when + // the user come back to the app and will never fetch the video stream. + // Note that the video is not fetched when the app is in background because the video + // renderer is fully disabled (see useVideoSource method), except for HLS streams + // (see https://github.com/google/ExoPlayer/issues/9282). + return videoResolver.resolve(info); } public void disablePreloadingOfCurrentTrack() { @@ -3538,37 +3616,6 @@ public void onDismiss(@Nullable final PopupMenu menu) { } } - private void onQualitySelectorClicked() { - if (DEBUG) { - Log.d(TAG, "onQualitySelectorClicked() called"); - } - qualityPopupMenu.show(); - isSomePopupMenuVisible = true; - - final VideoStream videoStream = getSelectedVideoStream(); - if (videoStream != null) { - final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " " - + videoStream.resolution; - binding.qualityTextView.setText(qualityText); - } - - saveWasPlaying(); - } - - private void onPlaybackSpeedClicked() { - if (DEBUG) { - Log.d(TAG, "onPlaybackSpeedClicked() called"); - } - if (videoPlayerSelected()) { - PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch(), - getPlaybackSkipSilence(), this::setPlaybackParameters) - .show(getParentActivity().getSupportFragmentManager(), null); - } else { - playbackSpeedPopupMenu.show(); - isSomePopupMenuVisible = true; - } - } - private void onCaptionClicked() { if (DEBUG) { Log.d(TAG, "onCaptionClicked() called"); @@ -3673,11 +3720,7 @@ public void onClick(final View v) { if (DEBUG) { Log.d(TAG, "onClick() called with: v = [" + v + "]"); } - if (v.getId() == binding.qualityTextView.getId()) { - onQualitySelectorClicked(); - } else if (v.getId() == binding.playbackSpeed.getId()) { - onPlaybackSpeedClicked(); - } else if (v.getId() == binding.resizeTextView.getId()) { + if (v.getId() == binding.resizeTextView.getId()) { onResizeClicked(); } else if (v.getId() == binding.captionTextView.getId()) { onCaptionClicked(); @@ -3689,18 +3732,6 @@ public void onClick(final View v) { playPrevious(); } else if (v.getId() == binding.playNextButton.getId()) { playNext(); - } else if (v.getId() == binding.queueButton.getId()) { - onQueueClicked(); - return; - } else if (v.getId() == binding.segmentsButton.getId()) { - onSegmentsClicked(); - return; - } else if (v.getId() == binding.repeatButton.getId()) { - onRepeatClicked(); - return; - } else if (v.getId() == binding.shuffleButton.getId()) { - onShuffleClicked(); - return; } else if (v.getId() == binding.moreOptionsButton.getId()) { onMoreOptionsClicked(); } else if (v.getId() == binding.share.getId()) { @@ -3729,23 +3760,33 @@ public void onClick(final View v) { context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); } - if (currentState != STATE_COMPLETED) { - controlsVisibilityHandler.removeCallbacksAndMessages(null); - showHideShadow(true, DEFAULT_CONTROLS_DURATION); - animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, - AnimationType.ALPHA, 0, () -> { - if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) { - if (v.getId() == binding.playPauseButton.getId() - // Hide controls in fullscreen immediately - || (v.getId() == binding.screenRotationButton.getId() - && isFullscreen)) { - hideControls(0, 0); - } else { - hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - } - }); + manageControlsAfterOnClick(v); + } + + /** + * Manages the controls after a click occurred on the player UI. + * @param v – The view that was clicked + */ + public void manageControlsAfterOnClick(@NonNull final View v) { + if (currentState == STATE_COMPLETED) { + return; } + + controlsVisibilityHandler.removeCallbacksAndMessages(null); + showHideShadow(true, DEFAULT_CONTROLS_DURATION); + animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, + AnimationType.ALPHA, 0, () -> { + if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) { + if (v.getId() == binding.playPauseButton.getId() + // Hide controls in fullscreen immediately + || (v.getId() == binding.screenRotationButton.getId() + && isFullscreen)) { + hideControls(0, 0); + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + }); } @Override @@ -3767,6 +3808,10 @@ public boolean onKeyDown(final int keyCode) { case KeyEvent.KEYCODE_SPACE: if (isFullscreen) { playPause(); + if (isPlaying()) { + hideControls(0, 0); + } + return true; } break; case KeyEvent.KEYCODE_BACK: @@ -3780,8 +3825,9 @@ public boolean onKeyDown(final int keyCode) { case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_CENTER: - if (binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) { - // do not interfere with focus in playlist etc. + if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) + || isQueueVisible) { + // do not interfere with focus in playlist and play queue etc. return false; } @@ -3789,15 +3835,13 @@ public boolean onKeyDown(final int keyCode) { return true; } - if (!isControlsVisible()) { - if (!isQueueVisible) { - binding.playPauseButton.requestFocus(); - } + if (isControlsVisible()) { + hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } else { + binding.playPauseButton.requestFocus(); showControlsThenHide(); showSystemUIPartially(); return true; - } else { - hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); } break; } @@ -4141,19 +4185,125 @@ public AppCompatActivity getParentActivity() { return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); } - private void useVideoSource(final boolean video) { - if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) { + private void useVideoSource(final boolean videoEnabled) { + if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) { return; } - isAudioOnly = !video; - // When a user returns from background controls could be hidden - // but systemUI will be shown 100%. Hide it + isAudioOnly = !videoEnabled; + // When a user returns from background, controls could be hidden but SystemUI will be shown + // 100%. Hide it. if (!isAudioOnly && !isControlsVisible()) { hideSystemUIIfNeeded(); } + + // The current metadata may be null sometimes (for e.g. when using an unstable connection + // in livestreams) so we will be not able to execute the block below. + // Reload the play queue manager in this case, which is the behavior when we don't know the + // index of the video renderer or playQueueManagerReloadingNeeded returns true. + if (currentMetadata == null) { + reloadPlayQueueManager(); + setRecovery(); + return; + } + + final int videoRenderIndex = getVideoRendererIndex(); + final StreamInfo info = currentMetadata.getMetadata(); + + // In the case we don't know the source type, fallback to the one with video with audio or + // audio-only source. + final SourceType sourceType = videoResolver.getStreamSourceType().orElse( + SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY); + + if (playQueueManagerReloadingNeeded(sourceType, info, videoRenderIndex)) { + reloadPlayQueueManager(); + } else { + final StreamType streamType = info.getStreamType(); + if (streamType == StreamType.AUDIO_STREAM + || streamType == StreamType.AUDIO_LIVE_STREAM) { + // Nothing to do more than setting the recovery position + setRecovery(); + return; + } + + final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull( + trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex); + if (videoEnabled) { + // Clearing the null selection override enable again the video stream (and its + // fetching). + trackSelector.setParameters(trackSelector.buildUponParameters() + .clearSelectionOverride(videoRenderIndex, videoTrackGroupArray)); + } else { + // Using setRendererDisabled still fetch the video stream in background, contrary + // to setSelectionOverride with a null override. + trackSelector.setParameters(trackSelector.buildUponParameters() + .setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null)); + } + } + setRecovery(); - reloadPlayQueueManager(); + } + + /** + * Return whether the play queue manager needs to be reloaded when switching player type. + * + *

+ * The play queue manager needs to be reloaded if the video renderer index is not known and if + * the content is not an audio content, but also if none of the following cases is met: + * + *

    + *
  • the content is an {@link StreamType#AUDIO_STREAM audio stream} or an + * {@link StreamType#AUDIO_LIVE_STREAM audio live stream};
  • + *
  • the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a + * {@link SourceType#LIVE_STREAM live source};
  • + *
  • the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream + * with a separated audio source} or has no audio-only streams available and is a + * {@link StreamType#LIVE_STREAM live stream} or a + * {@link StreamType#LIVE_STREAM live stream}. + *
  • + *
+ *

+ * + * @param sourceType the {@link SourceType} of the stream + * @param streamInfo the {@link StreamInfo} of the stream + * @param videoRendererIndex the video renderer index of the video source, if that's a video + * source (or {@link #RENDERER_UNAVAILABLE}) + * @return whether the play queue manager needs to be reloaded + */ + private boolean playQueueManagerReloadingNeeded(final SourceType sourceType, + @NonNull final StreamInfo streamInfo, + final int videoRendererIndex) { + final StreamType streamType = streamInfo.getStreamType(); + + if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM + && streamType != StreamType.AUDIO_LIVE_STREAM) { + return true; + } + + // The content is an audio stream, an audio live stream, or a live stream with a live + // source: it's not needed to reload the play queue manager because the stream source will + // be the same + if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM) + || (streamType == StreamType.LIVE_STREAM + && sourceType == SourceType.LIVE_STREAM)) { + return false; + } + + // The content's source is a video with separated audio or a video with audio -> the video + // and its fetch may be disabled + // The content's source is a video with embedded audio and the content has no separated + // audio stream available: it's probably not needed to reload the play queue manager + // because the stream source will be probably the same as the current played + if (sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO + || (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY + && isNullOrEmpty(streamInfo.getAudioStreams()))) { + // It's not needed to reload the play queue manager only if the content's stream type + // is a video stream or a live stream + return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM; + } + + // Other cases: the play queue manager reload is needed + return true; } //endregion @@ -4191,7 +4341,7 @@ private boolean isLoading() { private boolean isLive() { try { return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic(); - } catch (@NonNull final IndexOutOfBoundsException e) { + } catch (final IndexOutOfBoundsException e) { // Why would this even happen =(... but lets log it anyway, better safe than sorry if (DEBUG) { Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e); @@ -4263,6 +4413,10 @@ public boolean isSomePopupMenuVisible() { return isSomePopupMenuVisible; } + public void setSomePopupMenuVisible(final boolean somePopupMenuVisible) { + isSomePopupMenuVisible = somePopupMenuVisible; + } + public ImageButton getPlayPauseButton() { return binding.playPauseButton; } @@ -4344,6 +4498,11 @@ public ExpandableSurfaceView getSurfaceView() { public PlayQueueAdapter getPlayQueueAdapter() { return playQueueAdapter; } + + public PlayerBinding getBinding() { + return binding; + } + //endregion @@ -4369,15 +4528,42 @@ private void setupVideoSurface() { } private void cleanupVideoSurface() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 - if (surfaceHolderCallback != null) { - if (binding != null) { - binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); - } - surfaceHolderCallback.release(); - surfaceHolderCallback = null; + // Only for API >= 23 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) { + if (binding != null) { + binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); } + surfaceHolderCallback.release(); + surfaceHolderCallback = null; } } //endregion + + /** + * Get the video renderer index of the current playing stream. + * + * This method returns the video renderer index of the current + * {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current + * {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index. + * + * @return the video renderer index or {@link #RENDERER_UNAVAILABLE} if it cannot be get + */ + private int getVideoRendererIndex() { + final MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector + .getCurrentMappedTrackInfo(); + + if (mappedTrackInfo == null) { + return RENDERER_UNAVAILABLE; + } + + // Check every renderer + return IntStream.range(0, mappedTrackInfo.getRendererCount()) + // Check the renderer is a video renderer and has at least one track + .filter(i -> !mappedTrackInfo.getTrackGroups(i).isEmpty() + && simpleExoPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO) + // Return the first index found (there is at most one renderer per renderer type) + .findFirst() + // No video renderer index with at least one track found: return unavailable index + .orElse(RENDERER_UNAVAILABLE); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index b36f9f23488..087a3bc76b6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -149,7 +149,8 @@ public void onAnimationEnd(final Animator animation) { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAudioSessionIdChanged(final EventTime eventTime, final int audioSessionId) { + public void onAudioSessionIdChanged(@NonNull final EventTime eventTime, + final int audioSessionId) { notifyAudioSessionUpdate(true, audioSessionId); } private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index 9703a35884b..bcab92787a7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -63,6 +63,7 @@ private CacheFactory(@NonNull final Context context, } } + @NonNull @Override public DataSource createDataSource() { Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath()); @@ -86,8 +87,8 @@ public void tryDeleteCacheFiles() { Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful); } - } catch (final Exception ignored) { - Log.e(TAG, "Failed to delete file.", ignored); + } catch (final Exception e) { + Log.e(TAG, "Failed to delete file.", e); } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index 8d344c87788..cd04bc2ebcb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -135,9 +135,7 @@ public void setMetadata(@NonNull final String title, lastTitleHashCode = title.hashCode(); lastArtistHashCode = artist.hashCode(); lastDuration = duration; - if (optAlbumArt.isPresent()) { - lastAlbumArtHashCode = optAlbumArt.get().hashCode(); - } + optAlbumArt.ifPresent(bitmap -> lastAlbumArtHashCode = bitmap.hashCode()); } private boolean checkIfMetadataShouldBeSet( diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 5139ef9cd60..1a55c21c342 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -9,6 +9,7 @@ import android.util.Log; import android.view.View; import android.widget.CheckBox; +import android.widget.RelativeLayout; import android.widget.SeekBar; import android.widget.TextView; @@ -19,6 +20,7 @@ import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.SliderStrategy; public class PlaybackParameterDialog extends DialogFragment { @@ -37,6 +39,7 @@ public class PlaybackParameterDialog extends DialogFragment { private static final double DEFAULT_TEMPO = 1.00f; private static final double DEFAULT_PITCH = 1.00f; + private static final int DEFAULT_SEMITONES = 0; private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE; private static final boolean DEFAULT_SKIP_SILENCE = false; @@ -64,10 +67,11 @@ public class PlaybackParameterDialog extends DialogFragment { private double initialTempo = DEFAULT_TEMPO; private double initialPitch = DEFAULT_PITCH; + private int initialSemitones = DEFAULT_SEMITONES; private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE; private double tempo = DEFAULT_TEMPO; private double pitch = DEFAULT_PITCH; - private double stepSize = DEFAULT_STEP; + private int semitones = DEFAULT_SEMITONES; @Nullable private SeekBar tempoSlider; @@ -86,9 +90,19 @@ public class PlaybackParameterDialog extends DialogFragment { @Nullable private TextView pitchStepUpText; @Nullable + private SeekBar semitoneSlider; + @Nullable + private TextView semitoneCurrentText; + @Nullable + private TextView semitoneStepDownText; + @Nullable + private TextView semitoneStepUpText; + @Nullable private CheckBox unhookingCheckbox; @Nullable private CheckBox skipSilenceCheckbox; + @Nullable + private CheckBox adjustBySemitonesCheckbox; public static PlaybackParameterDialog newInstance(final double playbackTempo, final double playbackPitch, @@ -101,6 +115,7 @@ public static PlaybackParameterDialog newInstance(final double playbackTempo, dialog.tempo = playbackTempo; dialog.pitch = playbackPitch; + dialog.semitones = dialog.percentToSemitones(playbackPitch); dialog.initialSkipSilence = playbackSkipSilence; return dialog; @@ -111,7 +126,7 @@ public static PlaybackParameterDialog newInstance(final double playbackTempo, //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAttach(final Context context) { + public void onAttach(@NonNull final Context context) { super.onAttach(context); if (context instanceof Callback) { callback = (Callback) context; @@ -127,22 +142,22 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { if (savedInstanceState != null) { initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); + initialSemitones = percentToSemitones(initialPitch); tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO); pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH); - stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP); + semitones = percentToSemitones(pitch); } } @Override - public void onSaveInstanceState(final Bundle outState) { + public void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); outState.putDouble(INITIAL_PITCH_KEY, initialPitch); outState.putDouble(TEMPO_KEY, getCurrentTempo()); outState.putDouble(PITCH_KEY, getCurrentPitch()); - outState.putDouble(STEP_SIZE_KEY, getCurrentStepSize()); } /*////////////////////////////////////////////////////////////////////////// @@ -160,9 +175,11 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { .setView(view) .setCancelable(true) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> - setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence)) + setPlaybackParameters(initialTempo, initialPitch, + initialSemitones, initialSkipSilence)) .setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> - setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE)) + setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, + DEFAULT_SEMITONES, DEFAULT_SKIP_SILENCE)) .setPositiveButton(R.string.ok, (dialogInterface, i) -> setCurrentPlaybackParameters()); @@ -176,14 +193,49 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { private void setupControlViews(@NonNull final View rootView) { setupHookingControl(rootView); setupSkipSilenceControl(rootView); + setupAdjustBySemitonesControl(rootView); setupTempoControl(rootView); setupPitchControl(rootView); + setupSemitoneControl(rootView); + + togglePitchSliderType(rootView); - setStepSize(stepSize); setupStepSizeSelector(rootView); } + private void togglePitchSliderType(@NonNull final View rootView) { + final RelativeLayout pitchControl = rootView.findViewById(R.id.pitchControl); + final RelativeLayout semitoneControl = rootView.findViewById(R.id.semitoneControl); + + final View separatorStepSizeSelector = + rootView.findViewById(R.id.separatorStepSizeSelector); + final RelativeLayout.LayoutParams params = + (RelativeLayout.LayoutParams) separatorStepSizeSelector.getLayoutParams(); + if (pitchControl != null && semitoneControl != null && unhookingCheckbox != null) { + if (getCurrentAdjustBySemitones()) { + // replaces pitchControl slider with semitoneControl slider + pitchControl.setVisibility(View.GONE); + semitoneControl.setVisibility(View.VISIBLE); + params.addRule(RelativeLayout.BELOW, R.id.semitoneControl); + + // forces unhook for semitones + unhookingCheckbox.setChecked(true); + unhookingCheckbox.setEnabled(false); + + setupTempoStepSizeSelector(rootView); + } else { + semitoneControl.setVisibility(View.GONE); + pitchControl.setVisibility(View.VISIBLE); + params.addRule(RelativeLayout.BELOW, R.id.pitchControl); + + // (re)enables hooking selection + unhookingCheckbox.setEnabled(true); + setupCombinedStepSizeSelector(rootView); + } + } + } + private void setupTempoControl(@NonNull final View rootView) { tempoSlider = rootView.findViewById(R.id.tempoSeekbar); final TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText); @@ -234,23 +286,40 @@ private void setupPitchControl(@NonNull final View rootView) { } } + private void setupSemitoneControl(@NonNull final View rootView) { + semitoneSlider = rootView.findViewById(R.id.semitoneSeekbar); + semitoneCurrentText = rootView.findViewById(R.id.semitoneCurrentText); + semitoneStepDownText = rootView.findViewById(R.id.semitoneStepDown); + semitoneStepUpText = rootView.findViewById(R.id.semitoneStepUp); + + if (semitoneCurrentText != null) { + semitoneCurrentText.setText(getSignedSemitonesString(semitones)); + } + + if (semitoneSlider != null) { + setSemitoneSlider(semitones); + semitoneSlider.setOnSeekBarChangeListener(getOnSemitoneChangedListener()); + } + + } + private void setupHookingControl(@NonNull final View rootView) { unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); if (unhookingCheckbox != null) { - // restore whether pitch and tempo are unhooked or not + // restores whether pitch and tempo are unhooked or not unhookingCheckbox.setChecked(PreferenceManager .getDefaultSharedPreferences(requireContext()) .getBoolean(getString(R.string.playback_unhook_key), true)); unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { - // save whether pitch and tempo are unhooked or not + // saves whether pitch and tempo are unhooked or not PreferenceManager.getDefaultSharedPreferences(requireContext()) .edit() .putBoolean(getString(R.string.playback_unhook_key), isChecked) .apply(); if (!isChecked) { - // when unchecked, slide back to the minimum of current tempo or pitch + // when unchecked, slides back to the minimum of current tempo or pitch final double minimum = Math.min(getCurrentPitch(), getCurrentTempo()); setSliders(minimum); setCurrentPlaybackParameters(); @@ -268,7 +337,51 @@ private void setupSkipSilenceControl(@NonNull final View rootView) { } } + private void setupAdjustBySemitonesControl(@NonNull final View rootView) { + adjustBySemitonesCheckbox = rootView.findViewById(R.id.adjustBySemitonesCheckbox); + if (adjustBySemitonesCheckbox != null) { + // restores whether semitone adjustment is used or not + adjustBySemitonesCheckbox.setChecked(PreferenceManager + .getDefaultSharedPreferences(requireContext()) + .getBoolean(getString(R.string.playback_adjust_by_semitones_key), true)); + + // stores whether semitone adjustment is used or not + adjustBySemitonesCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .edit() + .putBoolean(getString(R.string.playback_adjust_by_semitones_key), isChecked) + .apply(); + togglePitchSliderType(rootView); + if (isChecked) { + setPlaybackParameters( + getCurrentTempo(), + getCurrentPitch(), + Integer.min(12, + Integer.max(-12, percentToSemitones(getCurrentPitch()) + )), + getCurrentSkipSilence() + ); + setSemitoneSlider(Integer.min(12, + Integer.max(-12, percentToSemitones(getCurrentPitch())) + )); + } else { + setPlaybackParameters( + getCurrentTempo(), + semitonesToPercent(getCurrentSemitones()), + getCurrentSemitones(), + getCurrentSkipSilence() + ); + setPitchSlider(semitonesToPercent(getCurrentSemitones())); + } + }); + } + } + private void setupStepSizeSelector(@NonNull final View rootView) { + setStepSize(PreferenceManager + .getDefaultSharedPreferences(requireContext()) + .getFloat(getString(R.string.adjustment_step_key), (float) DEFAULT_STEP)); + final TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent); final TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent); final TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent); @@ -310,8 +423,27 @@ private void setupStepSizeSelector(@NonNull final View rootView) { } } + private void setupTempoStepSizeSelector(@NonNull final View rootView) { + final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type); + if (playbackStepTypeText != null) { + playbackStepTypeText.setText(R.string.playback_tempo_step); + } + setupStepSizeSelector(rootView); + } + + private void setupCombinedStepSizeSelector(@NonNull final View rootView) { + final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type); + if (playbackStepTypeText != null) { + playbackStepTypeText.setText(R.string.playback_step); + } + setupStepSizeSelector(rootView); + } + private void setStepSize(final double stepSize) { - this.stepSize = stepSize; + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .edit() + .putFloat(getString(R.string.adjustment_step_key), (float) stepSize) + .apply(); if (tempoStepUpText != null) { tempoStepUpText.setText(getStepUpPercentString(stepSize)); @@ -344,16 +476,30 @@ private void setStepSize(final double stepSize) { setCurrentPlaybackParameters(); }); } + + if (semitoneStepDownText != null) { + semitoneStepDownText.setOnClickListener(view -> { + onSemitoneSliderUpdated(getCurrentSemitones() - 1); + setCurrentPlaybackParameters(); + }); + } + + if (semitoneStepUpText != null) { + semitoneStepUpText.setOnClickListener(view -> { + onSemitoneSliderUpdated(getCurrentSemitones() + 1); + setCurrentPlaybackParameters(); + }); + } } /*////////////////////////////////////////////////////////////////////////// // Sliders //////////////////////////////////////////////////////////////////////////*/ - private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() { - return new SeekBar.OnSeekBarChangeListener() { + private SimpleOnSeekBarChangeListener getOnTempoChangedListener() { + return 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 double currentTempo = strategy.valueOf(progress); if (fromUser) { @@ -361,23 +507,13 @@ public void onProgressChanged(final SeekBar seekBar, final int progress, setCurrentPlaybackParameters(); } } - - @Override - public void onStartTrackingTouch(final SeekBar seekBar) { - // Do Nothing. - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - // Do Nothing. - } }; } - private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { - return new SeekBar.OnSeekBarChangeListener() { + private SimpleOnSeekBarChangeListener getOnPitchChangedListener() { + return 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 double currentPitch = strategy.valueOf(progress); if (fromUser) { // this change is first in chain @@ -385,23 +521,27 @@ public void onProgressChanged(final SeekBar seekBar, final int progress, setCurrentPlaybackParameters(); } } + }; + } + private SimpleOnSeekBarChangeListener getOnSemitoneChangedListener() { + return new SimpleOnSeekBarChangeListener() { @Override - public void onStartTrackingTouch(final SeekBar seekBar) { - // Do Nothing. - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - // Do Nothing. + public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress, + final boolean fromUser) { + // semitone slider supplies values 0 to 24, subtraction by 12 is required + final int currentSemitones = progress - 12; + if (fromUser) { // this change is first in chain + onSemitoneSliderUpdated(currentSemitones); + // line below also saves semitones as pitch percentages + onPitchSliderUpdated(semitonesToPercent(currentSemitones)); + setCurrentPlaybackParameters(); + } } }; } private void onTempoSliderUpdated(final double newTempo) { - if (unhookingCheckbox == null) { - return; - } if (!unhookingCheckbox.isChecked()) { setSliders(newTempo); } else { @@ -410,9 +550,6 @@ private void onTempoSliderUpdated(final double newTempo) { } private void onPitchSliderUpdated(final double newPitch) { - if (unhookingCheckbox == null) { - return; - } if (!unhookingCheckbox.isChecked()) { setSliders(newPitch); } else { @@ -420,6 +557,10 @@ private void onPitchSliderUpdated(final double newPitch) { } } + private void onSemitoneSliderUpdated(final int newSemitone) { + setSemitoneSlider(newSemitone); + } + private void setSliders(final double newValue) { setTempoSlider(newValue); setPitchSlider(newValue); @@ -439,25 +580,49 @@ private void setPitchSlider(final double newPitch) { pitchSlider.setProgress(strategy.progressOf(newPitch)); } + private void setSemitoneSlider(final int newSemitone) { + if (semitoneSlider == null) { + return; + } + semitoneSlider.setProgress(newSemitone + 12); + } + /*////////////////////////////////////////////////////////////////////////// // Helper //////////////////////////////////////////////////////////////////////////*/ private void setCurrentPlaybackParameters() { - setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence()); + if (getCurrentAdjustBySemitones()) { + setPlaybackParameters( + getCurrentTempo(), + semitonesToPercent(getCurrentSemitones()), + getCurrentSemitones(), + getCurrentSkipSilence() + ); + } else { + setPlaybackParameters( + getCurrentTempo(), + getCurrentPitch(), + percentToSemitones(getCurrentPitch()), + getCurrentSkipSilence() + ); + } } private void setPlaybackParameters(final double newTempo, final double newPitch, - final boolean skipSilence) { - if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { + final int newSemitones, final boolean skipSilence) { + if (callback != null && tempoCurrentText != null + && pitchCurrentText != null && semitoneCurrentText != null) { if (DEBUG) { Log.d(TAG, "Setting playback parameters to " + "tempo=[" + newTempo + "], " - + "pitch=[" + newPitch + "]"); + + "pitch=[" + newPitch + "], " + + "semitones=[" + newSemitones + "]"); } tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo)); pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch)); + semitoneCurrentText.setText(getSignedSemitonesString(newSemitones)); callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, skipSilence); } } @@ -470,14 +635,19 @@ private double getCurrentPitch() { return pitchSlider == null ? pitch : strategy.valueOf(pitchSlider.getProgress()); } - private double getCurrentStepSize() { - return stepSize; + private int getCurrentSemitones() { + // semitoneSlider is absolute, that's why - 12 + return semitoneSlider == null ? semitones : semitoneSlider.getProgress() - 12; } private boolean getCurrentSkipSilence() { return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked(); } + private boolean getCurrentAdjustBySemitones() { + return adjustBySemitonesCheckbox != null && adjustBySemitonesCheckbox.isChecked(); + } + @NonNull private static String getStepUpPercentString(final double percent) { return STEP_UP_SIGN + getPercentString(percent); @@ -493,8 +663,21 @@ private static String getPercentString(final double percent) { return PlayerHelper.formatPitch(percent); } + @NonNull + private static String getSignedSemitonesString(final int semitones) { + return semitones > 0 ? "+" + semitones : "" + semitones; + } + public interface Callback { void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, boolean playbackSkipSilence); } + + public double semitonesToPercent(final int inSemitones) { + return Math.pow(2, inSemitones / 12.0); + } + + public int percentToSemitones(final double inPercent) { + return (int) Math.round(12 * Math.log(inPercent) / Math.log(2)); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index a2f0d7149e0..d7a9ffc3d24 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -31,11 +31,13 @@ public class PlayerDataSource { private static final int MANIFEST_MINIMUM_RETRY = 5; private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE; + private final int continueLoadingCheckIntervalBytes; private final DataSource.Factory cacheDataSourceFactory; private final DataSource.Factory cachelessDataSourceFactory; public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent, @NonNull final TransferListener transferListener) { + continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context); cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); @@ -91,6 +93,7 @@ public DashMediaSource.Factory getDashMediaSourceFactory() { public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() { return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) + .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes) .setLoadErrorHandlingPolicy( new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index c51b6d5dde0..6a7c27bdcfa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; @@ -391,6 +392,19 @@ public static boolean globalScreenOrientationLocked(final Context context) { context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0; } + public static int getProgressiveLoadIntervalBytes(@NonNull final Context context) { + final String preferredIntervalBytes = getPreferences(context).getString( + context.getString(R.string.progressive_load_interval_key), + context.getString(R.string.progressive_load_interval_default_value)); + + if (context.getString(R.string.progressive_load_interval_default_value) + .equals(preferredIntervalBytes)) { + return ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES; + } + // Keeping the same KiB unit used by ProgressiveMediaSource + return Integer.parseInt(preferredIntervalBytes) * 1024; + } + //////////////////////////////////////////////////////////////////////////// // Private helpers //////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt new file mode 100644 index 00000000000..52eff5a1cce --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt @@ -0,0 +1,47 @@ +package org.schabi.newpipe.player.listeners.view + +import android.util.Log +import android.view.View +import androidx.appcompat.widget.PopupMenu +import org.schabi.newpipe.MainActivity +import org.schabi.newpipe.player.Player +import org.schabi.newpipe.player.helper.PlaybackParameterDialog + +/** + * Click listener for the playbackSpeed textview of the player + */ +class PlaybackSpeedClickListener( + private val player: Player, + private val playbackSpeedPopupMenu: PopupMenu +) : View.OnClickListener { + + companion object { + private const val TAG: String = "PlaybSpeedClickListener" + } + + override fun onClick(v: View) { + if (MainActivity.DEBUG) { + Log.d(TAG, "onPlaybackSpeedClicked() called") + } + + if (player.videoPlayerSelected()) { + PlaybackParameterDialog.newInstance( + player.playbackSpeed.toDouble(), + player.playbackPitch.toDouble(), + player.playbackSkipSilence + ) { speed: Float, pitch: Float, skipSilence: Boolean -> + player.setPlaybackParameters( + speed, + pitch, + skipSilence + ) + } + .show(player.parentActivity!!.supportFragmentManager, null) + } else { + playbackSpeedPopupMenu.show() + player.isSomePopupMenuVisible = true + } + + player.manageControlsAfterOnClick(v) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt new file mode 100644 index 00000000000..b103ac0e6c5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt @@ -0,0 +1,41 @@ +package org.schabi.newpipe.player.listeners.view + +import android.annotation.SuppressLint +import android.util.Log +import android.view.View +import androidx.appcompat.widget.PopupMenu +import org.schabi.newpipe.MainActivity +import org.schabi.newpipe.extractor.MediaFormat +import org.schabi.newpipe.player.Player + +/** + * Click listener for the qualityTextView of the player + */ +class QualityClickListener( + private val player: Player, + private val qualityPopupMenu: PopupMenu +) : View.OnClickListener { + + companion object { + private const val TAG: String = "QualityClickListener" + } + + @SuppressLint("SetTextI18n") // we don't need I18N because of a " " + override fun onClick(v: View) { + if (MainActivity.DEBUG) { + Log.d(TAG, "onQualitySelectorClicked() called") + } + + qualityPopupMenu.show() + player.isSomePopupMenuVisible = true + + val videoStream = player.selectedVideoStream + if (videoStream != null) { + player.binding.qualityTextView.text = + MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.resolution + } + + player.saveWasPlaying() + player.manageControlsAfterOnClick(v) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java index 9dcb12344f6..ee0a6f11819 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java @@ -88,6 +88,8 @@ public MediaDescriptionCompat getQueueMetadata(final int index) { @Override public void play() { player.play(); + // hide the player controls even if the play command came from the media session + player.hideControls(0, 0); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java index 07c8d9f90b4..df2747c3b74 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java @@ -4,20 +4,19 @@ import androidx.annotation.NonNull; -import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import io.reactivex.rxjava3.core.SingleObserver; import io.reactivex.rxjava3.disposables.Disposable; -abstract class AbstractInfoPlayQueue extends PlayQueue { +abstract class AbstractInfoPlayQueue> + extends PlayQueue { boolean isInitial; private boolean isComplete; @@ -27,12 +26,15 @@ abstract class AbstractInfoPlayQueue ext private transient Disposable fetchReactor; - AbstractInfoPlayQueue(final U item) { - this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0); + protected AbstractInfoPlayQueue(final T info) { + this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0); } - AbstractInfoPlayQueue(final int serviceId, final String url, final Page nextPage, - final List streams, final int index) { + protected AbstractInfoPlayQueue(final int serviceId, + final String url, + final Page nextPage, + final List streams, + final int index) { super(index, extractListItems(streams)); this.baseUrl = url; @@ -51,7 +53,7 @@ public boolean isComplete() { } SingleObserver getHeadListObserver() { - return new SingleObserver() { + return new SingleObserver<>() { @Override public void onSubscribe(@NonNull final Disposable d) { if (isComplete || !isInitial || (fetchReactor != null @@ -85,8 +87,8 @@ public void onError(@NonNull final Throwable e) { }; } - SingleObserver getNextPageObserver() { - return new SingleObserver() { + SingleObserver> getNextPageObserver() { + return new SingleObserver<>() { @Override public void onSubscribe(@NonNull final Disposable d) { if (isComplete || isInitial || (fetchReactor != null @@ -98,7 +100,8 @@ public void onSubscribe(@NonNull final Disposable d) { } @Override - public void onSuccess(@NonNull final ListExtractor.InfoItemsPage result) { + public void onSuccess( + @NonNull final ListExtractor.InfoItemsPage result) { if (!result.hasNextPage()) { isComplete = true; } @@ -129,12 +132,6 @@ public void dispose() { } private static List extractListItems(final List infoItems) { - final List result = new ArrayList<>(); - for (final InfoItem stream : infoItems) { - if (stream instanceof StreamInfoItem) { - result.add(new PlayQueueItem((StreamInfoItem) stream)); - } - } - return result; + return infoItems.stream().map(PlayQueueItem::new).collect(Collectors.toList()); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java index f8534979770..1e1fef85ea2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java @@ -3,7 +3,6 @@ import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.util.ExtractorHelper; @@ -12,13 +11,10 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.schedulers.Schedulers; -public final class ChannelPlayQueue extends AbstractInfoPlayQueue { - public ChannelPlayQueue(final ChannelInfoItem item) { - super(item); - } +public final class ChannelPlayQueue extends AbstractInfoPlayQueue { public ChannelPlayQueue(final ChannelInfo info) { - this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0); + super(info); } public ChannelPlayQueue(final int serviceId, diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index f2259b1202c..f46c9d72fd0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -528,7 +528,19 @@ public boolean equals(@Nullable final Object obj) { return false; } final PlayQueue other = (PlayQueue) obj; - return streams.equals(other.streams); + if (size() != other.size()) { + return false; + } + for (int i = 0; i < size(); i++) { + final PlayQueueItem stream = streams.get(i); + final PlayQueueItem otherStream = other.streams.get(i); + // Check is based on serviceId and URL + if (stream.getServiceId() != otherStream.getServiceId() + || !stream.getUrl().equals(otherStream.getUrl())) { + return false; + } + } + return true; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java index f7dfc562e48..bf31ea9b10d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java @@ -52,6 +52,7 @@ public class PlayQueueItem implements Serializable { item.getUploaderUrl(), item.getStreamType()); } + @SuppressWarnings("ParameterNumber") private PlayQueueItem(@Nullable final String name, @Nullable final String url, final int serviceId, final long duration, @Nullable final String thumbnailUrl, @Nullable final String uploader, diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java index ac5dce9ba4e..01883d7d982 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java @@ -2,7 +2,6 @@ import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.util.ExtractorHelper; @@ -11,13 +10,10 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.schedulers.Schedulers; -public final class PlaylistPlayQueue extends AbstractInfoPlayQueue { - public PlaylistPlayQueue(final PlaylistInfoItem item) { - super(item); - } +public final class PlaylistPlayQueue extends AbstractInfoPlayQueue { public PlaylistPlayQueue(final PlaylistInfo info) { - this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0); + super(info); } public PlaylistPlayQueue(final int serviceId, diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index 245a85e7101..11949f55dec 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import static com.google.android.exoplayer2.C.TIME_UNSET; @@ -31,10 +32,17 @@ public class VideoPlaybackResolver implements PlaybackResolver { private final PlayerDataSource dataSource; @NonNull private final QualityResolver qualityResolver; + private SourceType streamSourceType; @Nullable private String playbackQuality; + public enum SourceType { + LIVE_STREAM, + VIDEO_WITH_SEPARATED_AUDIO, + VIDEO_WITH_AUDIO_OR_AUDIO_ONLY + } + public VideoPlaybackResolver(@NonNull final Context context, @NonNull final PlayerDataSource dataSource, @NonNull final QualityResolver qualityResolver) { @@ -48,6 +56,7 @@ public VideoPlaybackResolver(@NonNull final Context context, public MediaSource resolve(@NonNull final StreamInfo info) { final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); if (liveSource != null) { + streamSourceType = SourceType.LIVE_STREAM; return liveSource; } @@ -55,7 +64,7 @@ public MediaSource resolve(@NonNull final StreamInfo info) { // Create video stream source final List videos = ListHelper.getSortedStreamVideosList(context, - info.getVideoStreams(), info.getVideoOnlyStreams(), false); + info.getVideoStreams(), info.getVideoOnlyStreams(), false, true); final int index; if (videos.isEmpty()) { index = -1; @@ -85,6 +94,9 @@ public MediaSource resolve(@NonNull final StreamInfo info) { PlayerHelper.cacheKeyOf(info, audio), MediaFormat.getSuffixById(audio.getFormatId()), tag); mediaSources.add(audioSource); + streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO; + } else { + streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY; } // If there is no audio or video sources, then this media source cannot be played back @@ -118,6 +130,16 @@ public MediaSource resolve(@NonNull final StreamInfo info) { } } + /** + * Returns the last resolved {@link StreamInfo}'s {@link SourceType source type}. + * + * @return {@link Optional#empty()} if nothing was resolved, otherwise the {@link SourceType} + * of the last resolved {@link StreamInfo} inside an {@link Optional} + */ + public Optional getStreamSourceType() { + return Optional.ofNullable(streamSourceType); + } + @Nullable public String getPlaybackQuality() { return playbackQuality; diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index e0856290888..70ac5cdcc7d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -50,7 +50,7 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro @Override public boolean onPreferenceTreeClick(final Preference preference) { - if (preference.getKey().equals(getString(R.string.caption_settings_key))) { + if (getString(R.string.caption_settings_key).equals(preference.getKey())) { try { startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); } catch (final ActivityNotFoundException e) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index fe327e1b534..ec98b865ea5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -185,7 +185,7 @@ private void showMessageDialog(@StringRes final int title, @StringRes final int } @Override - public boolean onPreferenceTreeClick(final Preference preference) { + public boolean onPreferenceTreeClick(@NonNull final Preference preference) { if (DEBUG) { Log.d(TAG, "onPreferenceTreeClick() called with: " + "preference = [" + preference + "]"); diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index d7fb559d632..3776d78f679 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -7,10 +7,9 @@ import androidx.annotation.NonNull; -import org.schabi.newpipe.App; -import org.schabi.newpipe.CheckForNewAppVersion; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.ReleaseVersionUtil; public class MainSettingsFragment extends BasePreferenceFragment { public static final boolean DEBUG = MainActivity.DEBUG; @@ -24,7 +23,7 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called // Check if the app is updatable - if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { + if (!ReleaseVersionUtil.isReleaseApk()) { getPreferenceScreen().removePreference( findPreference(getString(R.string.update_pref_screen_key))); diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index dfc053a6257..5767d266fad 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -21,7 +21,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceManager; @@ -51,8 +50,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers; public class PeertubeInstanceListFragment extends Fragment { - private static final int MENU_ITEM_RESTORE_ID = 123456; - private final List instanceList = new ArrayList<>(); private PeertubeInstance selectedInstance; private String savedInstanceListKey; @@ -142,17 +139,12 @@ public void onDestroy() { public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - - final MenuItem restoreItem = menu - .add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults); - restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), - R.drawable.ic_settings_backup_restore)); + inflater.inflate(R.menu.menu_chooser_fragment, menu); } @Override public boolean onOptionsItemSelected(final MenuItem item) { - if (item.getItemId() == MENU_ITEM_RESTORE_ID) { + if (item.getItemId() == R.id.menu_item_restore_default) { restoreDefaults(); return true; } @@ -191,7 +183,7 @@ private void restoreDefaults() { .setTitle(R.string.restore_defaults) .setMessage(R.string.restore_defaults_confirmation) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.yes, (dialog, which) -> { + .setPositiveButton(R.string.ok, (dialog, which) -> { sharedPreferences.edit().remove(savedInstanceListKey).apply(); selectInstance(PeertubeInstance.defaultInstance); updateInstanceList(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java index 116807cbc8e..0f25be63083 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -5,6 +5,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; @@ -24,7 +25,6 @@ import java.util.List; import java.util.Vector; -import de.hdodenhof.circleimageview.CircleImageView; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observer; import io.reactivex.rxjava3.disposables.Disposable; @@ -200,7 +200,7 @@ public int getItemCount() { public class SelectChannelItemHolder extends RecyclerView.ViewHolder { public final View view; - final CircleImageView thumbnailView; + final ImageView thumbnailView; final TextView titleView; SelectChannelItemHolder(final View v) { super(v); diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 7510bb3bcde..3ee6668bf94 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -23,8 +23,6 @@ import com.jakewharton.rxbinding4.widget.RxTextView; -import org.schabi.newpipe.App; -import org.schabi.newpipe.CheckForNewAppVersion; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.SettingsLayoutBinding; @@ -37,6 +35,7 @@ import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.KeyboardUtil; +import org.schabi.newpipe.util.ReleaseVersionUtil; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -170,7 +169,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { } @Override - public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller, + public boolean onPreferenceStartFragment(@NonNull final PreferenceFragmentCompat caller, final Preference preference) { showSettingsFragment(instantiateFragment(preference.getFragment())); return true; @@ -267,7 +266,7 @@ private void initSearch( */ private void ensureSearchRepresentsApplicationState() { // Check if the update settings are available - if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) { + if (!ReleaseVersionUtil.isReleaseApk()) { SettingsResourceRegistry.getInstance() .getEntryByPreferencesResId(R.xml.update_settings) .setSearchable(false); diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java index 1d4badcac2b..78ddb37866d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsResourceRegistry.java @@ -116,6 +116,7 @@ public SettingRegistryEntry setSearchable(final boolean searchable) { return this; } + @NonNull public Class getFragmentClass() { return fragmentClass; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index 04bad38152a..1043e88c2e8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -1,12 +1,11 @@ package org.schabi.newpipe.settings; -import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService; - import android.os.Bundle; import android.widget.Toast; import androidx.preference.Preference; +import org.schabi.newpipe.NewVersionWorker; import org.schabi.newpipe.R; public class UpdateSettingsFragment extends BasePreferenceFragment { @@ -33,7 +32,7 @@ private void checkNewVersionNow() { // Reset the expire time. This is necessary to check for an update immediately. defaultPreferences.edit() .putLong(getString(R.string.update_expiry_key), 0).apply(); - startNewVersionCheckService(); + NewVersionWorker.enqueueNewVersionCheckingWork(getContext()); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt b/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt index 14801c01c37..f0b89c6774d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt @@ -14,10 +14,10 @@ import org.schabi.newpipe.util.Localization * If the entry values array have anything other than numbers in it, an exception will be raised. */ class DurationListPreference : ListPreference { - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context?) : super(context) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) override fun onAttached() { super.onAttached() diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java index 045e574be0a..62455d6823b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java @@ -50,7 +50,7 @@ public NotificationActionsPreference(final Context context, final AttributeSet a //////////////////////////////////////////////////////////////////////////// @Override - public void onBindViewHolder(final PreferenceViewHolder holder) { + public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) { super.onBindViewHolder(holder); holder.itemView.setClickable(false); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java index 5835dcab553..a445ea3095b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchConfiguration.java @@ -4,6 +4,7 @@ import androidx.preference.PreferenceScreen; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -11,7 +12,7 @@ public class PreferenceSearchConfiguration { private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction(); - private final List parserIgnoreElements = Arrays.asList( + private final List parserIgnoreElements = Collections.singletonList( PreferenceCategory.class.getSimpleName()); private final List parserContainerElements = Arrays.asList( PreferenceCategory.class.getSimpleName(), diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java index 52935ef8ec8..98d2a5d843e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java @@ -58,22 +58,27 @@ public PreferenceSearchItem( this.searchIndexItemResId = searchIndexItemResId; } + @NonNull public String getKey() { return key; } + @NonNull public String getTitle() { return title; } + @NonNull public String getSummary() { return summary; } + @NonNull public String getEntries() { return entries; } + @NonNull public String getBreadcrumbs() { return breadcrumbs; } @@ -94,7 +99,7 @@ public List getAllRelevantSearchFields() { getBreadcrumbs()); } - + @NonNull @Override public String toString() { return "PreferenceItem: " + title + " " + summary + " " + key; diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 490e299bd4b..73aec4a7bf3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -7,7 +7,6 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; -import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -17,7 +16,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.ItemTouchHelper; @@ -107,12 +105,8 @@ public void onPause() { public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - - final MenuItem restoreItem = menu.add(R.string.restore_defaults); - restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), - R.drawable.ic_settings_backup_restore)); - restoreItem.setOnMenuItemClickListener(ev -> { + inflater.inflate(R.menu.menu_chooser_fragment, menu); + menu.findItem(R.id.menu_item_restore_default).setOnMenuItemClickListener(item -> { restoreDefaults(); return true; }); @@ -136,7 +130,7 @@ private void restoreDefaults() { .setTitle(R.string.restore_defaults) .setMessage(R.string.restore_defaults_confirmation) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.yes, (dialog, which) -> { + .setPositiveButton(R.string.ok, (dialog, which) -> { tabsManager.resetTabs(); updateTabList(); selectedTabsAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index aa03bbfa6fd..6b1d70a8668 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -8,7 +8,7 @@ import androidx.fragment.app.Fragment; import com.grack.nanojson.JsonObject; -import com.grack.nanojson.JsonSink; +import com.grack.nanojson.JsonStringWriter; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem.LocalItemType; @@ -132,7 +132,7 @@ public int hashCode() { // JSON Handling //////////////////////////////////////////////////////////////////////////*/ - public void writeJsonOn(final JsonSink jsonSink) { + public void writeJsonOn(final JsonStringWriter jsonSink) { jsonSink.object(); jsonSink.value(JSON_TAB_ID_KEY, getTabId()); @@ -141,7 +141,7 @@ public void writeJsonOn(final JsonSink jsonSink) { jsonSink.end(); } - protected void writeDataToJson(final JsonSink writerSink) { + protected void writeDataToJson(final JsonStringWriter writerSink) { // No-op } @@ -340,7 +340,7 @@ public String getTabName(final Context context) { @DrawableRes @Override public int getTabIconRes(final Context context) { - final int kioskIcon = KioskTranslator.getKioskIcon(kioskId, context); + final int kioskIcon = KioskTranslator.getKioskIcon(kioskId); if (kioskIcon <= 0) { throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\""); @@ -355,7 +355,7 @@ public KioskFragment getFragment(final Context context) throws ExtractionExcepti } @Override - protected void writeDataToJson(final JsonSink writerSink) { + protected void writeDataToJson(final JsonStringWriter writerSink) { writerSink.value(JSON_KIOSK_SERVICE_ID_KEY, kioskServiceId) .value(JSON_KIOSK_ID_KEY, kioskId); } @@ -437,7 +437,7 @@ public ChannelFragment getFragment(final Context context) { } @Override - protected void writeDataToJson(final JsonSink writerSink) { + protected void writeDataToJson(final JsonStringWriter writerSink) { writerSink.value(JSON_CHANNEL_SERVICE_ID_KEY, channelServiceId) .value(JSON_CHANNEL_URL_KEY, channelUrl) .value(JSON_CHANNEL_NAME_KEY, channelName); @@ -496,7 +496,7 @@ public String getTabName(final Context context) { @DrawableRes @Override public int getTabIconRes(final Context context) { - return KioskTranslator.getKioskIcon(getDefaultKioskId(context), context); + return KioskTranslator.getKioskIcon(getDefaultKioskId(context)); } @Override @@ -584,7 +584,7 @@ public Fragment getFragment(final Context context) { } @Override - protected void writeDataToJson(final JsonSink writerSink) { + protected void writeDataToJson(final JsonStringWriter writerSink) { writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId) .value(JSON_PLAYLIST_URL_KEY, playlistUrl) .value(JSON_PLAYLIST_NAME_KEY, playlistName) diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index ca3da9d2449..889cc85e629 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -142,6 +142,7 @@ public void close() throws IOException { outStream = null; } + @SuppressWarnings("MethodLength") public void build(final SharpStream output) throws IOException { if (done) { throw new RuntimeException("already done"); diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index ebae3812ca0..2b69f23ac07 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -121,6 +121,7 @@ public void close() { clustersOffsetsSizes = null; } + @SuppressWarnings("MethodLength") public void build(final SharpStream out) throws IOException, RuntimeException { if (!out.canRewind()) { throw new IOException("The output stream must be allow seek"); diff --git a/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java b/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java deleted file mode 100644 index b6f1eaf491b..00000000000 --- a/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.schabi.newpipe.util; - -import android.graphics.Bitmap; - -import androidx.annotation.Nullable; - -public final class BitmapUtils { - private BitmapUtils() { } - - @Nullable - public static Bitmap centerCrop(final Bitmap inputBitmap, final int newWidth, - final int newHeight) { - if (inputBitmap == null || inputBitmap.isRecycled()) { - return null; - } - - final float sourceWidth = inputBitmap.getWidth(); - final float sourceHeight = inputBitmap.getHeight(); - - final float xScale = newWidth / sourceWidth; - final float yScale = newHeight / sourceHeight; - - final float newXScale; - final float newYScale; - - if (yScale > xScale) { - newXScale = xScale / yScale; - newYScale = 1.0f; - } else { - newXScale = 1.0f; - newYScale = yScale / xScale; - } - - final float scaledWidth = newXScale * sourceWidth; - final float scaledHeight = newYScale * sourceHeight; - - final int left = (int) ((sourceWidth - scaledWidth) / 2); - final int top = (int) ((sourceHeight - scaledHeight) / 2); - final int width = (int) scaledWidth; - final int height = (int) scaledHeight; - - return Bitmap.createBitmap(inputBitmap, left, top, width, height); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index af94e3366a9..27009efd192 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -19,6 +19,8 @@ package org.schabi.newpipe.util; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + import android.content.Context; import android.util.Log; import android.view.View; @@ -30,7 +32,6 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.external_communication.TextLinkifier; import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; @@ -41,6 +42,7 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo; +import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.feed.FeedExtractor; import org.schabi.newpipe.extractor.feed.FeedInfo; import org.schabi.newpipe.extractor.kiosk.KioskInfo; @@ -49,6 +51,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; +import org.schabi.newpipe.util.external_communication.TextLinkifier; import java.util.Collections; import java.util.List; @@ -57,8 +60,6 @@ import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; - public final class ExtractorHelper { private static final String TAG = ExtractorHelper.class.getSimpleName(); private static final InfoCache CACHE = InfoCache.getInstance(); @@ -84,11 +85,12 @@ public static Single searchFor(final int serviceId, final String sea .fromQuery(searchString, contentFilter, sortFilter))); } - public static Single getMoreSearchItems(final int serviceId, - final String searchString, - final List contentFilter, - final String sortFilter, - final Page page) { + public static Single> getMoreSearchItems( + final int serviceId, + final String searchString, + final List contentFilter, + final String sortFilter, + final Page page) { checkServiceId(serviceId); return Single.fromCallable(() -> SearchInfo.getMoreItems(NewPipe.getService(serviceId), @@ -124,8 +126,9 @@ public static Single getChannelInfo(final int serviceId, final Stri ChannelInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreChannelItems(final int serviceId, final String url, - final Page nextPage) { + public static Single> getMoreChannelItems(final int serviceId, + final String url, + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); @@ -155,15 +158,17 @@ public static Single getCommentsInfo(final int serviceId, final St CommentsInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreCommentItems(final int serviceId, - final CommentsInfo info, - final Page nextPage) { + public static Single> getMoreCommentItems( + final int serviceId, + final CommentsInfo info, + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage)); } - public static Single getPlaylistInfo(final int serviceId, final String url, + public static Single getPlaylistInfo(final int serviceId, + final String url, final boolean forceLoad) { checkServiceId(serviceId); return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, @@ -171,8 +176,9 @@ public static Single getPlaylistInfo(final int serviceId, final St PlaylistInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMorePlaylistItems(final int serviceId, final String url, - final Page nextPage) { + public static Single> getMorePlaylistItems(final int serviceId, + final String url, + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); @@ -184,8 +190,9 @@ public static Single getKioskInfo(final int serviceId, final String u Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreKioskItems(final int serviceId, final String url, - final Page nextPage) { + public static Single> getMoreKioskItems(final int serviceId, + final String url, + final Page nextPage) { return Single.fromCallable(() -> KioskInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java index f77aa0fdab9..b8c2ff23699 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java @@ -57,7 +57,7 @@ public static String getTranslatedKioskName(final String kioskId, final Context } } - public static int getKioskIcon(final String kioskId, final Context c) { + public static int getKioskIcon(final String kioskId) { switch (kioskId) { case "Trending": case "Top 50": diff --git a/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java b/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java deleted file mode 100644 index fd50d2edbd8..00000000000 --- a/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.Context; -import android.graphics.PointF; - -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.LinearSmoothScroller; -import androidx.recyclerview.widget.RecyclerView; - -public class LayoutManagerSmoothScroller extends LinearLayoutManager { - public LayoutManagerSmoothScroller(final Context context) { - super(context, VERTICAL, false); - } - - public LayoutManagerSmoothScroller(final Context context, final int orientation, - final boolean reverseLayout) { - super(context, orientation, reverseLayout); - } - - @Override - public void smoothScrollToPosition(final RecyclerView recyclerView, - final RecyclerView.State state, final int position) { - final RecyclerView.SmoothScroller smoothScroller - = new TopSnappedSmoothScroller(recyclerView.getContext()); - smoothScroller.setTargetPosition(position); - startSmoothScroll(smoothScroller); - } - - private class TopSnappedSmoothScroller extends LinearSmoothScroller { - TopSnappedSmoothScroller(final Context context) { - super(context); - - } - - @Override - public PointF computeScrollVectorForPosition(final int targetPosition) { - return LayoutManagerSmoothScroller.this - .computeScrollVectorForPosition(targetPosition); - } - - @Override - protected int getVerticalSnapPreference() { - return SNAP_TO_START; - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index eb3c2182755..c3ccef87c59 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -4,6 +4,7 @@ import android.content.SharedPreferences; import android.net.ConnectivityManager; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; @@ -19,7 +20,11 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; public final class ListHelper { // Video format in order of quality. 0=lowest quality, n=highest quality @@ -33,8 +38,9 @@ public final class ListHelper { private static final List AUDIO_FORMAT_EFFICIENCY_RANKING = Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); - private static final List HIGH_RESOLUTION_LIST - = Arrays.asList("1440p", "2160p", "1440p60", "2160p60"); + private static final Set HIGH_RESOLUTION_LIST + // Uses a HashSet for better performance + = new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60")); private ListHelper() { } @@ -108,17 +114,21 @@ public static int getDefaultAudioFormat(final Context context, * Join the two lists of video streams (video_only and normal videos), * and sort them according with default format chosen by the user. * - * @param context context to search for the format to give preference - * @param videoStreams normal videos list - * @param videoOnlyStreams video only stream list - * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + * @param context the context to search for the format to give preference + * @param videoStreams the normal videos list + * @param videoOnlyStreams the video-only stream list + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + * @param preferVideoOnlyStreams if video-only streams should preferred when both video-only + * streams and normal video streams are available * @return the sorted list */ - public static List getSortedStreamVideosList(final Context context, - final List videoStreams, - final List - videoOnlyStreams, - final boolean ascendingOrder) { + @NonNull + public static List getSortedStreamVideosList( + @NonNull final Context context, + @Nullable final List videoStreams, + @Nullable final List videoOnlyStreams, + final boolean ascendingOrder, + final boolean preferVideoOnlyStreams) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -128,7 +138,7 @@ public static List getSortedStreamVideosList(final Context context, R.string.default_video_format_key, R.string.default_video_format_value); return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams, - videoOnlyStreams, ascendingOrder); + videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams); } /*////////////////////////////////////////////////////////////////////////// @@ -192,56 +202,55 @@ static int getDefaultResolutionIndex(final String defaultResolution, * Join the two lists of video streams (video_only and normal videos), * and sort them according with default format chosen by the user. * - * @param defaultFormat format to give preference - * @param showHigherResolutions show >1080p resolutions - * @param videoStreams normal videos list - * @param videoOnlyStreams video only stream list - * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + * @param defaultFormat format to give preference + * @param showHigherResolutions show >1080p resolutions + * @param videoStreams normal videos list + * @param videoOnlyStreams video only stream list + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + * @param preferVideoOnlyStreams if video-only streams should preferred when both video-only + * streams and normal video streams are available * @return the sorted list */ - static List getSortedStreamVideosList(final MediaFormat defaultFormat, - final boolean showHigherResolutions, - final List videoStreams, - final List videoOnlyStreams, - final boolean ascendingOrder) { - final ArrayList retList = new ArrayList<>(); - final HashMap hashMap = new HashMap<>(); - - if (videoOnlyStreams != null) { - for (final VideoStream stream : videoOnlyStreams) { - if (!showHigherResolutions - && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) { - continue; - } - retList.add(stream); - } - } - if (videoStreams != null) { - for (final VideoStream stream : videoStreams) { - if (!showHigherResolutions - && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) { - continue; - } - retList.add(stream); - } - } + @NonNull + static List getSortedStreamVideosList( + @Nullable final MediaFormat defaultFormat, + final boolean showHigherResolutions, + @Nullable final List videoStreams, + @Nullable final List videoOnlyStreams, + final boolean ascendingOrder, + final boolean preferVideoOnlyStreams + ) { + // Determine order of streams + // The last added list is preferred + final List> videoStreamsOrdered = + preferVideoOnlyStreams + ? Arrays.asList(videoStreams, videoOnlyStreams) + : Arrays.asList(videoOnlyStreams, videoStreams); + + final List allInitialStreams = videoStreamsOrdered.stream() + // Ignore lists that are null + .filter(Objects::nonNull) + .flatMap(List::stream) + // Filter out higher resolutions (or not if high resolutions should always be shown) + .filter(stream -> showHigherResolutions + || !HIGH_RESOLUTION_LIST.contains(stream.getResolution())) + .collect(Collectors.toList()); + final HashMap hashMap = new HashMap<>(); // Add all to the hashmap - for (final VideoStream videoStream : retList) { + for (final VideoStream videoStream : allInitialStreams) { hashMap.put(videoStream.getResolution(), videoStream); } // Override the values when the key == resolution, with the defaultFormat - for (final VideoStream videoStream : retList) { + for (final VideoStream videoStream : allInitialStreams) { if (videoStream.getFormat() == defaultFormat) { hashMap.put(videoStream.getResolution(), videoStream); } } - retList.clear(); - retList.addAll(hashMap.values()); - sortStreamList(retList, ascendingOrder); - return retList; + // Return the sorted list + return sortStreamList(new ArrayList<>(hashMap.values()), ascendingOrder); } /** @@ -257,16 +266,18 @@ static List getSortedStreamVideosList(final MediaFormat defaultForm * 1080p -> 1080 * 1080p60 -> 1081 *
- * ascendingOrder ? 360 < 720 < 721 < 1080 < 1081 - * !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360 + * ascendingOrder ? 360 < 720 < 721 < 1080 < 1081 + * !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360 * * @param videoStreams list that the sorting will be applied * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + * @return The sorted list (same reference as parameter videoStreams) */ - private static void sortStreamList(final List videoStreams, - final boolean ascendingOrder) { + private static List sortStreamList(final List videoStreams, + final boolean ascendingOrder) { final Comparator comparator = ListHelper::compareVideoStreamResolution; Collections.sort(videoStreams, ascendingOrder ? comparator : comparator.reversed()); + return videoStreams; } /** @@ -277,28 +288,12 @@ private static void sortStreamList(final List videoStreams, * @param audioStreams List of audio streams * @return Index of audio stream that produces the most compact results or -1 if not found */ - static int getHighestQualityAudioIndex(@Nullable MediaFormat format, - final List audioStreams) { - int result = -1; - if (audioStreams != null) { - while (result == -1) { - AudioStream prevStream = null; - for (int idx = 0; idx < audioStreams.size(); idx++) { - final AudioStream stream = audioStreams.get(idx); - if ((format == null || stream.getFormat() == format) - && (prevStream == null || compareAudioStreamBitrate(prevStream, stream, - AUDIO_FORMAT_QUALITY_RANKING) < 0)) { - prevStream = stream; - result = idx; - } - } - if (result == -1 && format == null) { - break; - } - format = null; - } - } - return result; + static int getHighestQualityAudioIndex(@Nullable final MediaFormat format, + @Nullable final List audioStreams) { + return getAudioIndexByHighestRank(format, audioStreams, + // Compares descending (last = highest rank) + (s1, s2) -> compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_QUALITY_RANKING) + ); } /** @@ -309,28 +304,47 @@ static int getHighestQualityAudioIndex(@Nullable MediaFormat format, * @param audioStreams List of audio streams * @return Index of audio stream that produces the most compact results or -1 if not found */ - static int getMostCompactAudioIndex(@Nullable MediaFormat format, - final List audioStreams) { - int result = -1; - if (audioStreams != null) { - while (result == -1) { - AudioStream prevStream = null; - for (int idx = 0; idx < audioStreams.size(); idx++) { - final AudioStream stream = audioStreams.get(idx); - if ((format == null || stream.getFormat() == format) - && (prevStream == null || compareAudioStreamBitrate(prevStream, stream, - AUDIO_FORMAT_EFFICIENCY_RANKING) > 0)) { - prevStream = stream; - result = idx; - } - } - if (result == -1 && format == null) { - break; - } - format = null; + static int getMostCompactAudioIndex(@Nullable final MediaFormat format, + @Nullable final List audioStreams) { + + return getAudioIndexByHighestRank(format, audioStreams, + // The "-" is important -> Compares ascending (first = highest rank) + (s1, s2) -> -compareAudioStreamBitrate(s1, s2, AUDIO_FORMAT_EFFICIENCY_RANKING) + ); + } + + /** + * Get the audio-stream from the list with the highest rank, depending on the comparator. + * Format will be ignored if it yields no results. + * + * @param targetedFormat The target format type or null if it doesn't matter + * @param audioStreams List of audio streams + * @param comparator The comparator used for determining the max/best/highest ranked value + * @return Index of audio stream that produces the highest ranked result or -1 if not found + */ + private static int getAudioIndexByHighestRank(@Nullable final MediaFormat targetedFormat, + @Nullable final List audioStreams, + final Comparator comparator) { + if (audioStreams == null || audioStreams.isEmpty()) { + return -1; + } + + final AudioStream highestRankedAudioStream = audioStreams.stream() + .filter(audioStream -> targetedFormat == null + || audioStream.getFormat() == targetedFormat) + .max(comparator) + .orElse(null); + + if (highestRankedAudioStream == null) { + // Fallback: Ignore targetedFormat if not null + if (targetedFormat != null) { + return getAudioIndexByHighestRank(null, audioStreams, comparator); } + // targetedFormat is already null -> return -1 + return -1; } - return result; + + return audioStreams.indexOf(highestRankedAudioStream); } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 80d1b25aea2..e55114a2dd4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -35,6 +35,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; @@ -214,7 +215,8 @@ public static void enqueueNextOnPlayer(final Context context, final PlayQueue qu // External Players //////////////////////////////////////////////////////////////////////////*/ - public static void playOnExternalAudioPlayer(final Context context, final StreamInfo info) { + public static void playOnExternalAudioPlayer(@NonNull final Context context, + @NonNull final StreamInfo info) { final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); if (index == -1) { @@ -226,9 +228,11 @@ public static void playOnExternalAudioPlayer(final Context context, final Stream playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream); } - public static void playOnExternalVideoPlayer(final Context context, final StreamInfo info) { + public static void playOnExternalVideoPlayer(@NonNull final Context context, + @NonNull final StreamInfo info) { final ArrayList videoStreamsList = new ArrayList<>( - ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false)); + ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false, + false)); final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList); if (index == -1) { @@ -240,8 +244,10 @@ public static void playOnExternalVideoPlayer(final Context context, final Stream playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream); } - public static void playOnExternalPlayer(final Context context, final String name, - final String artist, final Stream stream) { + public static void playOnExternalPlayer(@NonNull final Context context, + @Nullable final String name, + @Nullable final String artist, + @NonNull final Stream stream) { final Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType()); @@ -253,7 +259,8 @@ public static void playOnExternalPlayer(final Context context, final String name resolveActivityOrAskToInstall(context, intent); } - public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) { + public static void resolveActivityOrAskToInstall(@NonNull final Context context, + @NonNull final Intent intent) { if (intent.resolveActivity(context.getPackageManager()) != null) { ShareUtils.openIntentInApp(context, intent, false); } else { @@ -402,6 +409,15 @@ public static void openChannelFragment(final FragmentManager fragmentManager, .commit(); } + public static void openChannelFragment(@NonNull final Fragment fragment, + @NonNull final StreamInfoItem item, + final String uploaderUrl) { + // For some reason `getParentFragmentManager()` doesn't work, but this does. + openChannelFragment( + fragment.requireActivity().getSupportFragmentManager(), + item.getServiceId(), uploaderUrl, item.getUploaderName()); + } + public static void openPlaylistFragment(final FragmentManager fragmentManager, final int serviceId, final String url, @NonNull final String name) { diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt new file mode 100644 index 00000000000..21a9059e289 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt @@ -0,0 +1,116 @@ +package org.schabi.newpipe.util + +import android.content.pm.PackageManager +import android.content.pm.Signature +import androidx.core.content.pm.PackageInfoCompat +import org.schabi.newpipe.App +import org.schabi.newpipe.error.ErrorInfo +import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification +import org.schabi.newpipe.error.UserAction +import java.io.ByteArrayInputStream +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.time.Instant +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +object ReleaseVersionUtil { + // Public key of the certificate that is used in NewPipe release versions + private const val 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" + + @JvmStatic + fun isReleaseApk(): Boolean { + return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1 + } + + /** + * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133. + * + * @return String with the APK's SHA1 fingerprint in hexadecimal + */ + private val certificateSHA1Fingerprint: String + get() { + val app = App.getApp() + val signatures: List = try { + PackageInfoCompat.getSignatures(app.packageManager, app.packageName) + } catch (e: PackageManager.NameNotFoundException) { + showRequestError(app, e, "Could not find package info") + return "" + } + if (signatures.isEmpty()) { + return "" + } + val x509cert = try { + val cert = signatures[0].toByteArray() + val input: InputStream = ByteArrayInputStream(cert) + val cf = CertificateFactory.getInstance("X509") + cf.generateCertificate(input) as X509Certificate + } catch (e: CertificateException) { + showRequestError(app, e, "Certificate error") + return "" + } + + return try { + val md = MessageDigest.getInstance("SHA1") + val publicKey = md.digest(x509cert.encoded) + byte2HexFormatted(publicKey) + } catch (e: NoSuchAlgorithmException) { + showRequestError(app, e, "Could not retrieve SHA1 key") + "" + } catch (e: CertificateEncodingException) { + showRequestError(app, e, "Could not retrieve SHA1 key") + "" + } + } + + private fun byte2HexFormatted(arr: ByteArray): String { + val str = StringBuilder(arr.size * 2) + for (i in arr.indices) { + var h = Integer.toHexString(arr[i].toInt()) + val l = h.length + if (l == 1) { + h = "0$h" + } + if (l > 2) { + h = h.substring(l - 2, l) + } + str.append(h.uppercase()) + if (i < arr.size - 1) { + str.append(':') + } + } + return str.toString() + } + + private fun showRequestError(app: App, e: Exception, request: String) { + createNotification( + app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request) + ) + } + + fun isLastUpdateCheckExpired(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 coerceUpdateCheckExpiry(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/util/SaveUploaderUrlHelper.java b/app/src/main/java/org/schabi/newpipe/util/SaveUploaderUrlHelper.java deleted file mode 100644 index 3c7b1ce9127..00000000000 --- a/app/src/main/java/org/schabi/newpipe/util/SaveUploaderUrlHelper.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.schabi.newpipe.util; - -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; - -import android.content.Context; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import org.schabi.newpipe.NewPipeDatabase; -import org.schabi.newpipe.R; -import org.schabi.newpipe.error.ErrorInfo; -import org.schabi.newpipe.error.ErrorUtil; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.schedulers.Schedulers; - -/** - * Utility class for putting the uploader url into the database - when required. - */ -public final class SaveUploaderUrlHelper { - private SaveUploaderUrlHelper() { - } - - // Public functions which call the function that does - // the actual work with the correct parameters - public static void saveUploaderUrlIfNeeded(@NonNull final Fragment fragment, - @NonNull final StreamInfoItem infoItem, - @NonNull final SaveUploaderUrlCallback callback) { - saveUploaderUrlIfNeeded(fragment.requireContext(), - infoItem.getServiceId(), - infoItem.getUrl(), - infoItem.getUploaderUrl(), - callback); - } - public static void saveUploaderUrlIfNeeded(@NonNull final Context context, - @NonNull final PlayQueueItem queueItem, - @NonNull final SaveUploaderUrlCallback callback) { - saveUploaderUrlIfNeeded(context, - queueItem.getServiceId(), - queueItem.getUrl(), - queueItem.getUploaderUrl(), - callback); - } - - /** - * Fetches and saves the uploaderUrl if it is empty (meaning that it does - * not exist in the video item). The callback is called with either the - * fetched uploaderUrl, or the already saved uploaderUrl, but it is always - * called with a valid uploaderUrl that can be used to show channel details. - * - * @param context Context - * @param serviceId The serviceId of the item - * @param url The item url - * @param uploaderUrl The uploaderUrl of the item, if null or empty, it - * will be fetched using the item url. - * @param callback The callback that returns the fetched or existing - * uploaderUrl - */ - private static void saveUploaderUrlIfNeeded(@NonNull final Context context, - final int serviceId, - @NonNull final String url, - // Only used if not null or empty - @Nullable final String uploaderUrl, - @NonNull final SaveUploaderUrlCallback callback) { - if (isNullOrEmpty(uploaderUrl)) { - Toast.makeText(context, R.string.loading_channel_details, - Toast.LENGTH_SHORT).show(); - ExtractorHelper.getStreamInfo(serviceId, url, false) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - NewPipeDatabase.getInstance(context).streamDAO() - .setUploaderUrl(serviceId, url, result.getUploaderUrl()) - .subscribeOn(Schedulers.io()).subscribe(); - callback.onCallback(result.getUploaderUrl()); - }, throwable -> ErrorUtil.createNotification(context, - new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL, - "Could not load channel details") - )); - } else { - callback.onCallback(uploaderUrl); - } - } - - public interface SaveUploaderUrlCallback { - void onCallback(@NonNull String uploaderUrl); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java b/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java index 9d97e013a22..b4c196ce4fa 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/SerializedCache.java @@ -19,7 +19,7 @@ public final class SerializedCache { private static final boolean DEBUG = MainActivity.DEBUG; private static final SerializedCache INSTANCE = new SerializedCache(); private static final int MAX_ITEMS_ON_CACHE = 5; - private static final LruCache LRU_CACHE = + private static final LruCache> LRU_CACHE = new LruCache<>(MAX_ITEMS_ON_CACHE); private static final String TAG = "SerializedCache"; @@ -47,7 +47,7 @@ public T get(@NonNull final String key, @NonNull final Class type) { Log.d(TAG, "get() called with: key = [" + key + "]"); } synchronized (LRU_CACHE) { - final CacheData data = LRU_CACHE.get(key); + final CacheData data = LRU_CACHE.get(key); return data != null ? getItem(data, type) : null; } } @@ -91,7 +91,7 @@ public long size() { } @Nullable - private T getItem(@NonNull final CacheData data, @NonNull final Class type) { + private T getItem(@NonNull final CacheData data, @NonNull final Class type) { return type.isAssignableFrom(data.type) ? type.cast(data.item) : null; } diff --git a/app/src/main/java/org/schabi/newpipe/util/SimpleOnSeekBarChangeListener.kt b/app/src/main/java/org/schabi/newpipe/util/SimpleOnSeekBarChangeListener.kt new file mode 100644 index 00000000000..a79085fc01c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SimpleOnSeekBarChangeListener.kt @@ -0,0 +1,12 @@ +package org.schabi.newpipe.util + +import android.widget.SeekBar + +/** + * Why the hell didn't they make a stub implementation for this? + */ +abstract class SimpleOnSeekBarChangeListener : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) {} +} diff --git a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java new file mode 100644 index 00000000000..5daf7f0736c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java @@ -0,0 +1,128 @@ +package org.schabi.newpipe.util; + +import static org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM; +import static org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + +import android.content.Context; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.player.playqueue.SinglePlayQueue; + +import java.util.function.Consumer; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Utility class for fetching additional data for stream items when needed. + */ +public final class SparseItemUtil { + private SparseItemUtil() { + } + + /** + * Use this to certainly obtain an single play queue with all of the data filled in when the + * stream info item you are handling might be sparse, e.g. because it was fetched via a {@link + * org.schabi.newpipe.extractor.feed.FeedExtractor}. FeedExtractors provide a fast and + * lightweight method to fetch info, but the info might be incomplete (see + * {@link org.schabi.newpipe.local.feed.service.FeedLoadService} for more details). + * + * @param context Android context + * @param item item which is checked and eventually loaded completely + * @param callback callback to call with the single play queue built from the original item if + * all info was available, otherwise from the fetched {@link + * org.schabi.newpipe.extractor.stream.StreamInfo} + */ + public static void fetchItemInfoIfSparse(@NonNull final Context context, + @NonNull final StreamInfoItem item, + @NonNull final Consumer callback) { + if (((item.getStreamType() == LIVE_STREAM || item.getStreamType() == AUDIO_LIVE_STREAM) + || item.getDuration() >= 0) && !isNullOrEmpty(item.getUploaderUrl())) { + // if the duration is >= 0 (provided that the item is not a livestream) and there is an + // uploader url, probably all info is already there, so there is no need to fetch it + callback.accept(new SinglePlayQueue(item)); + } + + // either the duration or the uploader url are not available, so fetch more info + fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(), + streamInfo -> callback.accept(new SinglePlayQueue(streamInfo))); + } + + /** + * Use this to certainly obtain an uploader url when the stream info item or play queue item you + * are handling might not have the uploader url (e.g. because it was fetched with {@link + * org.schabi.newpipe.extractor.feed.FeedExtractor}). A toast is shown if loading details is + * required. + * + * @param context Android context + * @param serviceId serviceId of the item + * @param url item url + * @param uploaderUrl uploaderUrl of the item; if null or empty will be fetched + * @param callback callback to be called with either the original uploaderUrl, if it was a + * valid url, otherwise with the uploader url obtained by fetching the {@link + * org.schabi.newpipe.extractor.stream.StreamInfo} corresponding to the item + */ + public static void fetchUploaderUrlIfSparse(@NonNull final Context context, + final int serviceId, + @NonNull final String url, + @Nullable final String uploaderUrl, + @NonNull final Consumer callback) { + if (isNullOrEmpty(uploaderUrl)) { + fetchStreamInfoAndSaveToDatabase(context, serviceId, url, + streamInfo -> callback.accept(streamInfo.getUploaderUrl())); + } else { + callback.accept(uploaderUrl); + } + } + + /** + * Loads the stream info corresponding to the given data on an I/O thread, stores the result in + * the database and calls the callback on the main thread with the result. A toast will be shown + * to the user about loading stream details, so this needs to be called on the main thread. + * + * @param context Android context + * @param serviceId service id of the stream to load + * @param url url of the stream to load + * @param callback callback to be called with the result + */ + private static void fetchStreamInfoAndSaveToDatabase(@NonNull final Context context, + final int serviceId, + @NonNull final String url, + final Consumer callback) { + Toast.makeText(context, R.string.loading_stream_details, Toast.LENGTH_SHORT).show(); + ExtractorHelper.getStreamInfo(serviceId, url, false) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + // save to database in the background (not on main thread) + Completable.fromAction(() -> NewPipeDatabase.getInstance(context) + .streamDAO().upsert(new StreamEntity(result))) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .doOnError(throwable -> + ErrorUtil.createNotification(context, + new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, + "Saving stream info to database", result))) + .subscribe(); + + // call callback on main thread with the obtained result + callback.accept(result); + }, throwable -> ErrorUtil.createNotification(context, + new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, + "Loading stream info: " + url, serviceId) + )); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java deleted file mode 100644 index 1b4c8046c54..00000000000 --- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java +++ /dev/null @@ -1,238 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -import androidx.fragment.app.Fragment; -import androidx.preference.PreferenceManager; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; -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.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.util.external_communication.KoreUtils; -import org.schabi.newpipe.util.external_communication.ShareUtils; - -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public enum StreamDialogEntry { - ////////////////////////////////////// - // enum values with DEFAULT actions // - ////////////////////////////////////// - - show_channel_details(R.string.show_channel_details, (fragment, item) -> { - SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(fragment, item, - uploaderUrl -> openChannelFragment(fragment, item, uploaderUrl)); - }), - - /** - * Enqueues the stream automatically to the current PlayerType.
- *
- * Info: Add this entry within showStreamDialog. - */ - enqueue(R.string.enqueue_stream, (fragment, item) -> { - fetchItemInfoIfSparse(fragment, item, fullItem -> - NavigationHelper.enqueueOnPlayer(fragment.getContext(), fullItem)); - }), - - enqueue_next(R.string.enqueue_next_stream, (fragment, item) -> { - fetchItemInfoIfSparse(fragment, item, fullItem -> - NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), fullItem)); - }), - - start_here_on_background(R.string.start_here_on_background, (fragment, item) -> { - fetchItemInfoIfSparse(fragment, item, fullItem -> - NavigationHelper.playOnBackgroundPlayer(fragment.getContext(), fullItem, true)); - }), - - start_here_on_popup(R.string.start_here_on_popup, (fragment, item) -> { - fetchItemInfoIfSparse(fragment, item, fullItem -> - NavigationHelper.playOnPopupPlayer(fragment.getContext(), fullItem, true)); - }), - - set_as_playlist_thumbnail(R.string.set_as_playlist_thumbnail, (fragment, item) -> { - }), // has to be set manually - - delete(R.string.delete, (fragment, item) -> { - }), // has to be set manually - - 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(); - }); - - /////////////// - // variables // - /////////////// - - private static StreamDialogEntry[] enabledEntries; - private final int resource; - private final StreamDialogEntryAction defaultAction; - private StreamDialogEntryAction customAction; - - StreamDialogEntry(final int resource, final StreamDialogEntryAction defaultAction) { - this.resource = resource; - this.defaultAction = defaultAction; - this.customAction = null; - } - - - /////////////////////////////////////////////////////// - // non-static methods to initialize and edit entries // - /////////////////////////////////////////////////////// - - public static void setEnabledEntries(final List entries) { - setEnabledEntries(entries.toArray(new StreamDialogEntry[0])); - } - - /** - * To be called before using {@link #setCustomAction(StreamDialogEntryAction)}. - * - * @param entries the entries to be enabled - */ - public static void setEnabledEntries(final StreamDialogEntry... entries) { - // cleanup from last time StreamDialogEntry was used - for (final StreamDialogEntry streamDialogEntry : values()) { - streamDialogEntry.customAction = null; - } - - enabledEntries = entries; - } - - public static String[] getCommands(final Context context) { - final String[] commands = new String[enabledEntries.length]; - for (int i = 0; i != enabledEntries.length; ++i) { - commands[i] = context.getResources().getString(enabledEntries[i].resource); - } - - return commands; - } - - - //////////////////////////////////////////////// - // static methods that act on enabled entries // - //////////////////////////////////////////////// - - public static void clickOn(final int which, final Fragment fragment, - final StreamInfoItem infoItem) { - if (enabledEntries[which].customAction == null) { - enabledEntries[which].defaultAction.onClick(fragment, infoItem); - } else { - enabledEntries[which].customAction.onClick(fragment, infoItem); - } - } - - /** - * Can be used after {@link #setEnabledEntries(StreamDialogEntry...)} has been called. - * - * @param action the action to be set - */ - public void setCustomAction(final StreamDialogEntryAction action) { - this.customAction = action; - } - - public interface StreamDialogEntryAction { - void onClick(Fragment fragment, StreamInfoItem infoItem); - } - - public static boolean shouldAddMarkAsWatched(final StreamType streamType, - final Context context) { - final boolean isWatchHistoryEnabled = PreferenceManager - .getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.enable_watch_history_key), false); - return streamType != StreamType.AUDIO_LIVE_STREAM - && streamType != StreamType.LIVE_STREAM - && isWatchHistoryEnabled; - } - - ///////////////////////////////////////////// - // private method to open channel fragment // - ///////////////////////////////////////////// - - private static void openChannelFragment(final Fragment fragment, - final StreamInfoItem item, - final String uploaderUrl) { - // For some reason `getParentFragmentManager()` doesn't work, but this does. - NavigationHelper.openChannelFragment( - fragment.requireActivity().getSupportFragmentManager(), - item.getServiceId(), uploaderUrl, item.getUploaderName()); - } - - ///////////////////////////////////////////// - // helper functions // - ///////////////////////////////////////////// - - private static void fetchItemInfoIfSparse(final Fragment fragment, - final StreamInfoItem item, - final Consumer callback) { - if (!(item.getStreamType() == StreamType.LIVE_STREAM - || item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) - && item.getDuration() < 0) { - // Sparse item: fetched by fast fetch - ExtractorHelper.getStreamInfo( - item.getServiceId(), - item.getUrl(), - false - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - final HistoryRecordManager recordManager = - new HistoryRecordManager(fragment.getContext()); - recordManager.saveStreamState(result, 0) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError(throwable -> Log.e("StreamDialogEntry", - throwable.toString())) - .subscribe(); - - callback.accept(new SinglePlayQueue(result)); - }, throwable -> Log.e("StreamDialogEntry", throwable.toString())); - } else { - callback.accept(new SinglePlayQueue(item)); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 9150b5c1a69..c89da9d459d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -12,6 +12,7 @@ import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.SubtitlesStream; @@ -137,7 +138,7 @@ private View getCustomView(final int position, final View view, final ViewGroup } if (streamsWrapper.getSizeInBytes(position) > 0) { - final SecondaryStreamHelper secondary = secondaryStreams == null ? null + final SecondaryStreamHelper secondary = secondaryStreams == null ? null : secondaryStreams.get(position); if (secondary != null) { final long size @@ -153,16 +154,11 @@ private View getCustomView(final int position, final View view, final ViewGroup if (stream instanceof SubtitlesStream) { formatNameView.setText(((SubtitlesStream) stream).getLanguageTag()); + } else if (stream.getFormat() == MediaFormat.WEBMA_OPUS) { + // noinspection AndroidLintSetTextI18n + formatNameView.setText("opus"); } else { - switch (stream.getFormat()) { - case WEBMA_OPUS: - // noinspection AndroidLintSetTextI18n - formatNameView.setText("opus"); - break; - default: - formatNameView.setText(stream.getFormat().getName()); - break; - } + formatNameView.setText(stream.getFormat().getName()); } qualityView.setText(qualityString); diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java index 6801f24ef2d..0df579d8814 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java @@ -10,6 +10,10 @@ import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.util.NavigationHelper; +/** + * Util class that provides methods which are related to the Kodi Media Center and its Kore app. + * @see Kodi website + */ public final class KoreUtils { private KoreUtils() { } diff --git a/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java b/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java deleted file mode 100644 index 1219304e1d0..00000000000 --- a/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright 2019 Alexander Rvachev - * FocusOverlayView.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.schabi.newpipe.views; - -import android.graphics.Rect; -import android.text.Layout; -import android.text.Selection; -import android.text.Spannable; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.widget.TextView; - -public class LargeTextMovementMethod extends LinkMovementMethod { - private final Rect visibleRect = new Rect(); - - private int direction; - - @Override - public void onTakeFocus(final TextView view, final Spannable text, final int dir) { - Selection.removeSelection(text); - - super.onTakeFocus(view, text, dir); - - this.direction = dirToRelative(dir); - } - - @Override - protected boolean handleMovementKey(final TextView widget, - final Spannable buffer, - final int keyCode, - final int movementMetaState, - final KeyEvent event) { - if (!doHandleMovement(widget, buffer, keyCode, movementMetaState, event)) { - // clear selection to make sure, that it does not confuse focus handling code - Selection.removeSelection(buffer); - return false; - } - - return true; - } - - private boolean doHandleMovement(final TextView widget, - final Spannable buffer, - final int keyCode, - final int movementMetaState, - final KeyEvent event) { - final int newDir = keyToDir(keyCode); - - if (direction != 0 && newDir != direction) { - return false; - } - - this.direction = 0; - - final ViewGroup root = findScrollableParent(widget); - - widget.getHitRect(visibleRect); - - root.offsetDescendantRectToMyCoords((View) widget.getParent(), visibleRect); - - return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); - } - - @Override - protected boolean up(final TextView widget, final Spannable buffer) { - if (gotoPrev(widget, buffer)) { - return true; - } - - return super.up(widget, buffer); - } - - @Override - protected boolean left(final TextView widget, final Spannable buffer) { - if (gotoPrev(widget, buffer)) { - return true; - } - - return super.left(widget, buffer); - } - - @Override - protected boolean right(final TextView widget, final Spannable buffer) { - if (gotoNext(widget, buffer)) { - return true; - } - - return super.right(widget, buffer); - } - - @Override - protected boolean down(final TextView widget, final Spannable buffer) { - if (gotoNext(widget, buffer)) { - return true; - } - - return super.down(widget, buffer); - } - - private boolean gotoPrev(final TextView view, final Spannable buffer) { - final Layout layout = view.getLayout(); - if (layout == null) { - return false; - } - - final View root = findScrollableParent(view); - - final int rootHeight = root.getHeight(); - - if (visibleRect.top >= 0) { - // we fit entirely into the viewport, no need for fancy footwork - return false; - } - - final int topExtra = -visibleRect.top; - - final int firstVisibleLineNumber = layout.getLineForVertical(topExtra); - - // when deciding whether to pass "focus" to span, account for one more line - // this ensures, that focus is never passed to spans partially outside scroll window - final int visibleStart = firstVisibleLineNumber == 0 - ? 0 - : layout.getLineStart(firstVisibleLineNumber - 1); - - final ClickableSpan[] candidates = buffer.getSpans( - visibleStart, buffer.length(), ClickableSpan.class); - - if (candidates.length != 0) { - final int a = Selection.getSelectionStart(buffer); - final int b = Selection.getSelectionEnd(buffer); - - final int selStart = Math.min(a, b); - final int selEnd = Math.max(a, b); - - int bestStart = -1; - int bestEnd = -1; - - for (final ClickableSpan candidate : candidates) { - final int start = buffer.getSpanStart(candidate); - final int end = buffer.getSpanEnd(candidate); - - if ((end < selEnd || selStart == selEnd) && start >= visibleStart) { - if (end > bestEnd) { - bestStart = buffer.getSpanStart(candidate); - bestEnd = end; - } - } - } - - if (bestStart >= 0) { - Selection.setSelection(buffer, bestEnd, bestStart); - return true; - } - } - - final float fourLines = view.getTextSize() * 4; - - visibleRect.left = 0; - visibleRect.right = view.getWidth(); - visibleRect.top = Math.max(0, (int) (topExtra - fourLines)); - visibleRect.bottom = visibleRect.top + rootHeight; - - return view.requestRectangleOnScreen(visibleRect); - } - - private boolean gotoNext(final TextView view, final Spannable buffer) { - final Layout layout = view.getLayout(); - if (layout == null) { - return false; - } - - final View root = findScrollableParent(view); - - final int rootHeight = root.getHeight(); - - if (visibleRect.bottom <= rootHeight) { - // we fit entirely into the viewport, no need for fancy footwork - return false; - } - - final int bottomExtra = visibleRect.bottom - rootHeight; - - final int visibleBottomBorder = view.getHeight() - bottomExtra; - - final int lineCount = layout.getLineCount(); - - final int lastVisibleLineNumber = layout.getLineForVertical(visibleBottomBorder); - - // when deciding whether to pass "focus" to span, account for one more line - // this ensures, that focus is never passed to spans partially outside scroll window - final int visibleEnd = lastVisibleLineNumber == lineCount - 1 - ? buffer.length() - : layout.getLineEnd(lastVisibleLineNumber - 1); - - final ClickableSpan[] candidates = buffer.getSpans(0, visibleEnd, ClickableSpan.class); - - if (candidates.length != 0) { - final int a = Selection.getSelectionStart(buffer); - final int b = Selection.getSelectionEnd(buffer); - - final int selStart = Math.min(a, b); - final int selEnd = Math.max(a, b); - - int bestStart = Integer.MAX_VALUE; - int bestEnd = Integer.MAX_VALUE; - - for (final ClickableSpan candidate : candidates) { - final int start = buffer.getSpanStart(candidate); - final int end = buffer.getSpanEnd(candidate); - - if ((start > selStart || selStart == selEnd) && end <= visibleEnd) { - if (start < bestStart) { - bestStart = start; - bestEnd = buffer.getSpanEnd(candidate); - } - } - } - - if (bestEnd < Integer.MAX_VALUE) { - // cool, we have managed to find next link without having to adjust self within view - Selection.setSelection(buffer, bestStart, bestEnd); - return true; - } - } - - // there are no links within visible area, but still some text past visible area - // scroll visible area further in required direction - final float fourLines = view.getTextSize() * 4; - - visibleRect.left = 0; - visibleRect.right = view.getWidth(); - visibleRect.bottom = Math.min((int) (visibleBottomBorder + fourLines), view.getHeight()); - visibleRect.top = visibleRect.bottom - rootHeight; - - return view.requestRectangleOnScreen(visibleRect); - } - - private ViewGroup findScrollableParent(final View view) { - View current = view; - - ViewParent parent; - do { - parent = current.getParent(); - - if (parent == current || !(parent instanceof View)) { - return (ViewGroup) view.getRootView(); - } - - current = (View) parent; - - if (current.isScrollContainer()) { - return (ViewGroup) current; - } - } - while (true); - } - - private static int dirToRelative(final int dir) { - switch (dir) { - case View.FOCUS_DOWN: - case View.FOCUS_RIGHT: - return View.FOCUS_FORWARD; - case View.FOCUS_UP: - case View.FOCUS_LEFT: - return View.FOCUS_BACKWARD; - } - - return dir; - } - - private int keyToDir(final int keyCode) { - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - return View.FOCUS_BACKWARD; - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_RIGHT: - return View.FOCUS_FORWARD; - } - - return View.FOCUS_FORWARD; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt index d209d24da4f..8472653fb97 100644 --- a/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt +++ b/app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt @@ -1,11 +1,11 @@ package org.schabi.newpipe.views.player -import android.animation.Animator import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout +import androidx.core.animation.addListener import org.schabi.newpipe.R import org.schabi.newpipe.databinding.PlayerFastSeekSecondsViewBinding import org.schabi.newpipe.util.DeviceUtils @@ -163,19 +163,10 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context setFloatValues(0f, 1f) addUpdateListener { update(it.animatedValue as Float) } - addListener(object : AnimatorListener { - override fun onAnimationStart(animation: Animator?) { - start() - } - - override fun onAnimationEnd(animation: Animator?) { - end() - } - - override fun onAnimationCancel(animation: Animator?) = Unit - - override fun onAnimationRepeat(animation: Animator?) = Unit - }) + addListener( + onStart = { start() }, + onEnd = { end() } + ) } } } diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index dda2d6deeb4..b5fc0297cf8 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -192,14 +192,7 @@ public boolean onOptionsItemSelected(MenuItem item) { updateList(); return true; case R.id.clear_list: - AlertDialog.Builder prompt = new AlertDialog.Builder(mContext); - prompt.setTitle(R.string.clear_download_history); - prompt.setMessage(R.string.confirm_prompt); - // Intentionally misusing button's purpose in order to achieve good order - prompt.setNegativeButton(R.string.clear_download_history, (dialog, which) -> mAdapter.clearFinishedDownloads(false)); - prompt.setPositiveButton(R.string.delete_downloaded_files, (dialog, which) -> mAdapter.clearFinishedDownloads(true)); - prompt.setNeutralButton(R.string.cancel, null); - prompt.create().show(); + showClearDownloadHistoryPrompt(); return true; case R.id.start_downloads: mBinder.getDownloadManager().startAllMissions(); @@ -212,6 +205,32 @@ public boolean onOptionsItemSelected(MenuItem item) { } } + public void showClearDownloadHistoryPrompt() { + // ask the user whether he wants to just clear history or instead delete files on disk + new AlertDialog.Builder(mContext) + .setTitle(R.string.clear_download_history) + .setMessage(R.string.confirm_prompt) + // Intentionally misusing buttons' purpose in order to achieve good order + .setNegativeButton(R.string.clear_download_history, + (dialog, which) -> mAdapter.clearFinishedDownloads(false)) + .setNeutralButton(R.string.cancel, null) + .setPositiveButton(R.string.delete_downloaded_files, + (dialog, which) -> showDeleteDownloadedFilesConfirmationPrompt()) + .create() + .show(); + } + + public void showDeleteDownloadedFilesConfirmationPrompt() { + // make sure the user confirms once more before deleting files on disk + new AlertDialog.Builder(mContext) + .setTitle(R.string.delete_downloaded_files_confirm) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok, + (dialog, which) -> mAdapter.clearFinishedDownloads(true)) + .create() + .show(); + } + private void updateList() { if (mLinear) { mList.setLayoutManager(mLinearManager); diff --git a/app/src/main/res/drawable-night/ic_add.xml b/app/src/main/res/drawable-night/ic_add.xml deleted file mode 100644 index bbda803b053..00000000000 --- a/app/src/main/res/drawable-night/ic_add.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_add_circle_outline.xml b/app/src/main/res/drawable-night/ic_add_circle_outline.xml deleted file mode 100644 index 2f2cfe3e3d9..00000000000 --- a/app/src/main/res/drawable-night/ic_add_circle_outline.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_apps.xml b/app/src/main/res/drawable-night/ic_apps.xml deleted file mode 100644 index 2d7d796f7cf..00000000000 --- a/app/src/main/res/drawable-night/ic_apps.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_arrow_back.xml b/app/src/main/res/drawable-night/ic_arrow_back.xml deleted file mode 100644 index b7c7287839b..00000000000 --- a/app/src/main/res/drawable-night/ic_arrow_back.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_asterisk.xml b/app/src/main/res/drawable-night/ic_asterisk.xml deleted file mode 100644 index c66bb405121..00000000000 --- a/app/src/main/res/drawable-night/ic_asterisk.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_attach_money.xml b/app/src/main/res/drawable-night/ic_attach_money.xml deleted file mode 100644 index fcc1ab160c0..00000000000 --- a/app/src/main/res/drawable-night/ic_attach_money.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_backup.xml b/app/src/main/res/drawable-night/ic_backup.xml deleted file mode 100644 index 29259b0e03a..00000000000 --- a/app/src/main/res/drawable-night/ic_backup.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_bookmark.xml b/app/src/main/res/drawable-night/ic_bookmark.xml deleted file mode 100644 index 2e919f18d06..00000000000 --- a/app/src/main/res/drawable-night/ic_bookmark.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_bug_report.xml b/app/src/main/res/drawable-night/ic_bug_report.xml deleted file mode 100644 index e1a204a29c6..00000000000 --- a/app/src/main/res/drawable-night/ic_bug_report.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_campaign.xml b/app/src/main/res/drawable-night/ic_campaign.xml deleted file mode 100644 index eabaddaee95..00000000000 --- a/app/src/main/res/drawable-night/ic_campaign.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_cast.xml b/app/src/main/res/drawable-night/ic_cast.xml deleted file mode 100644 index 61a1f61fe6f..00000000000 --- a/app/src/main/res/drawable-night/ic_cast.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_child_care.xml b/app/src/main/res/drawable-night/ic_child_care.xml deleted file mode 100644 index 9375e3116ea..00000000000 --- a/app/src/main/res/drawable-night/ic_child_care.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable-night/ic_close.xml b/app/src/main/res/drawable-night/ic_close.xml deleted file mode 100644 index c63eeb59765..00000000000 --- a/app/src/main/res/drawable-night/ic_close.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_cloud_download.xml b/app/src/main/res/drawable-night/ic_cloud_download.xml deleted file mode 100644 index 67e87045662..00000000000 --- a/app/src/main/res/drawable-night/ic_cloud_download.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_computer.xml b/app/src/main/res/drawable-night/ic_computer.xml deleted file mode 100644 index 68f85594d75..00000000000 --- a/app/src/main/res/drawable-night/ic_computer.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_crop_portrait.xml b/app/src/main/res/drawable-night/ic_crop_portrait.xml deleted file mode 100644 index fc11eba5725..00000000000 --- a/app/src/main/res/drawable-night/ic_crop_portrait.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_delete.xml b/app/src/main/res/drawable-night/ic_delete.xml deleted file mode 100644 index 3760de238c8..00000000000 --- a/app/src/main/res/drawable-night/ic_delete.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_directions_bike.xml b/app/src/main/res/drawable-night/ic_directions_bike.xml deleted file mode 100644 index 90c7f7a775e..00000000000 --- a/app/src/main/res/drawable-night/ic_directions_bike.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_directions_car.xml b/app/src/main/res/drawable-night/ic_directions_car.xml deleted file mode 100644 index 26404bddbf2..00000000000 --- a/app/src/main/res/drawable-night/ic_directions_car.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_done.xml b/app/src/main/res/drawable-night/ic_done.xml deleted file mode 100644 index bb657f6ec0d..00000000000 --- a/app/src/main/res/drawable-night/ic_done.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_drag_handle.xml b/app/src/main/res/drawable-night/ic_drag_handle.xml deleted file mode 100644 index a6d3b527082..00000000000 --- a/app/src/main/res/drawable-night/ic_drag_handle.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_expand_more.xml b/app/src/main/res/drawable-night/ic_expand_more.xml deleted file mode 100644 index b6a470043f3..00000000000 --- a/app/src/main/res/drawable-night/ic_expand_more.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_explore.xml b/app/src/main/res/drawable-night/ic_explore.xml deleted file mode 100644 index a910c54297d..00000000000 --- a/app/src/main/res/drawable-night/ic_explore.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_fastfood.xml b/app/src/main/res/drawable-night/ic_fastfood.xml deleted file mode 100644 index ddb9b6257e1..00000000000 --- a/app/src/main/res/drawable-night/ic_fastfood.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_favorite.xml b/app/src/main/res/drawable-night/ic_favorite.xml deleted file mode 100644 index efc717ee9ad..00000000000 --- a/app/src/main/res/drawable-night/ic_favorite.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_file_download.xml b/app/src/main/res/drawable-night/ic_file_download.xml deleted file mode 100644 index 97bdac0f1e6..00000000000 --- a/app/src/main/res/drawable-night/ic_file_download.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_filter_list.xml b/app/src/main/res/drawable-night/ic_filter_list.xml deleted file mode 100644 index 2df495e1549..00000000000 --- a/app/src/main/res/drawable-night/ic_filter_list.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_fitness_center.xml b/app/src/main/res/drawable-night/ic_fitness_center.xml deleted file mode 100644 index 892def49124..00000000000 --- a/app/src/main/res/drawable-night/ic_fitness_center.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_headset.xml b/app/src/main/res/drawable-night/ic_headset.xml deleted file mode 100644 index f2376476651..00000000000 --- a/app/src/main/res/drawable-night/ic_headset.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_help.xml b/app/src/main/res/drawable-night/ic_help.xml deleted file mode 100644 index 04c1c00fc25..00000000000 --- a/app/src/main/res/drawable-night/ic_help.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_history.xml b/app/src/main/res/drawable-night/ic_history.xml deleted file mode 100644 index 2418fd6f94b..00000000000 --- a/app/src/main/res/drawable-night/ic_history.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_home.xml b/app/src/main/res/drawable-night/ic_home.xml deleted file mode 100644 index 12afe905189..00000000000 --- a/app/src/main/res/drawable-night/ic_home.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_info_outline.xml b/app/src/main/res/drawable-night/ic_info_outline.xml deleted file mode 100644 index 085665e4b25..00000000000 --- a/app/src/main/res/drawable-night/ic_info_outline.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_insert_emoticon.xml b/app/src/main/res/drawable-night/ic_insert_emoticon.xml deleted file mode 100644 index de8e66530cd..00000000000 --- a/app/src/main/res/drawable-night/ic_insert_emoticon.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_language.xml b/app/src/main/res/drawable-night/ic_language.xml deleted file mode 100644 index 9b97aa59221..00000000000 --- a/app/src/main/res/drawable-night/ic_language.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_list.xml b/app/src/main/res/drawable-night/ic_list.xml deleted file mode 100644 index 4fd341d82cf..00000000000 --- a/app/src/main/res/drawable-night/ic_list.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_live_tv.xml b/app/src/main/res/drawable-night/ic_live_tv.xml deleted file mode 100644 index 303858f9d84..00000000000 --- a/app/src/main/res/drawable-night/ic_live_tv.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_mic.xml b/app/src/main/res/drawable-night/ic_mic.xml deleted file mode 100644 index c0c92fcc76e..00000000000 --- a/app/src/main/res/drawable-night/ic_mic.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_more_vert.xml b/app/src/main/res/drawable-night/ic_more_vert.xml deleted file mode 100644 index 19703e8e7db..00000000000 --- a/app/src/main/res/drawable-night/ic_more_vert.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_motorcycle.xml b/app/src/main/res/drawable-night/ic_motorcycle.xml deleted file mode 100644 index 4ffd8b45125..00000000000 --- a/app/src/main/res/drawable-night/ic_motorcycle.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_movie.xml b/app/src/main/res/drawable-night/ic_movie.xml deleted file mode 100644 index 79f93d1c1ad..00000000000 --- a/app/src/main/res/drawable-night/ic_movie.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_music_note.xml b/app/src/main/res/drawable-night/ic_music_note.xml deleted file mode 100644 index ca80ad5ad9b..00000000000 --- a/app/src/main/res/drawable-night/ic_music_note.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_palette.xml b/app/src/main/res/drawable-night/ic_palette.xml deleted file mode 100644 index 8edcceb76e9..00000000000 --- a/app/src/main/res/drawable-night/ic_palette.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_pause.xml b/app/src/main/res/drawable-night/ic_pause.xml deleted file mode 100644 index ea843aff309..00000000000 --- a/app/src/main/res/drawable-night/ic_pause.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_people.xml b/app/src/main/res/drawable-night/ic_people.xml deleted file mode 100644 index 8b925badcb1..00000000000 --- a/app/src/main/res/drawable-night/ic_people.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_person.xml b/app/src/main/res/drawable-night/ic_person.xml deleted file mode 100644 index 5efaaf0dd79..00000000000 --- a/app/src/main/res/drawable-night/ic_person.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_pets.xml b/app/src/main/res/drawable-night/ic_pets.xml deleted file mode 100644 index 14373a3c5b8..00000000000 --- a/app/src/main/res/drawable-night/ic_pets.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_picture_in_picture.xml b/app/src/main/res/drawable-night/ic_picture_in_picture.xml deleted file mode 100644 index 1b01f323324..00000000000 --- a/app/src/main/res/drawable-night/ic_picture_in_picture.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_play_arrow.xml b/app/src/main/res/drawable-night/ic_play_arrow.xml deleted file mode 100644 index 95cace1c819..00000000000 --- a/app/src/main/res/drawable-night/ic_play_arrow.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_playlist_add.xml b/app/src/main/res/drawable-night/ic_playlist_add.xml deleted file mode 100644 index bf86fd24af3..00000000000 --- a/app/src/main/res/drawable-night/ic_playlist_add.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_playlist_add_check.xml b/app/src/main/res/drawable-night/ic_playlist_add_check.xml deleted file mode 100644 index a69d284a1fd..00000000000 --- a/app/src/main/res/drawable-night/ic_playlist_add_check.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable-night/ic_public.xml b/app/src/main/res/drawable-night/ic_public.xml deleted file mode 100644 index 6ae97422a84..00000000000 --- a/app/src/main/res/drawable-night/ic_public.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_radio.xml b/app/src/main/res/drawable-night/ic_radio.xml deleted file mode 100644 index d0902426bec..00000000000 --- a/app/src/main/res/drawable-night/ic_radio.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_refresh.xml b/app/src/main/res/drawable-night/ic_refresh.xml deleted file mode 100644 index 4ca5e73a705..00000000000 --- a/app/src/main/res/drawable-night/ic_refresh.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_restaurant.xml b/app/src/main/res/drawable-night/ic_restaurant.xml deleted file mode 100644 index dbb84968023..00000000000 --- a/app/src/main/res/drawable-night/ic_restaurant.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_rss_feed.xml b/app/src/main/res/drawable-night/ic_rss_feed.xml deleted file mode 100644 index 193f4fe924e..00000000000 --- a/app/src/main/res/drawable-night/ic_rss_feed.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable-night/ic_save.xml b/app/src/main/res/drawable-night/ic_save.xml deleted file mode 100644 index b32b11451fc..00000000000 --- a/app/src/main/res/drawable-night/ic_save.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_school.xml b/app/src/main/res/drawable-night/ic_school.xml deleted file mode 100644 index dc16c478223..00000000000 --- a/app/src/main/res/drawable-night/ic_school.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_search.xml b/app/src/main/res/drawable-night/ic_search.xml deleted file mode 100644 index 4d0f185842a..00000000000 --- a/app/src/main/res/drawable-night/ic_search.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_search_add.xml b/app/src/main/res/drawable-night/ic_search_add.xml deleted file mode 100644 index 856433e419b..00000000000 --- a/app/src/main/res/drawable-night/ic_search_add.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_select_all.xml b/app/src/main/res/drawable-night/ic_select_all.xml deleted file mode 100644 index 15773491175..00000000000 --- a/app/src/main/res/drawable-night/ic_select_all.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_settings.xml b/app/src/main/res/drawable-night/ic_settings.xml deleted file mode 100644 index 61ee02ee038..00000000000 --- a/app/src/main/res/drawable-night/ic_settings.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_share.xml b/app/src/main/res/drawable-night/ic_share.xml deleted file mode 100644 index 9dad7b85f24..00000000000 --- a/app/src/main/res/drawable-night/ic_share.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_shopping_cart.xml b/app/src/main/res/drawable-night/ic_shopping_cart.xml deleted file mode 100644 index 75c330cefc4..00000000000 --- a/app/src/main/res/drawable-night/ic_shopping_cart.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_sort.xml b/app/src/main/res/drawable-night/ic_sort.xml deleted file mode 100644 index 484be5ad2db..00000000000 --- a/app/src/main/res/drawable-night/ic_sort.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_stars.xml b/app/src/main/res/drawable-night/ic_stars.xml deleted file mode 100644 index 135980afebe..00000000000 --- a/app/src/main/res/drawable-night/ic_stars.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_telescope.xml b/app/src/main/res/drawable-night/ic_telescope.xml deleted file mode 100644 index 86468f34ad7..00000000000 --- a/app/src/main/res/drawable-night/ic_telescope.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_thumb_down.xml b/app/src/main/res/drawable-night/ic_thumb_down.xml deleted file mode 100644 index 1ee3ed0180e..00000000000 --- a/app/src/main/res/drawable-night/ic_thumb_down.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_thumb_up.xml b/app/src/main/res/drawable-night/ic_thumb_up.xml deleted file mode 100644 index c4e3878664e..00000000000 --- a/app/src/main/res/drawable-night/ic_thumb_up.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_trending_up.xml b/app/src/main/res/drawable-night/ic_trending_up.xml deleted file mode 100644 index ca4eb654b46..00000000000 --- a/app/src/main/res/drawable-night/ic_trending_up.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_tv.xml b/app/src/main/res/drawable-night/ic_tv.xml deleted file mode 100644 index b9d14869b61..00000000000 --- a/app/src/main/res/drawable-night/ic_tv.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_videogame_asset.xml b/app/src/main/res/drawable-night/ic_videogame_asset.xml deleted file mode 100644 index 4861bf809d0..00000000000 --- a/app/src/main/res/drawable-night/ic_videogame_asset.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_visibility_off.xml b/app/src/main/res/drawable-night/ic_visibility_off.xml deleted file mode 100644 index 689f3f47c14..00000000000 --- a/app/src/main/res/drawable-night/ic_visibility_off.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_visibility_on.xml b/app/src/main/res/drawable-night/ic_visibility_on.xml deleted file mode 100644 index e02f1d19120..00000000000 --- a/app/src/main/res/drawable-night/ic_visibility_on.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_volume_off.xml b/app/src/main/res/drawable-night/ic_volume_off.xml deleted file mode 100644 index a2cabcee03d..00000000000 --- a/app/src/main/res/drawable-night/ic_volume_off.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable-night/ic_volume_up.xml b/app/src/main/res/drawable-night/ic_volume_up.xml deleted file mode 100644 index 5d604f823ad..00000000000 --- a/app/src/main/res/drawable-night/ic_volume_up.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_watch_later.xml b/app/src/main/res/drawable-night/ic_watch_later.xml deleted file mode 100644 index ff93ce2d7f8..00000000000 --- a/app/src/main/res/drawable-night/ic_watch_later.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_wb_sunny.xml b/app/src/main/res/drawable-night/ic_wb_sunny.xml deleted file mode 100644 index 12a5d977491..00000000000 --- a/app/src/main/res/drawable-night/ic_wb_sunny.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_whatshot.xml b/app/src/main/res/drawable-night/ic_whatshot.xml deleted file mode 100644 index 935ac845088..00000000000 --- a/app/src/main/res/drawable-night/ic_whatshot.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-night/ic_work.xml b/app/src/main/res/drawable-night/ic_work.xml deleted file mode 100644 index 8af0219f954..00000000000 --- a/app/src/main/res/drawable-night/ic_work.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml index fedd077d841..fc2163f43ac 100644 --- a/app/src/main/res/drawable/ic_add.xml +++ b/app/src/main/res/drawable/ic_add.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_add_circle_outline.xml b/app/src/main/res/drawable/ic_add_circle_outline.xml index 1596099f3b1..0d79d69182d 100644 --- a/app/src/main/res/drawable/ic_add_circle_outline.xml +++ b/app/src/main/res/drawable/ic_add_circle_outline.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_apps.xml b/app/src/main/res/drawable/ic_apps.xml index b8c4ab12e23..b800b1743d5 100644 --- a/app/src/main/res/drawable/ic_apps.xml +++ b/app/src/main/res/drawable/ic_apps.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml index 2d68f797b50..5ed19d5fd5d 100644 --- a/app/src/main/res/drawable/ic_arrow_back.xml +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_arrow_drop_down.xml b/app/src/main/res/drawable/ic_arrow_drop_down.xml index 270637216c1..da5d308078c 100644 --- a/app/src/main/res/drawable/ic_arrow_drop_down.xml +++ b/app/src/main/res/drawable/ic_arrow_drop_down.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_arrow_drop_up.xml b/app/src/main/res/drawable/ic_arrow_drop_up.xml index fdc9dcf8de5..df4199d1864 100644 --- a/app/src/main/res/drawable/ic_arrow_drop_up.xml +++ b/app/src/main/res/drawable/ic_arrow_drop_up.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_art_track.xml b/app/src/main/res/drawable/ic_art_track.xml index abfdc203a83..7e61e1044b5 100644 --- a/app/src/main/res/drawable/ic_art_track.xml +++ b/app/src/main/res/drawable/ic_art_track.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_asterisk.xml b/app/src/main/res/drawable/ic_asterisk.xml index 840682feeb5..df7c4b32c9c 100644 --- a/app/src/main/res/drawable/ic_asterisk.xml +++ b/app/src/main/res/drawable/ic_asterisk.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_attach_money.xml b/app/src/main/res/drawable/ic_attach_money.xml index dd93a7599c9..b2c0f5c36b1 100644 --- a/app/src/main/res/drawable/ic_attach_money.xml +++ b/app/src/main/res/drawable/ic_attach_money.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_backup.xml b/app/src/main/res/drawable/ic_backup.xml index 200bb70814a..cf996d1972c 100644 --- a/app/src/main/res/drawable/ic_backup.xml +++ b/app/src/main/res/drawable/ic_backup.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_bookmark.xml b/app/src/main/res/drawable/ic_bookmark.xml index 5bf2e951cbd..32cd107f78c 100644 --- a/app/src/main/res/drawable/ic_bookmark.xml +++ b/app/src/main/res/drawable/ic_bookmark.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_brightness_high.xml b/app/src/main/res/drawable/ic_brightness_high.xml index 1ff2d2e26bf..d613ed523fb 100644 --- a/app/src/main/res/drawable/ic_brightness_high.xml +++ b/app/src/main/res/drawable/ic_brightness_high.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_brightness_low.xml b/app/src/main/res/drawable/ic_brightness_low.xml index 1a00ce2dd44..498a67ec03f 100644 --- a/app/src/main/res/drawable/ic_brightness_low.xml +++ b/app/src/main/res/drawable/ic_brightness_low.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_brightness_medium.xml b/app/src/main/res/drawable/ic_brightness_medium.xml index 853e219bd3c..1f3952586c1 100644 --- a/app/src/main/res/drawable/ic_brightness_medium.xml +++ b/app/src/main/res/drawable/ic_brightness_medium.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_bug_report.xml b/app/src/main/res/drawable/ic_bug_report.xml index 206702ff247..c7c44ccb2a1 100644 --- a/app/src/main/res/drawable/ic_bug_report.xml +++ b/app/src/main/res/drawable/ic_bug_report.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_campaign.xml b/app/src/main/res/drawable/ic_campaign.xml index 4a0e2ddbb51..a368f50f63d 100644 --- a/app/src/main/res/drawable/ic_campaign.xml +++ b/app/src/main/res/drawable/ic_campaign.xml @@ -1,6 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_child_care.xml b/app/src/main/res/drawable/ic_child_care.xml index 25a51bb23ff..5d2ac1665c4 100644 --- a/app/src/main/res/drawable/ic_child_care.xml +++ b/app/src/main/res/drawable/ic_child_care.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml index f50fd991b75..1d5133364c0 100644 --- a/app/src/main/res/drawable/ic_close.xml +++ b/app/src/main/res/drawable/ic_close.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_cloud_download.xml b/app/src/main/res/drawable/ic_cloud_download.xml index aa051b25da5..79c7db8e32c 100644 --- a/app/src/main/res/drawable/ic_cloud_download.xml +++ b/app/src/main/res/drawable/ic_cloud_download.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_comment.xml b/app/src/main/res/drawable/ic_comment.xml index 7361b7fa6cb..4bc124a8175 100644 --- a/app/src/main/res/drawable/ic_comment.xml +++ b/app/src/main/res/drawable/ic_comment.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_computer.xml b/app/src/main/res/drawable/ic_computer.xml index 04eb86a51e8..6b0e79313f3 100644 --- a/app/src/main/res/drawable/ic_computer.xml +++ b/app/src/main/res/drawable/ic_computer.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_crop_portrait.xml b/app/src/main/res/drawable/ic_crop_portrait.xml index d906df150cf..50ce52f91da 100644 --- a/app/src/main/res/drawable/ic_crop_portrait.xml +++ b/app/src/main/res/drawable/ic_crop_portrait.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml index 962e033743f..f38c5f130cf 100644 --- a/app/src/main/res/drawable/ic_delete.xml +++ b/app/src/main/res/drawable/ic_delete.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_description.xml b/app/src/main/res/drawable/ic_description.xml index e7ef3d4b573..5b80cbefd2d 100644 --- a/app/src/main/res/drawable/ic_description.xml +++ b/app/src/main/res/drawable/ic_description.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_directions_bike.xml b/app/src/main/res/drawable/ic_directions_bike.xml index 328fbe39331..b5580ee8d37 100644 --- a/app/src/main/res/drawable/ic_directions_bike.xml +++ b/app/src/main/res/drawable/ic_directions_bike.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_directions_car.xml b/app/src/main/res/drawable/ic_directions_car.xml index b2fe8bdbd6d..3bfd9b4c3e8 100644 --- a/app/src/main/res/drawable/ic_directions_car.xml +++ b/app/src/main/res/drawable/ic_directions_car.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml index bda675f1429..43b77a9cd9e 100644 --- a/app/src/main/res/drawable/ic_done.xml +++ b/app/src/main/res/drawable/ic_done.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml index 41663132454..c08695e98e7 100644 --- a/app/src/main/res/drawable/ic_drag_handle.xml +++ b/app/src/main/res/drawable/ic_drag_handle.xml @@ -1,6 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_explore.xml b/app/src/main/res/drawable/ic_explore.xml index e94079fedcc..2b974c69f36 100644 --- a/app/src/main/res/drawable/ic_explore.xml +++ b/app/src/main/res/drawable/ic_explore.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_fast_forward.xml b/app/src/main/res/drawable/ic_fast_forward.xml index ab5ae6c37b9..4edc96a9b33 100644 --- a/app/src/main/res/drawable/ic_fast_forward.xml +++ b/app/src/main/res/drawable/ic_fast_forward.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_fast_rewind.xml b/app/src/main/res/drawable/ic_fast_rewind.xml index ccc0721584a..33d9f56ef8b 100644 --- a/app/src/main/res/drawable/ic_fast_rewind.xml +++ b/app/src/main/res/drawable/ic_fast_rewind.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_fastfood.xml b/app/src/main/res/drawable/ic_fastfood.xml index 4d43eafd281..b2a1abdf37c 100644 --- a/app/src/main/res/drawable/ic_fastfood.xml +++ b/app/src/main/res/drawable/ic_fastfood.xml @@ -1,9 +1,10 @@ diff --git a/app/src/main/res/drawable/ic_favorite.xml b/app/src/main/res/drawable/ic_favorite.xml index 17cea9270c7..87d14880fd7 100644 --- a/app/src/main/res/drawable/ic_favorite.xml +++ b/app/src/main/res/drawable/ic_favorite.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_file_download.xml b/app/src/main/res/drawable/ic_file_download.xml index 370bba93dd0..b4d9e15e9ee 100644 --- a/app/src/main/res/drawable/ic_file_download.xml +++ b/app/src/main/res/drawable/ic_file_download.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_filter_list.xml b/app/src/main/res/drawable/ic_filter_list.xml index 6826b3d5acf..e1a2b236b35 100644 --- a/app/src/main/res/drawable/ic_filter_list.xml +++ b/app/src/main/res/drawable/ic_filter_list.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_fitness_center.xml b/app/src/main/res/drawable/ic_fitness_center.xml index 3e2425e4080..56670cba62e 100644 --- a/app/src/main/res/drawable/ic_fitness_center.xml +++ b/app/src/main/res/drawable/ic_fitness_center.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_format_list_numbered.xml b/app/src/main/res/drawable/ic_format_list_numbered.xml index 429616ec98a..b11666c562c 100644 --- a/app/src/main/res/drawable/ic_format_list_numbered.xml +++ b/app/src/main/res/drawable/ic_format_list_numbered.xml @@ -1,7 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_fullscreen_exit.xml b/app/src/main/res/drawable/ic_fullscreen_exit.xml index a940aa13c86..a497da742ea 100644 --- a/app/src/main/res/drawable/ic_fullscreen_exit.xml +++ b/app/src/main/res/drawable/ic_fullscreen_exit.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_headset.xml b/app/src/main/res/drawable/ic_headset.xml index 674aa8def40..3eff4b7dd9e 100644 --- a/app/src/main/res/drawable/ic_headset.xml +++ b/app/src/main/res/drawable/ic_headset.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml index b1d7a2cf55a..45955eae78a 100644 --- a/app/src/main/res/drawable/ic_help.xml +++ b/app/src/main/res/drawable/ic_help.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml index d9f75ea6d73..4e21de19d2c 100644 --- a/app/src/main/res/drawable/ic_history.xml +++ b/app/src/main/res/drawable/ic_history.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml index f8bb0b55633..48f968b4cce 100644 --- a/app/src/main/res/drawable/ic_home.xml +++ b/app/src/main/res/drawable/ic_home.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_hourglass_top.xml b/app/src/main/res/drawable/ic_hourglass_top.xml index 59ad4b2d2e3..f92496779ea 100644 --- a/app/src/main/res/drawable/ic_hourglass_top.xml +++ b/app/src/main/res/drawable/ic_hourglass_top.xml @@ -1,9 +1,10 @@ diff --git a/app/src/main/res/drawable/ic_info_outline.xml b/app/src/main/res/drawable/ic_info_outline.xml index 6c60606190c..3bbe5191787 100644 --- a/app/src/main/res/drawable/ic_info_outline.xml +++ b/app/src/main/res/drawable/ic_info_outline.xml @@ -1,6 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_language.xml b/app/src/main/res/drawable/ic_language.xml index 340a4bf0f54..8bc821acc26 100644 --- a/app/src/main/res/drawable/ic_language.xml +++ b/app/src/main/res/drawable/ic_language.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml index 1471c52f5ce..f6538e875ce 100644 --- a/app/src/main/res/drawable/ic_list.xml +++ b/app/src/main/res/drawable/ic_list.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_live_tv.xml b/app/src/main/res/drawable/ic_live_tv.xml index 1f7957c4a46..80fb172aaa0 100644 --- a/app/src/main/res/drawable/ic_live_tv.xml +++ b/app/src/main/res/drawable/ic_live_tv.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_mic.xml b/app/src/main/res/drawable/ic_mic.xml index 8b765ffd42c..9da90f5a951 100644 --- a/app/src/main/res/drawable/ic_mic.xml +++ b/app/src/main/res/drawable/ic_mic.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_more_vert.xml b/app/src/main/res/drawable/ic_more_vert.xml index 7b7f195546f..1a873cf8bc3 100644 --- a/app/src/main/res/drawable/ic_more_vert.xml +++ b/app/src/main/res/drawable/ic_more_vert.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_motorcycle.xml b/app/src/main/res/drawable/ic_motorcycle.xml index e354f8bdab6..7684b067391 100644 --- a/app/src/main/res/drawable/ic_motorcycle.xml +++ b/app/src/main/res/drawable/ic_motorcycle.xml @@ -1,6 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_music_note.xml b/app/src/main/res/drawable/ic_music_note.xml index 830a7fab179..cc4e5bd10f3 100644 --- a/app/src/main/res/drawable/ic_music_note.xml +++ b/app/src/main/res/drawable/ic_music_note.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml index 9f3462892c9..2805ebb269a 100644 --- a/app/src/main/res/drawable/ic_next.xml +++ b/app/src/main/res/drawable/ic_next.xml @@ -1,10 +1,9 @@ - diff --git a/app/src/main/res/drawable/ic_palette.xml b/app/src/main/res/drawable/ic_palette.xml index 568f8e4de42..0356bfe8f94 100644 --- a/app/src/main/res/drawable/ic_palette.xml +++ b/app/src/main/res/drawable/ic_palette.xml @@ -1,6 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_people.xml b/app/src/main/res/drawable/ic_people.xml index 603c006db55..9cd3ad3fbe4 100644 --- a/app/src/main/res/drawable/ic_people.xml +++ b/app/src/main/res/drawable/ic_people.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml index 55495d5a08e..db64734ae10 100644 --- a/app/src/main/res/drawable/ic_person.xml +++ b/app/src/main/res/drawable/ic_person.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_pets.xml b/app/src/main/res/drawable/ic_pets.xml index 58e52bf6cb9..0aadab03d89 100644 --- a/app/src/main/res/drawable/ic_pets.xml +++ b/app/src/main/res/drawable/ic_pets.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_picture_in_picture.xml b/app/src/main/res/drawable/ic_picture_in_picture.xml index 326ff030430..91fd5241321 100644 --- a/app/src/main/res/drawable/ic_picture_in_picture.xml +++ b/app/src/main/res/drawable/ic_picture_in_picture.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_play_arrow.xml b/app/src/main/res/drawable/ic_play_arrow.xml index dbe3ec664e8..a70a4ddbb98 100644 --- a/app/src/main/res/drawable/ic_play_arrow.xml +++ b/app/src/main/res/drawable/ic_play_arrow.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_playlist_add.xml b/app/src/main/res/drawable/ic_playlist_add.xml index 341894ba97f..144f123b166 100644 --- a/app/src/main/res/drawable/ic_playlist_add.xml +++ b/app/src/main/res/drawable/ic_playlist_add.xml @@ -1,6 +1,7 @@ - diff --git a/app/src/main/res/drawable/ic_public.xml b/app/src/main/res/drawable/ic_public.xml index 19288457023..796f37812d4 100644 --- a/app/src/main/res/drawable/ic_public.xml +++ b/app/src/main/res/drawable/ic_public.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_radio.xml b/app/src/main/res/drawable/ic_radio.xml index ca4501bb7c6..f009ff54ea6 100644 --- a/app/src/main/res/drawable/ic_radio.xml +++ b/app/src/main/res/drawable/ic_radio.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml index 1f9072a36fa..20af23dde34 100644 --- a/app/src/main/res/drawable/ic_refresh.xml +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_repeat.xml b/app/src/main/res/drawable/ic_repeat.xml index 24d9f44f0a6..fb9ef820b94 100644 --- a/app/src/main/res/drawable/ic_repeat.xml +++ b/app/src/main/res/drawable/ic_repeat.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_replay.xml b/app/src/main/res/drawable/ic_replay.xml index d00231b51c1..987710fc713 100644 --- a/app/src/main/res/drawable/ic_replay.xml +++ b/app/src/main/res/drawable/ic_replay.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_restaurant.xml b/app/src/main/res/drawable/ic_restaurant.xml index 51f1145c682..9dccc8ee708 100644 --- a/app/src/main/res/drawable/ic_restaurant.xml +++ b/app/src/main/res/drawable/ic_restaurant.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_rss_feed.xml b/app/src/main/res/drawable/ic_rss_feed.xml index ed6228cc253..a73eff527b4 100644 --- a/app/src/main/res/drawable/ic_rss_feed.xml +++ b/app/src/main/res/drawable/ic_rss_feed.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml index 0651fcc6c36..26e6645892b 100644 --- a/app/src/main/res/drawable/ic_save.xml +++ b/app/src/main/res/drawable/ic_save.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_school.xml b/app/src/main/res/drawable/ic_school.xml index 54dc17ddb53..6d7e2f0e9d1 100644 --- a/app/src/main/res/drawable/ic_school.xml +++ b/app/src/main/res/drawable/ic_school.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml index d23ea57f880..a889b09e531 100644 --- a/app/src/main/res/drawable/ic_search.xml +++ b/app/src/main/res/drawable/ic_search.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_search_add.xml b/app/src/main/res/drawable/ic_search_add.xml index 889ea4c6fb2..449115e3a53 100644 --- a/app/src/main/res/drawable/ic_search_add.xml +++ b/app/src/main/res/drawable/ic_search_add.xml @@ -1,6 +1,7 @@ - + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index e50f6fe3ae5..1e259c6ad4e 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -1,6 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml index 338d95ad509..40971e408e8 100644 --- a/app/src/main/res/drawable/ic_share.xml +++ b/app/src/main/res/drawable/ic_share.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_shopping_cart.xml b/app/src/main/res/drawable/ic_shopping_cart.xml index 18e1b930d61..9e361b60d35 100644 --- a/app/src/main/res/drawable/ic_shopping_cart.xml +++ b/app/src/main/res/drawable/ic_shopping_cart.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_shuffle.xml b/app/src/main/res/drawable/ic_shuffle.xml index 1192dec9ffd..86717de363e 100644 --- a/app/src/main/res/drawable/ic_shuffle.xml +++ b/app/src/main/res/drawable/ic_shuffle.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml index b537e982ec1..a97bebd87ee 100644 --- a/app/src/main/res/drawable/ic_sort.xml +++ b/app/src/main/res/drawable/ic_sort.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_stars.xml b/app/src/main/res/drawable/ic_stars.xml index 35957427d0a..ac5b9dd1990 100644 --- a/app/src/main/res/drawable/ic_stars.xml +++ b/app/src/main/res/drawable/ic_stars.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_subtitles.xml b/app/src/main/res/drawable/ic_subtitles.xml index 1d997a03251..43bf3e16bef 100644 --- a/app/src/main/res/drawable/ic_subtitles.xml +++ b/app/src/main/res/drawable/ic_subtitles.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_telescope.xml b/app/src/main/res/drawable/ic_telescope.xml index 8077e9325ac..e3d5ea33b7b 100644 --- a/app/src/main/res/drawable/ic_telescope.xml +++ b/app/src/main/res/drawable/ic_telescope.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_thumb_down.xml b/app/src/main/res/drawable/ic_thumb_down.xml index 103e5fea394..aa828aa5013 100644 --- a/app/src/main/res/drawable/ic_thumb_down.xml +++ b/app/src/main/res/drawable/ic_thumb_down.xml @@ -1,6 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_tv.xml b/app/src/main/res/drawable/ic_tv.xml index 11d2d25b649..91d860eaf56 100644 --- a/app/src/main/res/drawable/ic_tv.xml +++ b/app/src/main/res/drawable/ic_tv.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_videogame_asset.xml b/app/src/main/res/drawable/ic_videogame_asset.xml index 02fa7eb56a4..01a91b0539f 100644 --- a/app/src/main/res/drawable/ic_videogame_asset.xml +++ b/app/src/main/res/drawable/ic_videogame_asset.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_visibility_off.xml b/app/src/main/res/drawable/ic_visibility_off.xml index e0b170300a0..f833d5e0622 100644 --- a/app/src/main/res/drawable/ic_visibility_off.xml +++ b/app/src/main/res/drawable/ic_visibility_off.xml @@ -1,9 +1,10 @@ + android:width="24dp" + android:height="24dp" + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> + android:fillColor="#FF000000" + android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z" /> diff --git a/app/src/main/res/drawable/ic_visibility_on.xml b/app/src/main/res/drawable/ic_visibility_on.xml index 6c95a5d2921..06e530961c5 100644 --- a/app/src/main/res/drawable/ic_visibility_on.xml +++ b/app/src/main/res/drawable/ic_visibility_on.xml @@ -1,9 +1,10 @@ + android:width="24dp" + android:height="24dp" + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> + android:fillColor="#FF000000" + android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" /> diff --git a/app/src/main/res/drawable/ic_volume_down.xml b/app/src/main/res/drawable/ic_volume_down.xml index bcc36327986..0fe36fad348 100644 --- a/app/src/main/res/drawable/ic_volume_down.xml +++ b/app/src/main/res/drawable/ic_volume_down.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_volume_mute.xml b/app/src/main/res/drawable/ic_volume_mute.xml index 2c915139695..b18f6337cf7 100644 --- a/app/src/main/res/drawable/ic_volume_mute.xml +++ b/app/src/main/res/drawable/ic_volume_mute.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_volume_off.xml b/app/src/main/res/drawable/ic_volume_off.xml index 7700239a377..420593e04f6 100644 --- a/app/src/main/res/drawable/ic_volume_off.xml +++ b/app/src/main/res/drawable/ic_volume_off.xml @@ -1,10 +1,10 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_volume_up.xml b/app/src/main/res/drawable/ic_volume_up.xml index aaaf8498333..b5a47789bba 100644 --- a/app/src/main/res/drawable/ic_volume_up.xml +++ b/app/src/main/res/drawable/ic_volume_up.xml @@ -1,9 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_watch_later.xml b/app/src/main/res/drawable/ic_watch_later.xml index 72952bcaad2..34ecad214a8 100644 --- a/app/src/main/res/drawable/ic_watch_later.xml +++ b/app/src/main/res/drawable/ic_watch_later.xml @@ -1,6 +1,7 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_whatshot.xml b/app/src/main/res/drawable/ic_whatshot.xml index 07965067e95..84260ffe42f 100644 --- a/app/src/main/res/drawable/ic_whatshot.xml +++ b/app/src/main/res/drawable/ic_whatshot.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_work.xml b/app/src/main/res/drawable/ic_work.xml index 2ee55ea23e0..014718e603a 100644 --- a/app/src/main/res/drawable/ic_work.xml +++ b/app/src/main/res/drawable/ic_work.xml @@ -1,8 +1,9 @@ + android:tint="@color/defaultIconTint" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 1ee11c49b1b..851085b5be3 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -266,14 +266,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"> - + android:src="@drawable/buddy" + app:shapeAppearance="@style/CircularImageView" /> - diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml deleted file mode 100644 index 71a325cf39d..00000000000 --- a/app/src/main/res/layout-large-land/player.xml +++ /dev/null @@ -1,756 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -