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

+ * Following conditions need to be met, before data is request from the server: + * + * * The app is signed with the correct signing key (by TeamNewPipe / schabi). + * If the signing key differs from the one used upstream, the update cannot be installed. + * * The user enabled searching for and notifying about updates in the settings. + * * The app did not recently check for updates. + * We do not want to make unnecessary connections and DOS our servers. + * + */ + @JvmStatic + fun enqueueNewVersionCheckingWork(context: Context) { + val workRequest: WorkRequest = + OneTimeWorkRequest.Builder(NewVersionWorker::class.java).build() + WorkManager.getInstance(context).enqueue(workRequest) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java index fde006a60..c7604e512 100644 --- a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java +++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java @@ -14,7 +14,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.SaveUploaderUrlHelper; +import org.schabi.newpipe.util.SparseItemUtil; import java.util.Collections; @@ -62,7 +62,8 @@ public final class QueueItemMenuUtil { return true; case R.id.menu_item_channel_details: - SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(context, item, + SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(), + item.getUrl(), item.getUploaderUrl(), // An intent must be used here. // Opening with FragmentManager transactions is not working, // as PlayQueueActivity doesn't use fragments. diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 9d6e44f04..adef3c0e4 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -633,7 +633,7 @@ public class RouterActivity extends AppCompatActivity { .subscribe(result -> { final List sortedVideoStreams = ListHelper .getSortedStreamVideosList(this, result.getVideoStreams(), - result.getVideoOnlyStreams(), false); + result.getVideoOnlyStreams(), false, false); final int selectedVideoStreamIndex = ListHelper .getDefaultResolutionIndex(this, sortedVideoStreams); diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt index 1e5bd8799..50a3984e3 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt @@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import org.schabi.newpipe.BuildConfig import org.schabi.newpipe.R @@ -21,30 +20,28 @@ import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.external_communication.ShareUtils class AboutActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { Localization.assureCorrectAppLanguage(this) super.onCreate(savedInstanceState) ThemeHelper.setTheme(this) title = getString(R.string.title_activity_about) + val aboutBinding = ActivityAboutBinding.inflate(layoutInflater) setContentView(aboutBinding.root) setSupportActionBar(aboutBinding.aboutToolbar) - supportActionBar!!.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + // Create the adapter that will return a fragment for each of the three // primary sections of the activity. val mAboutStateAdapter = AboutStateAdapter(this) - // Set up the ViewPager with the sections adapter. aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter TabLayoutMediator( aboutBinding.aboutTabLayout, aboutBinding.aboutViewPager2 - ) { tab: TabLayout.Tab, position: Int -> - when (position) { - POS_ABOUT -> tab.setText(R.string.tab_about) - POS_LICENSE -> tab.setText(R.string.tab_licenses) - else -> throw IllegalArgumentException("Unknown position for ViewPager2") - } + ) { tab, position -> + tab.setText(mAboutStateAdapter.getPageTitle(position)) }.attach() } @@ -75,13 +72,14 @@ class AboutActivity : AppCompatActivity() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - val aboutBinding = FragmentAboutBinding.inflate(inflater, container, false) - aboutBinding.aboutAppVersion.text = BuildConfig.VERSION_NAME - aboutBinding.aboutGithubLink.openLink(R.string.github_url) - aboutBinding.aboutDonationLink.openLink(R.string.donation_url) - aboutBinding.aboutWebsiteLink.openLink(R.string.website_url) - aboutBinding.aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url) - return aboutBinding.root + FragmentAboutBinding.inflate(inflater, container, false).apply { + aboutAppVersion.text = BuildConfig.VERSION_NAME + aboutGithubLink.openLink(R.string.github_url) + aboutDonationLink.openLink(R.string.donation_url) + aboutWebsiteLink.openLink(R.string.website_url) + aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url) + return root + } } } @@ -90,17 +88,29 @@ class AboutActivity : AppCompatActivity() { * one of the sections/tabs/pages. */ private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { + private val posAbout = 0 + private val posLicense = 1 + private val totalCount = 2 + override fun createFragment(position: Int): Fragment { return when (position) { - POS_ABOUT -> AboutFragment() - POS_LICENSE -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS) + posAbout -> AboutFragment() + posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS) else -> throw IllegalArgumentException("Unknown position for ViewPager2") } } override fun getItemCount(): Int { // Show 2 total pages. - return TOTAL_COUNT + return totalCount + } + + fun getPageTitle(position: Int): Int { + return when (position) { + posAbout -> R.string.tab_about + posLicense -> R.string.tab_licenses + else -> throw IllegalArgumentException("Unknown position for ViewPager2") + } } } @@ -117,10 +127,6 @@ class AboutActivity : AppCompatActivity() { "AndroidX", "2005 - 2011", "The Android Open Source Project", "https://developer.android.com/jetpack", StandardLicenses.APACHE2 ), - SoftwareComponent( - "CircleImageView", "2014 - 2020", "Henning Dodenhof", - "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2 - ), SoftwareComponent( "ExoPlayer", "2014 - 2020", "Google, Inc.", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2 @@ -191,8 +197,5 @@ class AboutActivity : AppCompatActivity() { "https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT ), ) - private const val POS_ABOUT = 0 - private const val POS_LICENSE = 1 - private const val TOTAL_COUNT = 2 } } diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt index a04de8abc..c1dd38389 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt @@ -87,60 +87,50 @@ object LicenseFragmentHelper { return context.getString(color).substring(3) } - @JvmStatic fun showLicense(context: Context?, license: License): Disposable { + return showLicense(context, license) { alertDialog -> + alertDialog.setPositiveButton(R.string.ok) { dialog, _ -> + dialog.dismiss() + } + } + } + + fun showLicense(context: Context?, component: SoftwareComponent): Disposable { + return showLicense(context, component.license) { alertDialog -> + alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ -> + dialog.dismiss() + } + alertDialog.setNeutralButton(R.string.open_website_license) { _, _ -> + ShareUtils.openUrlInBrowser(context!!, component.link) + } + } + } + + private fun showLicense( + context: Context?, + license: License, + block: (AlertDialog.Builder) -> Unit + ): Disposable { return if (context == null) { Disposable.empty() } else { Observable.fromCallable { getFormattedLicense(context, license) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { formattedLicense: String -> + .subscribe { formattedLicense -> val webViewData = Base64.encodeToString( - formattedLicense - .toByteArray(StandardCharsets.UTF_8), - Base64.NO_PADDING + formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING ) val webView = WebView(context) webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") - val alert = AlertDialog.Builder(context) - alert.setTitle(license.name) - alert.setView(webView) - Localization.assureCorrectAppLanguage(context) - alert.setNegativeButton( - context.getString(R.string.ok) - ) { dialog, _ -> dialog.dismiss() } - alert.show() - } - } - } - @JvmStatic - fun showLicense(context: Context?, component: SoftwareComponent): Disposable { - return if (context == null) { - Disposable.empty() - } else { - Observable.fromCallable { getFormattedLicense(context, component.license) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { formattedLicense: String -> - val webViewData = Base64.encodeToString( - formattedLicense - .toByteArray(StandardCharsets.UTF_8), - Base64.NO_PADDING - ) - val webView = WebView(context) - webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") - val alert = AlertDialog.Builder(context) - alert.setTitle(component.license.name) - alert.setView(webView) - Localization.assureCorrectAppLanguage(context) - alert.setPositiveButton( - R.string.dismiss - ) { dialog, _ -> dialog.dismiss() } - alert.setNeutralButton(R.string.open_website_license) { _, _ -> - ShareUtils.openUrlInBrowser(context, component.link) + + AlertDialog.Builder(context).apply { + setTitle(license.name) + setView(webView) + Localization.assureCorrectAppLanguage(context) + block(this) + show() } - alert.show() } } } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 0a765ed4e..150d4a8e5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.database.history.dao; import androidx.annotation.Nullable; import androidx.room.Dao; import androidx.room.Query; +import androidx.room.RewriteQueriesToDropUnusedColumns; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; @@ -67,6 +68,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO { + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") Flowable getMaximumIndexOf(long playlistId); + @RewriteQueriesToDropUnusedColumns @Transaction @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " // get ids of streams of the given playlist diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 9798ec72d..47b6f4dd9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns import androidx.room.Transaction import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Maybe @@ -31,6 +32,7 @@ abstract class SubscriptionDAO : BasicDAO { ) abstract fun getSubscriptionsFiltered(filter: String): Flowable> + @RewriteQueriesToDropUnusedColumns @Query( """ SELECT * FROM subscriptions s @@ -47,6 +49,7 @@ abstract class SubscriptionDAO : BasicDAO { currentGroupId: Long ): Flowable> + @RewriteQueriesToDropUnusedColumns @Query( """ SELECT * FROM subscriptions s diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 5c954ad64..f5c226908 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -61,6 +61,7 @@ import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.SecondaryStreamHelper; +import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; @@ -151,7 +152,7 @@ public class DownloadDialog extends DialogFragment public static DownloadDialog newInstance(final Context context, final StreamInfo info) { final ArrayList streamsList = new ArrayList<>(ListHelper .getSortedStreamVideosList(context, info.getVideoStreams(), - info.getVideoOnlyStreams(), false)); + info.getVideoOnlyStreams(), false, false)); final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList); final DownloadDialog instance = newInstance(info); @@ -321,21 +322,15 @@ public class DownloadDialog extends DialogFragment final int threads = prefs.getInt(getString(R.string.default_download_threads), 3); dialogBinding.threadsCount.setText(String.valueOf(threads)); dialogBinding.threads.setProgress(threads - 1); - dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() { @Override - public void onProgressChanged(final SeekBar seekbar, final int progress, + public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress, final boolean fromUser) { final int newProgress = progress + 1; prefs.edit().putInt(getString(R.string.default_download_threads), newProgress) .apply(); dialogBinding.threadsCount.setText(String.valueOf(newProgress)); } - - @Override - public void onStartTrackingTouch(final SeekBar p1) { } - - @Override - public void onStopTrackingTouch(final SeekBar p1) { } }); fetchStreamsSize(); diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.java index 8e7a455b4..976173373 100644 --- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.java @@ -29,8 +29,8 @@ public enum UserAction { NEW_STREAMS_NOTIFICATIONS("new streams notifications"), PREFERENCES_MIGRATION("migration of preferences"), SHARE_TO_NEWPIPE("share to newpipe"), - CHECK_FOR_NEW_APP_VERSION("check for new app version"); - + CHECK_FOR_NEW_APP_VERSION("check for new app version"), + OPEN_INFO_ITEM_DIALOG("open info item dialog"); private final String message; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java index cbd44566e..6b17803c4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; @@ -10,7 +11,7 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager; */ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener { @Override - public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) { + public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { super.onScrolled(recyclerView, dx, dy); if (dy > 0) { int pastVisibleItems = 0; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java index 2fe615764..5016a49f6 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.fragments.detail; +import androidx.annotation.NonNull; + import org.schabi.newpipe.player.playqueue.PlayQueue; import java.io.Serializable; @@ -46,6 +48,7 @@ class StackItem implements Serializable { return playQueue; } + @NonNull @Override public String toString() { return getServiceId() + ":" + getUrl() + " > " + getTitle(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 2d9abc6dc..0af5ec99e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1617,6 +1617,7 @@ public final class VideoDetailFragment activity, info.getVideoStreams(), info.getVideoOnlyStreams(), + false, false); selectedVideoStreamIndex = ListHelper .getDefaultResolutionIndex(activity, sortedVideoStreams); @@ -1994,9 +1995,7 @@ public final class VideoDetailFragment // Prevent jumping of the player on devices with cutout if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { activity.getWindow().getAttributes().layoutInDisplayCutoutMode = - isMultiWindowOrFullscreen() - ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; } activity.getWindow().getDecorView().setSystemUiVisibility(0); activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); @@ -2018,9 +2017,7 @@ public final class VideoDetailFragment // Prevent jumping of the player on devices with cutout if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { activity.getWindow().getAttributes().layoutInDisplayCutoutMode = - isMultiWindowOrFullscreen() - ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; } int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN @@ -2037,7 +2034,7 @@ public final class VideoDetailFragment activity.getWindow().getDecorView().setSystemUiVisibility(visibility); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && isMultiWindowOrFullscreen()) { + && (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) { activity.getWindow().setStatusBarColor(Color.TRANSPARENT); activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); } @@ -2053,11 +2050,6 @@ public final class VideoDetailFragment } } - private boolean isMultiWindowOrFullscreen() { - return DeviceUtils.isInMultiWindow(activity) - || (isPlayerAvailable() && player.isFullscreen()); - } - private boolean playerIsNotStopped() { return isPlayerAvailable() && !player.isStopped(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 3c2e65bb7..27e5a8571 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.fragments.list; -import android.app.Activity; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; + import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; @@ -17,37 +19,26 @@ import androidx.appcompat.app.ActionBar; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import androidx.viewbinding.ViewBinding; import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.PignateFooterBinding; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.info_list.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.player.helper.PlayerHolder; -import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; -import org.schabi.newpipe.util.StreamDialogEntry; import org.schabi.newpipe.views.SuperScrollLayoutManager; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Queue; - -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; +import java.util.function.Supplier; public abstract class BaseListFragment extends BaseStateFragment implements ListViewContract, StateSaver.WriteRead, @@ -79,11 +70,6 @@ public abstract class BaseListFragment extends BaseStateFragment } } - @Override - public void onDetach() { - super.onDetach(); - } - @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -220,14 +206,10 @@ public abstract class BaseListFragment extends BaseStateFragment //////////////////////////////////////////////////////////////////////////*/ @Nullable - protected ViewBinding getListHeader() { + protected Supplier getListHeaderSupplier() { return null; } - protected ViewBinding getListFooter() { - return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false); - } - protected RecyclerView.LayoutManager getListLayoutManager() { return new SuperScrollLayoutManager(activity); } @@ -252,11 +234,10 @@ public abstract class BaseListFragment extends BaseStateFragment itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); infoListAdapter.setUseGridVariant(useGrid); - infoListAdapter.setFooter(getListFooter().getRoot()); - final ViewBinding listHeader = getListHeader(); - if (listHeader != null) { - infoListAdapter.setHeader(listHeader.getRoot()); + final Supplier listHeaderSupplier = getListHeaderSupplier(); + if (listHeaderSupplier != null) { + infoListAdapter.setHeaderSupplier(listHeaderSupplier); } itemsList.setAdapter(infoListAdapter); @@ -271,7 +252,7 @@ public abstract class BaseListFragment extends BaseStateFragment @Override protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnClickGesture() { + infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<>() { @Override public void selected(final StreamInfoItem selectedItem) { onStreamSelected(selectedItem); @@ -279,11 +260,11 @@ public abstract class BaseListFragment extends BaseStateFragment @Override public void held(final StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + showInfoItemDialog(selectedItem); } }); - infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() { + infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() { @Override public void selected(final ChannelInfoItem selectedItem) { try { @@ -299,7 +280,7 @@ public abstract class BaseListFragment extends BaseStateFragment } }); - infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture() { + infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() { @Override public void selected(final PlaylistInfoItem selectedItem) { try { @@ -315,22 +296,99 @@ public abstract class BaseListFragment extends BaseStateFragment } }); - infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture() { + infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() { @Override public void selected(final CommentsInfoItem selectedItem) { onItemSelected(selectedItem); } }); + // Ensure that there is always a scroll listener (e.g. when rotating the device) + useNormalItemListScrollListener(); + } + + /** + * Removes all listeners and adds the normal scroll listener to the {@link #itemsList}. + */ + protected void useNormalItemListScrollListener() { + if (DEBUG) { + Log.d(TAG, "useNormalItemListScrollListener called"); + } itemsList.clearOnScrollListeners(); - itemsList.addOnScrollListener(new OnScrollBelowItemsListener() { + itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener()); + } + + /** + * Removes all listeners and adds the initial scroll listener to the {@link #itemsList}. + *
+ * Which tries to load more items when not enough are in the view (not scrollable) + * and more are available. + *
+ * Note: This method only works because "This callback will also be called if visible + * item range changes after a layout calculation. In that case, dx and dy will be 0." + * - which might be unexpected because no actual scrolling occurs... + *
+ * This listener will be replaced by DefaultItemListOnScrolledDownListener when + *
    + *
  • the view was actually scrolled
  • + *
  • the view is scrollable
  • + *
  • no more items can be loaded
  • + *
+ */ + protected void useInitialItemListLoadScrollListener() { + if (DEBUG) { + Log.d(TAG, "useInitialItemListLoadScrollListener called"); + } + itemsList.clearOnScrollListeners(); + itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener() { @Override - public void onScrolledDown(final RecyclerView recyclerView) { - onScrollToBottom(); + public void onScrolled(@NonNull final RecyclerView recyclerView, + final int dx, final int dy) { + super.onScrolled(recyclerView, dx, dy); + + if (dy != 0) { + log("Vertical scroll occurred"); + + useNormalItemListScrollListener(); + return; + } + if (isLoading.get()) { + log("Still loading data -> Skipping"); + return; + } + if (!hasMoreItems()) { + log("No more items to load"); + + useNormalItemListScrollListener(); + return; + } + if (itemsList.canScrollVertically(1) + || itemsList.canScrollVertically(-1)) { + log("View is scrollable"); + + useNormalItemListScrollListener(); + return; + } + + log("Loading more data"); + loadMoreItems(); + } + + private void log(final String msg) { + if (DEBUG) { + Log.d(TAG, "initItemListLoadScrollListener - " + msg); + } } }); } + class DefaultItemListOnScrolledDownListener extends OnScrollBelowItemsListener { + @Override + public void onScrolledDown(final RecyclerView recyclerView) { + onScrollToBottom(); + } + } + private void onStreamSelected(final StreamInfoItem selectedItem) { onItemSelected(selectedItem); NavigationHelper.openVideoDetailFragment(requireContext(), getFM(), @@ -344,55 +402,12 @@ public abstract class BaseListFragment extends BaseStateFragment } } - protected void showStreamDialog(final StreamInfoItem item) { - final Context context = getContext(); - final Activity activity = getActivity(); - if (context == null || context.getResources() == null || activity == null) { - return; + protected void showInfoItemDialog(final StreamInfoItem item) { + try { + new InfoItemDialog.Builder(getActivity(), getContext(), this, item).create().show(); + } catch (final IllegalArgumentException e) { + InfoItemDialog.Builder.reportErrorDuringInitialization(e, item); } - final List entries = new ArrayList<>(); - - if (PlayerHolder.getInstance().isPlayQueueReady()) { - entries.add(StreamDialogEntry.enqueue); - - if (PlayerHolder.getInstance().getQueueSize() > 1) { - entries.add(StreamDialogEntry.enqueue_next); - } - } - - if (item.getStreamType() == StreamType.AUDIO_STREAM) { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } else { - entries.addAll(Arrays.asList( - StreamDialogEntry.start_here_on_background, - StreamDialogEntry.start_here_on_popup, - StreamDialogEntry.append_playlist, - StreamDialogEntry.share - )); - } - entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { - entries.add(StreamDialogEntry.play_with_kodi); - } - - // show "mark as watched" only when watch history is enabled - if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) { - entries.add( - StreamDialogEntry.mark_as_watched - ); - } - if (!isNullOrEmpty(item.getUploaderUrl())) { - entries.add(StreamDialogEntry.show_channel_details); - } - - StreamDialogEntry.setEnabledEntries(entries); - - new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), - (dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show(); } /*////////////////////////////////////////////////////////////////////////// @@ -418,6 +433,12 @@ public abstract class BaseListFragment extends BaseStateFragment // Load and handle //////////////////////////////////////////////////////////////////////////*/ + @Override + protected void startLoading(final boolean forceLoad) { + useInitialItemListLoadScrollListener(); + super.startLoading(forceLoad); + } + protected abstract void loadMoreItems(); protected abstract boolean hasMoreItems(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index e98dc9fda..35424437d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.Page; @@ -27,8 +28,8 @@ import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public abstract class BaseListInfoFragment - extends BaseListFragment { +public abstract class BaseListInfoFragment> + extends BaseListFragment> { @State protected int serviceId = Constants.NO_SERVICE_ID; @State @@ -37,7 +38,7 @@ public abstract class BaseListInfoFragment protected String url; private final UserAction errorUserAction; - protected I currentInfo; + protected L currentInfo; protected Page currentNextPage; protected Disposable currentWorker; @@ -65,7 +66,7 @@ public abstract class BaseListInfoFragment super.onResume(); // Check if it was loading when the fragment was stopped/paused, if (wasLoading.getAndSet(false)) { - if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) { + if (hasMoreItems() && !infoListAdapter.getItemsList().isEmpty()) { loadMoreItems(); } else { doInitialLoadLogic(); @@ -97,7 +98,7 @@ public abstract class BaseListInfoFragment @SuppressWarnings("unchecked") public void readFrom(@NonNull final Queue savedObjects) throws Exception { super.readFrom(savedObjects); - currentInfo = (I) savedObjects.poll(); + currentInfo = (L) savedObjects.poll(); currentNextPage = (Page) savedObjects.poll(); } @@ -105,6 +106,7 @@ public abstract class BaseListInfoFragment // Load and handle //////////////////////////////////////////////////////////////////////////*/ + @Override protected void doInitialLoadLogic() { if (DEBUG) { Log.d(TAG, "doInitialLoadLogic() called"); @@ -123,7 +125,7 @@ public abstract class BaseListInfoFragment * @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 abstract class BaseListInfoFragment 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 abstract class BaseListInfoFragment * * @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 @@ public abstract class BaseListInfoFragment .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 @@ public abstract class BaseListInfoFragment } @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 @@ public abstract class BaseListInfoFragment //////////////////////////////////////////////////////////////////////////*/ @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 abstract class BaseListInfoFragment 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 ccc2be7b4..869503b5b 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.NonNull; 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.databinding.PlaylistControlBinding; 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.player.playqueue.PlayQueue; 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.Consumer; 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 class ChannelFragment extends BaseListInfoFragment //////////////////////////////////////////////////////////////////////////*/ @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 class ChannelFragment extends BaseListInfoFragment } } - 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 class ChannelFragment extends BaseListInfoFragment 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 @@ public class ChannelFragment extends BaseListInfoFragment //////////////////////////////////////////////////////////////////////////*/ @Override - protected Single loadMoreItemsLogic() { + protected Single> loadMoreItemsLogic() { return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage); } @@ -575,12 +570,11 @@ public class ChannelFragment extends BaseListInfoFragment } 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 3d11e90c0..3b092cc28 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.R; 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 org.schabi.newpipe.util.ExtractorHelper; 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 class CommentsFragment extends BaseListInfoFragment { //////////////////////////////////////////////////////////////////////////*/ @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 c25f18e8b..0b01627d6 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.exceptions.ExtractionException; 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 @@ import io.reactivex.rxjava3.core.Single; *

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

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

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

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

+ * 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 000000000..7e87318ee --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java @@ -0,0 +1,142 @@ +package org.schabi.newpipe.info_list.dialog; + +import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment; +import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse; +import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; +import org.schabi.newpipe.local.dialog.PlaylistDialog; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.external_communication.KoreUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; + +import java.util.Collections; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; + +/** + *

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

+ *

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

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

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

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

+ * + * @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 @@ public final class Player implements 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 final class Player implements return isSomePopupMenuVisible; } + public void setSomePopupMenuVisible(final boolean somePopupMenuVisible) { + isSomePopupMenuVisible = somePopupMenuVisible; + } + public ImageButton getPlayPauseButton() { return binding.playPauseButton; } @@ -4344,6 +4498,11 @@ public final class Player implements public PlayQueueAdapter getPlayQueueAdapter() { return playQueueAdapter; } + + public PlayerBinding getBinding() { + return binding; + } + //endregion @@ -4369,15 +4528,42 @@ public final class Player implements } 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 b36f9f234..087a3bc76 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 class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An //////////////////////////////////////////////////////////////////////////*/ @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 9703a3588..bcab92787 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 @@ import java.io.File; } } + @NonNull @Override public DataSource createDataSource() { Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath()); @@ -86,8 +87,8 @@ import java.io.File; 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 8d344c877..cd04bc2eb 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 class MediaSessionManager { 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 5139ef9cd..1a55c21c3 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.os.Bundle; 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.fragment.app.DialogFragment; 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 class PlaybackParameterDialog extends DialogFragment { dialog.tempo = playbackTempo; dialog.pitch = playbackPitch; + dialog.semitones = dialog.percentToSemitones(playbackPitch); dialog.initialSkipSilence = playbackSkipSilence; return dialog; @@ -111,7 +126,7 @@ public class PlaybackParameterDialog extends DialogFragment { //////////////////////////////////////////////////////////////////////////*/ @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 class PlaybackParameterDialog extends DialogFragment { 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 class PlaybackParameterDialog extends DialogFragment { .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 class PlaybackParameterDialog extends DialogFragment { 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 @@ public class PlaybackParameterDialog extends DialogFragment { } } + 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 @@ public class PlaybackParameterDialog extends DialogFragment { } } + 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 @@ public class PlaybackParameterDialog extends DialogFragment { } } + 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 @@ public class PlaybackParameterDialog extends DialogFragment { 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 class PlaybackParameterDialog extends DialogFragment { 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 class PlaybackParameterDialog extends DialogFragment { 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 @@ public class PlaybackParameterDialog extends DialogFragment { } private void onPitchSliderUpdated(final double newPitch) { - if (unhookingCheckbox == null) { - return; - } if (!unhookingCheckbox.isChecked()) { setSliders(newPitch); } else { @@ -420,6 +557,10 @@ public class PlaybackParameterDialog extends DialogFragment { } } + private void onSemitoneSliderUpdated(final int newSemitone) { + setSemitoneSlider(newSemitone); + } + private void setSliders(final double newValue) { setTempoSlider(newValue); setPitchSlider(newValue); @@ -439,25 +580,49 @@ public class PlaybackParameterDialog extends DialogFragment { 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 @@ public class PlaybackParameterDialog extends DialogFragment { 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 @@ public class PlaybackParameterDialog extends DialogFragment { 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 a2f0d7149..d7a9ffc3d 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 class PlayerDataSource { 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 c51b6d5dd..6a7c27bdc 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 androidx.preference.PreferenceManager; 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 final class PlayerHelper { 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 000000000..52eff5a1c --- /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 000000000..b103ac0e6 --- /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 9dcb12344..ee0a6f118 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 class PlayerMediaSession implements MediaSessionCallback { @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 07c8d9f90..df2747c3b 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 android.util.Log; 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 @@ abstract class AbstractInfoPlayQueue ext } 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 @@ abstract class AbstractInfoPlayQueue ext }; } - 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 @@ abstract class AbstractInfoPlayQueue ext } @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 @@ abstract class AbstractInfoPlayQueue ext } 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 f85349797..1e1fef85e 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 @@ package org.schabi.newpipe.player.playqueue; 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 java.util.List; 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 f2259b120..f46c9d72f 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 abstract class PlayQueue implements Serializable { 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 f7dfc562e..bf31ea9b1 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 ac5dce9ba..01883d7d9 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 @@ package org.schabi.newpipe.player.playqueue; 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 java.util.List; 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 245a85e71..11949f55d 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 org.schabi.newpipe.util.ListHelper; 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 class VideoPlaybackResolver implements PlaybackResolver { 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 class VideoPlaybackResolver implements PlaybackResolver { // 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 class VideoPlaybackResolver implements PlaybackResolver { 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 class VideoPlaybackResolver implements PlaybackResolver { } } + /** + * 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 e08562908..70ac5cdcc 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 class AppearanceSettingsFragment extends BasePreferenceFragment { @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 fe327e1b5..ec98b865e 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 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } @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 d7fb559d6..3776d78f6 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 android.view.MenuItem; 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 class MainSettingsFragment extends BasePreferenceFragment { 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 dfc053a62..5767d266f 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 android.widget.Toast; 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.disposables.Disposable; 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 class PeertubeInstanceListFragment extends Fragment { 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 @@ public class PeertubeInstanceListFragment extends Fragment { .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 116807cbc..0f25be630 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.os.Bundle; 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 org.schabi.newpipe.util.ThemeHelper; 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 class SelectChannelFragment extends DialogFragment { 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 7510bb3bc..3ee6668bf 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 androidx.preference.PreferenceFragmentCompat; 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.PreferenceSearchResultListen 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 class SettingsActivity extends AppCompatActivity implements } @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 @@ public class SettingsActivity extends AppCompatActivity implements */ 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 1d4badcac..78ddb3786 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 final class SettingsResourceRegistry { 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 04bad3815..1043e88c2 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 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { // 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 14801c01c..f0b89c677 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 045e574be..62455d682 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 class NotificationActionsPreference extends Preference { //////////////////////////////////////////////////////////////////////////// @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 5835dcab5..a445ea309 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.PreferenceCategory; 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 @@ import java.util.stream.Stream; 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 52935ef8e..98d2a5d84 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 class 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 class PreferenceSearchItem { 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 490e299bd..73aec4a7b 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.os.Bundle; 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 android.widget.TextView; 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 class ChooseTabsFragment extends Fragment { 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 @@ public class ChooseTabsFragment extends Fragment { .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 aa03bbfa6..6b1d70a86 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.annotation.Nullable; 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 abstract class Tab { // 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 abstract class Tab { jsonSink.end(); } - protected void writeDataToJson(final JsonSink writerSink) { + protected void writeDataToJson(final JsonStringWriter writerSink) { // No-op } @@ -340,7 +340,7 @@ public abstract class Tab { @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 abstract class Tab { } @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 abstract class Tab { } @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 abstract class Tab { @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 abstract class Tab { } @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 ca3da9d24..889cc85e6 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 class Mp4FromDashWriter { 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 ebae3812c..2b69f23ac 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 class WebMWriter implements Closeable { 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 b6f1eaf49..000000000 --- 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 af94e3366..27009efd1 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 androidx.preference.PreferenceManager; 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.Page; 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.search.SearchInfo; 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.Maybe; 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 final class ExtractorHelper { .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 final class ExtractorHelper { 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 final class ExtractorHelper { 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 final class ExtractorHelper { 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 final class ExtractorHelper { 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 f77aa0fda..b8c2ff236 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 final class KioskTranslator { } } - 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 fd50d2edb..000000000 --- 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 eb3c21827..c3ccef87c 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.Context; 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.Arrays; 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 final class ListHelper { * 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 final class ListHelper { 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 @@ public final class ListHelper { * 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<>(); + @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<>(); - - 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); - } - } - // 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 @@ public final class ListHelper { * 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 @@ public final class ListHelper { * @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 @@ public final class ListHelper { * @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; } - return result; + + 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 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 80d1b25ae..e55114a2d 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.exceptions.ExtractionException; 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 final class NavigationHelper { // 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 final class NavigationHelper { 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 final class NavigationHelper { 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 final class NavigationHelper { 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 final class NavigationHelper { .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 000000000..21a9059e2 --- /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 3c7b1ce91..000000000 --- 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 9d97e013a..b4c196ce4 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 final class SerializedCache { 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 final class SerializedCache { } @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 000000000..a79085fc0 --- /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 000000000..5daf7f073 --- /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 1b4c8046c..000000000 --- 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 9150b5c1a..c89da9d45 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 android.widget.TextView; 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 @@ public class StreamItemAdapter extends BaseA } 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 @@ public class StreamItemAdapter extends BaseA 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 6801f24ef..0df579d88 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.R; 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 1219304e1..000000000 --- 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 d209d24da..8472653fb 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 dda2d6dee..b5fc0297c 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 class MissionsFragment extends Fragment { 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 class MissionsFragment extends Fragment { } } + 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 bbda803b0..000000000 --- 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 2f2cfe3e3..000000000 --- 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 2d7d796f7..000000000 --- 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 b7c728783..000000000 --- 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 c66bb4051..000000000 --- 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 fcc1ab160..000000000 --- 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 29259b0e0..000000000 --- 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 2e919f18d..000000000 --- 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 e1a204a29..000000000 --- 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 eabaddaee..000000000 --- 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 61a1f61fe..000000000 --- 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 9375e3116..000000000 --- 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 c63eeb597..000000000 --- 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 67e870456..000000000 --- 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 68f85594d..000000000 --- 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 fc11eba57..000000000 --- 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 3760de238..000000000 --- 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 90c7f7a77..000000000 --- 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 26404bddb..000000000 --- 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 bb657f6ec..000000000 --- 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 a6d3b5270..000000000 --- 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 b6a470043..000000000 --- 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 a910c5429..000000000 --- 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 ddb9b6257..000000000 --- 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 efc717ee9..000000000 --- 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 97bdac0f1..000000000 --- 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 2df495e15..000000000 --- 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 892def491..000000000 --- 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 f23764766..000000000 --- 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 04c1c00fc..000000000 --- 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 2418fd6f9..000000000 --- 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 12afe9051..000000000 --- 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 085665e4b..000000000 --- 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 de8e66530..000000000 --- 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 9b97aa592..000000000 --- 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 4fd341d82..000000000 --- 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 303858f9d..000000000 --- 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 c0c92fcc7..000000000 --- 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 19703e8e7..000000000 --- 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 4ffd8b451..000000000 --- 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 79f93d1c1..000000000 --- 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 ca80ad5ad..000000000 --- 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 8edcceb76..000000000 --- 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 ea843aff3..000000000 --- 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 8b925badc..000000000 --- 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 5efaaf0dd..000000000 --- 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 14373a3c5..000000000 --- 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 1b01f3233..000000000 --- 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 95cace1c8..000000000 --- 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 bf86fd24a..000000000 --- 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 a69d284a1..000000000 --- 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 6ae97422a..000000000 --- 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 d0902426b..000000000 --- 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 4ca5e73a7..000000000 --- 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 dbb849680..000000000 --- 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 193f4fe92..000000000 --- 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 b32b11451..000000000 --- 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 dc16c4782..000000000 --- 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 4d0f18584..000000000 --- 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 856433e41..000000000 --- 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 157734911..000000000 --- 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 61ee02ee0..000000000 --- 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 9dad7b85f..000000000 --- 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 75c330cef..000000000 --- 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 484be5ad2..000000000 --- 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 135980afe..000000000 --- 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 86468f34a..000000000 --- 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 1ee3ed018..000000000 --- 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 c4e387866..000000000 --- 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 ca4eb654b..000000000 --- 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 b9d14869b..000000000 --- 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 4861bf809..000000000 --- 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 689f3f47c..000000000 --- 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 e02f1d191..000000000 --- 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 a2cabcee0..000000000 --- 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 5d604f823..000000000 --- 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 ff93ce2d7..000000000 --- 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 12a5d9774..000000000 --- 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 935ac8450..000000000 --- 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 8af0219f9..000000000 --- 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 fedd077d8..fc2163f43 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 1596099f3..0d79d6918 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 b8c4ab12e..b800b1743 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 2d68f797b..5ed19d5fd 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 270637216..da5d30807 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 fdc9dcf8d..df4199d18 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 abfdc203a..7e61e1044 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 840682fee..df7c4b32c 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 dd93a7599..b2c0f5c36 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 200bb7081..cf996d197 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 5bf2e951c..32cd107f7 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 1ff2d2e26..d613ed523 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 1a00ce2dd..498a67ec0 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 853e219bd..1f3952586 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 206702ff2..c7c44ccb2 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 4a0e2ddbb..a368f50f6 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 25a51bb23..5d2ac1665 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 f50fd991b..1d5133364 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 aa051b25d..79c7db8e3 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 7361b7fa6..4bc124a81 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 04eb86a51..6b0e79313 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 d906df150..50ce52f91 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 962e03374..f38c5f130 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 e7ef3d4b5..5b80cbefd 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 328fbe393..b5580ee8d 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 b2fe8bdbd..3bfd9b4c3 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 bda675f14..43b77a9cd 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 416631324..c08695e98 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 e94079fed..2b974c69f 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 ab5ae6c37..4edc96a9b 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 ccc072158..33d9f56ef 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 4d43eafd2..b2a1abdf3 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 17cea9270..87d14880f 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 370bba93d..b4d9e15e9 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 6826b3d5a..e1a2b236b 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 3e2425e40..56670cba6 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 429616ec9..b11666c56 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 a940aa13c..a497da742 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 674aa8def..3eff4b7dd 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 b1d7a2cf5..45955eae7 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 d9f75ea6d..4e21de19d 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 f8bb0b556..48f968b4c 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 59ad4b2d2..f92496779 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 6c6060619..3bbe51917 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 340a4bf0f..8bc821acc 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 1471c52f5..f6538e875 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 1f7957c4a..80fb172aa 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 8b765ffd4..9da90f5a9 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 7b7f19554..1a873cf8b 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 e354f8bda..7684b0673 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 830a7fab1..cc4e5bd10 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 9f3462892..2805ebb26 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 568f8e4de..0356bfe8f 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 603c006db..9cd3ad3fb 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 55495d5a0..db64734ae 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 58e52bf6c..0aadab03d 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 326ff0304..91fd52413 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 dbe3ec664..a70a4ddbb 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 341894ba9..144f123b1 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 192884570..796f37812 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 ca4501bb7..f009ff54e 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 1f9072a36..20af23dde 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 24d9f44f0..fb9ef820b 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 d00231b51..987710fc7 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 51f1145c6..9dccc8ee7 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 ed6228cc2..a73eff527 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 0651fcc6c..26e664589 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 54dc17ddb..6d7e2f0e9 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 d23ea57f8..a889b09e5 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 889ea4c6f..449115e3a 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 e50f6fe3a..1e259c6ad 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 338d95ad5..40971e408 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 18e1b930d..9e361b60d 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 1192dec9f..86717de36 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 b537e982e..a97bebd87 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 35957427d..ac5b9dd19 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 1d997a032..43bf3e16b 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 8077e9325..e3d5ea33b 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 103e5fea3..aa828aa50 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 11d2d25b6..91d860eaf 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 02fa7eb56..01a91b053 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 e0b170300..f833d5e06 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 6c95a5d29..06e530961 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 bcc363279..0fe36fad3 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 2c9151396..b18f6337c 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 7700239a3..420593e04 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 aaaf84983..b5a47789b 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 72952bcaa..34ecad214 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 07965067e..84260ffe4 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 2ee55ea23..014718e60 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 1ee11c49b..851085b5b 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 71a325cf3..000000000 --- a/app/src/main/res/layout-large-land/player.xml +++ /dev/null @@ -1,756 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -